UNPKG

@sgnl-ai/secevent

Version:

A comprehensive JavaScript/TypeScript library for Security Event Tokens (SET) implementing RFC 8417, CAEP, and SSF standards

675 lines (671 loc) 18.2 kB
import { SignJWT, createRemoteJWKSet, decodeJwt, jwtVerify, generateKeyPair, importJWK, importPKCS8, importSPKI } from 'jose'; import { v4 } from 'uuid'; // src/types/subject.ts function isAccountIdentifier(subject) { return subject.format === "account"; } function isEmailIdentifier(subject) { return subject.format === "email"; } function isIssuerSubjectIdentifier(subject) { return subject.format === "iss_sub"; } function isOpaqueIdentifier(subject) { return subject.format === "opaque"; } function isPhoneNumberIdentifier(subject) { return subject.format === "phone_number"; } function isDidIdentifier(subject) { return subject.format === "did"; } function isUriIdentifier(subject) { return subject.format === "uri"; } function isAliasesIdentifier(subject) { return subject.format === "aliases"; } var SubjectIdentifiers = class { static account(uri) { return { format: "account", uri }; } static email(email) { return { format: "email", email }; } static issuerSubject(iss, sub) { return { format: "iss_sub", iss, sub }; } static opaque(id) { return { format: "opaque", id }; } static phoneNumber(phone_number) { return { format: "phone_number", phone_number }; } static did(url) { return { format: "did", url }; } static uri(uri) { return { format: "uri", uri }; } static aliases(...identifiers) { return { format: "aliases", identifiers }; } }; // src/types/events.ts var CAEP_EVENT_TYPES = { SESSION_REVOKED: "https://schemas.openid.net/secevent/caep/event-type/session-revoked", TOKEN_CLAIMS_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/token-claims-change", CREDENTIAL_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/credential-change", ASSURANCE_LEVEL_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/assurance-level-change", DEVICE_COMPLIANCE_CHANGE: "https://schemas.openid.net/secevent/caep/event-type/device-compliance-change" }; var SSF_EVENT_TYPES = { STREAM_UPDATED: "https://schemas.openid.net/secevent/ssf/event-type/stream-updated", VERIFICATION: "https://schemas.openid.net/secevent/ssf/event-type/verification" }; var RISC_EVENT_TYPES = { ACCOUNT_CREDENTIAL_CHANGE_REQUIRED: "https://schemas.openid.net/secevent/risc/event-type/account-credential-change-required", ACCOUNT_PURGED: "https://schemas.openid.net/secevent/risc/event-type/account-purged", ACCOUNT_DISABLED: "https://schemas.openid.net/secevent/risc/event-type/account-disabled", ACCOUNT_ENABLED: "https://schemas.openid.net/secevent/risc/event-type/account-enabled", IDENTIFIER_CHANGED: "https://schemas.openid.net/secevent/risc/event-type/identifier-changed", IDENTIFIER_RECYCLED: "https://schemas.openid.net/secevent/risc/event-type/identifier-recycled", OPT_IN: "https://schemas.openid.net/secevent/risc/event-type/opt-in", OPT_OUT_INITIATED: "https://schemas.openid.net/secevent/risc/event-type/opt-out-initiated", OPT_OUT_CANCELLED: "https://schemas.openid.net/secevent/risc/event-type/opt-out-cancelled", OPT_OUT_EFFECTIVE: "https://schemas.openid.net/secevent/risc/event-type/opt-out-effective", RECOVERY_ACTIVATED: "https://schemas.openid.net/secevent/risc/event-type/recovery-activated", RECOVERY_INFORMATION_CHANGED: "https://schemas.openid.net/secevent/risc/event-type/recovery-information-changed", SESSIONS_REVOKED: "https://schemas.openid.net/secevent/risc/event-type/sessions-revoked" }; var EVENT_TYPES = { ...CAEP_EVENT_TYPES, ...SSF_EVENT_TYPES, ...RISC_EVENT_TYPES }; var Events = class { static sessionRevoked(subject, timestamp, reason) { return { [CAEP_EVENT_TYPES.SESSION_REVOKED]: { subject, event_timestamp: timestamp, reason } }; } static tokenClaimsChange(subject, timestamp, claims) { return { [CAEP_EVENT_TYPES.TOKEN_CLAIMS_CHANGE]: { subject, event_timestamp: timestamp, claims } }; } static credentialChange(subject, timestamp, options) { return { [CAEP_EVENT_TYPES.CREDENTIAL_CHANGE]: { subject, event_timestamp: timestamp, ...options } }; } static assuranceLevelChange(subject, timestamp, options) { return { [CAEP_EVENT_TYPES.ASSURANCE_LEVEL_CHANGE]: { subject, event_timestamp: timestamp, ...options } }; } static deviceComplianceChange(subject, timestamp, options) { return { [CAEP_EVENT_TYPES.DEVICE_COMPLIANCE_CHANGE]: { subject, event_timestamp: timestamp, ...options } }; } static streamUpdated(effectiveTime) { return { [SSF_EVENT_TYPES.STREAM_UPDATED]: { effective_time: effectiveTime } }; } static verification(state) { return { [SSF_EVENT_TYPES.VERIFICATION]: { state } }; } }; var UuidGenerator = class { generate() { return v4(); } }; var TimestampGenerator = class { constructor() { this.counter = 0; } generate() { const timestamp = Date.now(); const count = this.counter++; return `${timestamp}-${count}`; } }; var PrefixedGenerator = class { constructor(prefix, baseGenerator = new UuidGenerator()) { this.prefix = prefix; this.baseGenerator = baseGenerator; } generate() { return `${this.prefix}-${this.baseGenerator.generate()}`; } }; var CustomGenerator = class { constructor(generatorFn) { this.generatorFn = generatorFn; } generate() { return this.generatorFn(); } }; var defaultIdGenerator = new UuidGenerator(); // src/builder/builder.ts var SecEventBuilder = class _SecEventBuilder { constructor(options = {}) { this.options = options; this.events = {}; this.additionalClaims = {}; if (options.defaultIssuer !== void 0) { this.issuer = options.defaultIssuer; } if (options.defaultAudience !== void 0) { this.audience = options.defaultAudience; } if (options.signingKey !== void 0) { this.signingKey = options.signingKey; } } /** * Set the issuer */ withIssuer(issuer) { this.issuer = issuer; return this; } /** * Set the audience */ withAudience(audience) { this.audience = audience; return this; } /** * Set the token ID */ withJti(jti) { this.jti = jti; return this; } /** * Set the issued at timestamp */ withIat(iat) { this.iat = iat; return this; } /** * Set the transaction ID */ withTxn(txn) { this.txn = txn; return this; } /** * Add an event to the token */ withEvent(event) { this.events = { ...this.events, ...event }; return this; } /** * Add multiple events to the token */ withEvents(...events) { events.forEach((event) => { this.events = { ...this.events, ...event }; }); return this; } /** * Add a custom claim */ withClaim(key, value) { this.additionalClaims[key] = value; return this; } /** * Add multiple custom claims */ withClaims(claims) { this.additionalClaims = { ...this.additionalClaims, ...claims }; return this; } /** * Set the signing key */ withSigningKey(key) { this.signingKey = key; return this; } /** * Build the payload without signing */ buildPayload() { if (!this.issuer) { throw new Error("Issuer is required"); } if (Object.keys(this.events).length === 0) { throw new Error("At least one event is required"); } const idGenerator = this.options.idGenerator || defaultIdGenerator; const payload = { iss: this.issuer, jti: this.jti || idGenerator.generate(), iat: this.iat || Math.floor(Date.now() / 1e3), events: this.events, ...this.additionalClaims }; if (this.audience) { payload.aud = this.audience; } if (this.txn) { payload.txn = this.txn; } return payload; } /** * Build and sign the token */ async sign(signingKey) { const key = signingKey || this.signingKey; if (!key) { throw new Error("Signing key is required"); } const payload = this.buildPayload(); let jwt = new SignJWT(payload).setProtectedHeader({ alg: key.alg, typ: "secevent+jwt", ...key.kid && { kid: key.kid } }).setIssuedAt(payload.iat).setIssuer(payload.iss).setJti(payload.jti); if (payload.aud) { jwt = jwt.setAudience(payload.aud); } const token = await jwt.sign(key.key); return { jwt: token, payload }; } /** * Reset the builder */ reset() { if (this.options.defaultIssuer !== void 0) { this.issuer = this.options.defaultIssuer; } else { delete this.issuer; } if (this.options.defaultAudience !== void 0) { this.audience = this.options.defaultAudience; } else { delete this.audience; } delete this.jti; delete this.iat; delete this.txn; this.events = {}; this.additionalClaims = {}; if (this.options.signingKey !== void 0) { this.signingKey = this.options.signingKey; } else { delete this.signingKey; } return this; } /** * Create a new builder with the same configuration */ clone() { const builder = new _SecEventBuilder(this.options); if (this.issuer !== void 0) { builder.issuer = this.issuer; } if (this.audience !== void 0) { builder.audience = this.audience; } if (this.jti !== void 0) { builder.jti = this.jti; } if (this.iat !== void 0) { builder.iat = this.iat; } if (this.txn !== void 0) { builder.txn = this.txn; } builder.events = { ...this.events }; builder.additionalClaims = { ...this.additionalClaims }; if (this.signingKey !== void 0) { builder.signingKey = this.signingKey; } return builder; } }; function createBuilder(options) { return new SecEventBuilder(options); } var SecEventParser = class { constructor(options = {}) { this.options = options; if (options.jwksUrl) { this.jwks = createRemoteJWKSet(new URL(options.jwksUrl)); } } /** * Parse a JWT without verification */ decode(jwt) { const payload = decodeJwt(jwt); this.validatePayloadStructure(payload); return payload; } /** * Parse and verify a signed SET */ async verify(jwt, verificationKey, options) { try { const mergedOptions = { ...this.options.defaultValidationOptions, ...options }; const keys = verificationKey ? Array.isArray(verificationKey) ? verificationKey : [verificationKey] : this.options.verificationKeys; if (!keys && !this.jwks) { throw new Error("No verification key or JWKS URL provided"); } let payload; const jwtOptions = {}; if (mergedOptions.issuer) { jwtOptions.issuer = mergedOptions.issuer; } if (mergedOptions.audience) { jwtOptions.audience = mergedOptions.audience; } if (mergedOptions.clockTolerance) { jwtOptions.clockTolerance = mergedOptions.clockTolerance; } if (mergedOptions.currentDate) { jwtOptions.currentDate = mergedOptions.currentDate; } if (mergedOptions.maxTokenAge) { jwtOptions.maxTokenAge = `${mergedOptions.maxTokenAge}s`; } if (this.jwks) { const result = await jwtVerify(jwt, this.jwks, jwtOptions); payload = result.payload; } else if (keys) { let lastError; for (const key of keys) { try { const result = await jwtVerify( jwt, key.key, jwtOptions ); payload = result.payload; break; } catch (error) { lastError = error; } } if (!payload) { throw lastError || new Error("Verification failed with all provided keys"); } } else { throw new Error("No verification method available"); } const secEventPayload = payload; const validationErrors = this.validateSecEvent(secEventPayload, mergedOptions); if (validationErrors.length > 0) { return { valid: false, errors: validationErrors, error: validationErrors.join("; ") }; } return { valid: true, payload: secEventPayload }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : "Unknown error" }; } } /** * Validate SET payload structure */ validatePayloadStructure(payload) { if (!payload.events || typeof payload.events !== "object") { throw new Error("Missing or invalid events claim"); } if (!payload.iss || typeof payload.iss !== "string") { throw new Error("Missing or invalid issuer claim"); } if (!payload.jti || typeof payload.jti !== "string") { throw new Error("Missing or invalid jti claim"); } if (typeof payload.iat !== "number") { throw new Error("Missing or invalid iat claim"); } } /** * Validate SET-specific requirements */ validateSecEvent(payload, options) { const errors = []; try { this.validatePayloadStructure(payload); } catch (error) { errors.push(error instanceof Error ? error.message : "Invalid payload structure"); } if (options.requiredClaims) { for (const claim of options.requiredClaims) { if (!(claim in payload)) { errors.push(`Missing required claim: ${claim}`); } } } if (payload.events) { const eventCount = Object.keys(payload.events).length; if (eventCount === 0) { errors.push("No events present in events claim"); } for (const eventUri of Object.keys(payload.events)) { if (!this.isValidEventUri(eventUri)) { errors.push(`Invalid event URI format: ${eventUri}`); } } } if (payload.sub) ; return errors; } /** * Check if an event URI is valid */ isValidEventUri(uri) { try { const url = new URL(uri); return url.protocol === "https:" || url.protocol === "http:"; } catch { return false; } } /** * Extract events from a decoded payload */ extractEvents(payload) { return payload.events; } /** * Extract a specific event type from a payload */ extractEvent(payload, eventType) { return payload.events[eventType]; } /** * Check if a payload contains a specific event type */ hasEvent(payload, eventType) { return eventType in payload.events; } /** * Get all event types in a payload */ getEventTypes(payload) { return Object.keys(payload.events); } }; function createParser(options) { return new SecEventParser(options); } var Algorithm = /* @__PURE__ */ ((Algorithm2) => { Algorithm2["RS256"] = "RS256"; Algorithm2["RS384"] = "RS384"; Algorithm2["RS512"] = "RS512"; Algorithm2["PS256"] = "PS256"; Algorithm2["PS384"] = "PS384"; Algorithm2["PS512"] = "PS512"; Algorithm2["ES256"] = "ES256"; Algorithm2["ES384"] = "ES384"; Algorithm2["ES512"] = "ES512"; Algorithm2["HS256"] = "HS256"; Algorithm2["HS384"] = "HS384"; Algorithm2["HS512"] = "HS512"; return Algorithm2; })(Algorithm || {}); var KeyManager = class { constructor() { this.keys = /* @__PURE__ */ new Map(); } /** * Add a key to the manager */ addKey(kid, key) { this.keys.set(kid, { ...key, kid }); } /** * Get a key by ID */ getKey(kid) { return this.keys.get(kid); } /** * Get all keys */ getAllKeys() { return Array.from(this.keys.values()); } /** * Remove a key */ removeKey(kid) { return this.keys.delete(kid); } /** * Clear all keys */ clear() { this.keys.clear(); } /** * Get the default key (first key added) */ getDefaultKey() { const firstKey = this.keys.values().next(); return firstKey.done ? void 0 : firstKey.value; } }; var SigningUtils = class { /** * Generate a new key pair */ static async generateKeyPair(alg = "RS256" /* RS256 */, options) { return generateKeyPair(alg, options); } /** * Import a JWK */ static async importJWK(jwk, alg) { const result = await importJWK(jwk, alg); return result; } /** * Import a PKCS8 private key */ static async importPrivateKey(pkcs8, alg, options) { return importPKCS8(pkcs8, alg, options); } /** * Import a SPKI public key */ static async importPublicKey(spki, alg, options) { return importSPKI(spki, alg, options); } /** * Create a signing key from various formats */ static async createSigningKey(key, alg, kid) { let keyLike; if (typeof key === "string") { if (key.includes("PRIVATE KEY")) { keyLike = await this.importPrivateKey(key, alg); } else if (key.includes("PUBLIC KEY")) { keyLike = await this.importPublicKey(key, alg); } else { keyLike = new TextEncoder().encode(key); } } else if (typeof key === "object" && !("type" in key)) { keyLike = await this.importJWK(key, alg); } else { keyLike = key; } return { key: keyLike, alg, ...kid !== void 0 && { kid } }; } /** * Create a symmetric signing key */ static createSymmetricKey(secret, alg = "HS256" /* HS256 */, kid) { return { key: new TextEncoder().encode(secret), alg, ...kid !== void 0 && { kid } }; } }; var defaultKeyManager = new KeyManager(); export { Algorithm, CAEP_EVENT_TYPES, CustomGenerator, EVENT_TYPES, Events, KeyManager, PrefixedGenerator, RISC_EVENT_TYPES, SSF_EVENT_TYPES, SecEventBuilder, SecEventParser, SigningUtils, SubjectIdentifiers, TimestampGenerator, UuidGenerator, createBuilder, createParser, defaultIdGenerator, defaultKeyManager, isAccountIdentifier, isAliasesIdentifier, isDidIdentifier, isEmailIdentifier, isIssuerSubjectIdentifier, isOpaqueIdentifier, isPhoneNumberIdentifier, isUriIdentifier }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map