@sgnl-ai/secevent
Version:
A comprehensive JavaScript/TypeScript library for Security Event Tokens (SET) implementing RFC 8417, CAEP, and SSF standards
703 lines (698 loc) • 18.9 kB
JavaScript
'use strict';
var jose = require('jose');
var uuid = require('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 uuid.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 jose.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 = jose.createRemoteJWKSet(new URL(options.jwksUrl));
}
}
/**
* Parse a JWT without verification
*/
decode(jwt) {
const payload = jose.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 jose.jwtVerify(jwt, this.jwks, jwtOptions);
payload = result.payload;
} else if (keys) {
let lastError;
for (const key of keys) {
try {
const result = await jose.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 jose.generateKeyPair(alg, options);
}
/**
* Import a JWK
*/
static async importJWK(jwk, alg) {
const result = await jose.importJWK(jwk, alg);
return result;
}
/**
* Import a PKCS8 private key
*/
static async importPrivateKey(pkcs8, alg, options) {
return jose.importPKCS8(pkcs8, alg, options);
}
/**
* Import a SPKI public key
*/
static async importPublicKey(spki, alg, options) {
return jose.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();
exports.Algorithm = Algorithm;
exports.CAEP_EVENT_TYPES = CAEP_EVENT_TYPES;
exports.CustomGenerator = CustomGenerator;
exports.EVENT_TYPES = EVENT_TYPES;
exports.Events = Events;
exports.KeyManager = KeyManager;
exports.PrefixedGenerator = PrefixedGenerator;
exports.RISC_EVENT_TYPES = RISC_EVENT_TYPES;
exports.SSF_EVENT_TYPES = SSF_EVENT_TYPES;
exports.SecEventBuilder = SecEventBuilder;
exports.SecEventParser = SecEventParser;
exports.SigningUtils = SigningUtils;
exports.SubjectIdentifiers = SubjectIdentifiers;
exports.TimestampGenerator = TimestampGenerator;
exports.UuidGenerator = UuidGenerator;
exports.createBuilder = createBuilder;
exports.createParser = createParser;
exports.defaultIdGenerator = defaultIdGenerator;
exports.defaultKeyManager = defaultKeyManager;
exports.isAccountIdentifier = isAccountIdentifier;
exports.isAliasesIdentifier = isAliasesIdentifier;
exports.isDidIdentifier = isDidIdentifier;
exports.isEmailIdentifier = isEmailIdentifier;
exports.isIssuerSubjectIdentifier = isIssuerSubjectIdentifier;
exports.isOpaqueIdentifier = isOpaqueIdentifier;
exports.isPhoneNumberIdentifier = isPhoneNumberIdentifier;
exports.isUriIdentifier = isUriIdentifier;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map