@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
text/typescript
/**
* 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);
}