sos-sdk-node-ts 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,12 @@ emergency request over HTTPS with a stable idempotency key and validates the
48
48
  provider acknowledgement. The bearer credential remains only in the trusted
49
49
  Node runtime.
50
50
 
51
+ `sos-sdk-node-ts/bff-email-assertion` lets one authenticated BFF present a
52
+ normalized email to one exact receiving BFF for a short-lived, named purpose.
53
+ The receiving application still authorizes the operation and should reject
54
+ replay where it matters. Never expose the shared secret or let a browser set
55
+ the trusted issuer, audience, purpose or identity claims.
56
+
51
57
  ## Secure-message CSPRNG adapter
52
58
 
53
59
  `sos-sdk-node-ts/secure-message-crypto` exports `NodeSecureRandomSource` and
@@ -0,0 +1,33 @@
1
+ export type BffEmailAssertion = Readonly<{
2
+ issuer: string;
3
+ audience: string;
4
+ purpose: string;
5
+ email: string;
6
+ assertionId: string;
7
+ issuedAt: number;
8
+ expiresAt: number;
9
+ }>;
10
+ type CreateInput = Readonly<{
11
+ issuer: string;
12
+ audience: string;
13
+ purpose: string;
14
+ email: string;
15
+ secret: string;
16
+ now?: Date;
17
+ lifetimeSeconds?: number;
18
+ }>;
19
+ type VerifyInput = Readonly<{
20
+ issuer: string;
21
+ audience: string;
22
+ purpose: string;
23
+ secret: string;
24
+ now?: Date;
25
+ }>;
26
+ /**
27
+ * Issues a minimal, short-lived identity assertion from one trusted BFF to an
28
+ * exact receiving BFF. It is not a browser mode switch or an authorization
29
+ * capability; the receiver must still enforce the requested operation.
30
+ */
31
+ export declare function createBffEmailAssertion(input: CreateInput): string;
32
+ export declare function verifyBffEmailAssertion(assertion: string, input: VerifyInput): BffEmailAssertion;
33
+ export {};
@@ -0,0 +1,113 @@
1
+ import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
2
+ const MAXIMUM_LIFETIME_SECONDS = 5 * 60;
3
+ /**
4
+ * Issues a minimal, short-lived identity assertion from one trusted BFF to an
5
+ * exact receiving BFF. It is not a browser mode switch or an authorization
6
+ * capability; the receiver must still enforce the requested operation.
7
+ */
8
+ export function createBffEmailAssertion(input) {
9
+ const secret = validatedSecret(input.secret);
10
+ const issuedAt = epochSeconds(input.now ?? new Date());
11
+ const lifetimeSeconds = input.lifetimeSeconds ?? 120;
12
+ if (!Number.isInteger(lifetimeSeconds) || lifetimeSeconds < 1 || lifetimeSeconds > MAXIMUM_LIFETIME_SECONDS) {
13
+ throw new Error('bff_assertion_lifetime_invalid');
14
+ }
15
+ const payload = Object.freeze({
16
+ issuer: absoluteUrl(input.issuer, 'bff_assertion_issuer_invalid'),
17
+ audience: absoluteUrl(input.audience, 'bff_assertion_audience_invalid'),
18
+ purpose: required(input.purpose, 'bff_assertion_purpose_invalid'),
19
+ email: normalizedEmail(input.email),
20
+ assertionId: randomUUID(),
21
+ issuedAt,
22
+ expiresAt: issuedAt + lifetimeSeconds,
23
+ });
24
+ const encoded = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
25
+ return `${encoded}.${signature(encoded, secret)}`;
26
+ }
27
+ export function verifyBffEmailAssertion(assertion, input) {
28
+ const secret = validatedSecret(input.secret);
29
+ const [encoded, receivedSignature, extra] = assertion.split('.');
30
+ if (!encoded || !receivedSignature || extra)
31
+ throw new Error('bff_assertion_format_invalid');
32
+ const expectedSignature = signature(encoded, secret);
33
+ const received = Buffer.from(receivedSignature, 'base64url');
34
+ const expected = Buffer.from(expectedSignature, 'base64url');
35
+ if (received.length !== expected.length || !timingSafeEqual(received, expected))
36
+ throw new Error('bff_assertion_signature_invalid');
37
+ const payload = parsePayload(encoded);
38
+ if (payload.issuer !== absoluteUrl(input.issuer, 'bff_assertion_issuer_invalid'))
39
+ throw new Error('bff_assertion_issuer_mismatch');
40
+ if (payload.audience !== absoluteUrl(input.audience, 'bff_assertion_audience_invalid'))
41
+ throw new Error('bff_assertion_audience_mismatch');
42
+ if (payload.purpose !== required(input.purpose, 'bff_assertion_purpose_invalid'))
43
+ throw new Error('bff_assertion_purpose_mismatch');
44
+ const now = epochSeconds(input.now ?? new Date());
45
+ if (payload.expiresAt <= now)
46
+ throw new Error('bff_assertion_expired');
47
+ if (payload.issuedAt > now || payload.expiresAt - payload.issuedAt > MAXIMUM_LIFETIME_SECONDS)
48
+ throw new Error('bff_assertion_time_invalid');
49
+ return payload;
50
+ }
51
+ function parsePayload(encoded) {
52
+ try {
53
+ const value = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
54
+ if (!value || typeof value !== 'object'
55
+ || typeof value.issuer !== 'string' || typeof value.audience !== 'string'
56
+ || typeof value.purpose !== 'string' || typeof value.email !== 'string'
57
+ || typeof value.assertionId !== 'string' || typeof value.issuedAt !== 'number'
58
+ || typeof value.expiresAt !== 'number')
59
+ throw new Error('invalid');
60
+ return Object.freeze({
61
+ issuer: absoluteUrl(value.issuer, 'bff_assertion_payload_invalid'),
62
+ audience: absoluteUrl(value.audience, 'bff_assertion_payload_invalid'),
63
+ purpose: required(value.purpose, 'bff_assertion_payload_invalid'),
64
+ email: normalizedEmail(value.email),
65
+ assertionId: required(value.assertionId, 'bff_assertion_payload_invalid'),
66
+ issuedAt: value.issuedAt,
67
+ expiresAt: value.expiresAt,
68
+ });
69
+ }
70
+ catch (reason) {
71
+ if (reason instanceof Error && reason.message.startsWith('bff_assertion_'))
72
+ throw reason;
73
+ throw new Error('bff_assertion_payload_invalid');
74
+ }
75
+ }
76
+ function signature(encoded, secret) {
77
+ return createHmac('sha256', secret).update(encoded).digest('base64url');
78
+ }
79
+ function validatedSecret(value) {
80
+ const secret = Buffer.from(value, 'utf8');
81
+ if (secret.length < 32)
82
+ throw new Error('bff_assertion_secret_invalid');
83
+ return secret;
84
+ }
85
+ function normalizedEmail(value) {
86
+ const email = value.trim().toLowerCase();
87
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
88
+ throw new Error('bff_assertion_email_invalid');
89
+ return email;
90
+ }
91
+ function required(value, issue) {
92
+ const normalized = value.trim();
93
+ if (!normalized)
94
+ throw new Error(issue);
95
+ return normalized;
96
+ }
97
+ function absoluteUrl(value, issue) {
98
+ try {
99
+ const url = new URL(value);
100
+ if (url.protocol !== 'https:' && url.hostname !== 'localhost')
101
+ throw new Error(issue);
102
+ return url.href.replace(/\/$/, '');
103
+ }
104
+ catch {
105
+ throw new Error(issue);
106
+ }
107
+ }
108
+ function epochSeconds(value) {
109
+ const milliseconds = value.getTime();
110
+ if (!Number.isFinite(milliseconds))
111
+ throw new Error('bff_assertion_time_invalid');
112
+ return Math.floor(milliseconds / 1_000);
113
+ }
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export * from './secure-message-crypto.js';
5
5
  export * from './confidential-channel-cache.js';
6
6
  export * from './bundle-sync-service.js';
7
7
  export * from './encounter-transfer-audience-service.js';
8
+ export * from './bff-email-assertion.js';
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export * from './secure-message-crypto.js';
5
5
  export * from './confidential-channel-cache.js';
6
6
  export * from './bundle-sync-service.js';
7
7
  export * from './encounter-transfer-audience-service.js';
8
+ export * from './bff-email-assertion.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sos-sdk-node-ts",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Product-neutral Node adapters shared by SOSChain, Vet and UHC runtimes",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -39,6 +39,10 @@
39
39
  "./confidential-channel-cache": {
40
40
  "types": "./dist/confidential-channel-cache.d.ts",
41
41
  "default": "./dist/confidential-channel-cache.js"
42
+ },
43
+ "./bff-email-assertion": {
44
+ "types": "./dist/bff-email-assertion.d.ts",
45
+ "default": "./dist/bff-email-assertion.js"
42
46
  }
43
47
  },
44
48
  "files": [