UNPKG

@sgnl-ai/secevent

Version:

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

271 lines (237 loc) 5.86 kB
/** * Security Event Token Builder * Provides a fluent API for constructing SETs */ import { SignJWT } from 'jose'; import { SecEventPayload, SignedSecEvent, IdGenerator, SigningKey } from '../types/secevent'; import { SecurityEvent } from '../types/events'; import { defaultIdGenerator } from '../id/generator'; /** * Builder configuration options */ export interface BuilderOptions { /** * Default issuer for all tokens */ defaultIssuer?: string; /** * Default audience for all tokens */ defaultAudience?: string | string[]; /** * ID generator for JTI values */ idGenerator?: IdGenerator; /** * Default signing key */ signingKey?: SigningKey; } /** * Security Event Token Builder */ export class SecEventBuilder { private issuer?: string; private audience?: string | string[]; private jti?: string; private iat?: number; private txn?: string; private events: SecurityEvent = {}; private additionalClaims: Record<string, unknown> = {}; private signingKey?: SigningKey; constructor(private options: BuilderOptions = {}) { if (options.defaultIssuer !== undefined) { this.issuer = options.defaultIssuer; } if (options.defaultAudience !== undefined) { this.audience = options.defaultAudience; } if (options.signingKey !== undefined) { this.signingKey = options.signingKey; } } /** * Set the issuer */ withIssuer(issuer: string): this { this.issuer = issuer; return this; } /** * Set the audience */ withAudience(audience: string | string[]): this { this.audience = audience; return this; } /** * Set the token ID */ withJti(jti: string): this { this.jti = jti; return this; } /** * Set the issued at timestamp */ withIat(iat: number): this { this.iat = iat; return this; } /** * Set the transaction ID */ withTxn(txn: string): this { this.txn = txn; return this; } /** * Add an event to the token */ withEvent(event: SecurityEvent): this { this.events = { ...this.events, ...event }; return this; } /** * Add multiple events to the token */ withEvents(...events: SecurityEvent[]): this { events.forEach((event) => { this.events = { ...this.events, ...event }; }); return this; } /** * Add a custom claim */ withClaim(key: string, value: unknown): this { this.additionalClaims[key] = value; return this; } /** * Add multiple custom claims */ withClaims(claims: Record<string, unknown>): this { this.additionalClaims = { ...this.additionalClaims, ...claims }; return this; } /** * Set the signing key */ withSigningKey(key: SigningKey): this { this.signingKey = key; return this; } /** * Build the payload without signing */ buildPayload(): SecEventPayload { 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: SecEventPayload = { iss: this.issuer, jti: this.jti || idGenerator.generate(), iat: this.iat || Math.floor(Date.now() / 1000), 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?: SigningKey): Promise<SignedSecEvent> { const key = signingKey || this.signingKey; if (!key) { throw new Error('Signing key is required'); } const payload = this.buildPayload(); let jwt = new SignJWT(payload as unknown as Record<string, unknown>) .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 as Parameters<typeof jwt.sign>[0]); return { jwt: token, payload, }; } /** * Reset the builder */ reset(): this { if (this.options.defaultIssuer !== undefined) { this.issuer = this.options.defaultIssuer; } else { delete this.issuer; } if (this.options.defaultAudience !== undefined) { 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 !== undefined) { this.signingKey = this.options.signingKey; } else { delete this.signingKey; } return this; } /** * Create a new builder with the same configuration */ clone(): SecEventBuilder { const builder = new SecEventBuilder(this.options); if (this.issuer !== undefined) { builder.issuer = this.issuer; } if (this.audience !== undefined) { builder.audience = this.audience; } if (this.jti !== undefined) { builder.jti = this.jti; } if (this.iat !== undefined) { builder.iat = this.iat; } if (this.txn !== undefined) { builder.txn = this.txn; } builder.events = { ...this.events }; builder.additionalClaims = { ...this.additionalClaims }; if (this.signingKey !== undefined) { builder.signingKey = this.signingKey; } return builder; } } /** * Factory function for creating a builder */ export function createBuilder(options?: BuilderOptions): SecEventBuilder { return new SecEventBuilder(options); }