@blamejs/core 0.7.106 → 0.8.0
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/CHANGELOG.md +19 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +16 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit.js +6 -0
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- package/lib/auth/sd-jwt-vc-disclosure.js +95 -0
- package/lib/auth/sd-jwt-vc-holder.js +203 -0
- package/lib/auth/sd-jwt-vc-issuer.js +197 -0
- package/lib/auth/sd-jwt-vc.js +526 -0
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/compliance-ai-act-logging.js +186 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +2 -0
- package/lib/crypto.js +32 -0
- package/lib/flag-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- package/lib/inbox.js +367 -0
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-step-up.js +186 -0
- package/lib/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/pqc-software.js +195 -0
- package/lib/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +829 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SD-JWT VC disclosure encoding/decoding helper.
|
|
4
|
+
*
|
|
5
|
+
* Per draft-ietf-oauth-sd-jwt-vc §4.1, a disclosure is the
|
|
6
|
+
* base64url-encoded JSON serialisation of one of:
|
|
7
|
+
*
|
|
8
|
+
* For object members: [<salt>, <claim_name>, <claim_value>]
|
|
9
|
+
* For array elements: [<salt>, <array_element>]
|
|
10
|
+
*
|
|
11
|
+
* The salt is a 128-bit random value (base64url) preventing dictionary
|
|
12
|
+
* attacks on the disclosure digests. Operators that need deterministic
|
|
13
|
+
* salt for testing pass opts.saltSource.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
var nodeCrypto = require("node:crypto");
|
|
17
|
+
var safeJson = require("../safe-json");
|
|
18
|
+
var { AuthError } = require("../framework-error");
|
|
19
|
+
|
|
20
|
+
var DEFAULT_SALT_BYTES = 16; // allow:raw-byte-literal — 128-bit salt per spec recommendation
|
|
21
|
+
|
|
22
|
+
function _newSalt(opts) {
|
|
23
|
+
if (opts && opts.saltSource && typeof opts.saltSource === "function") {
|
|
24
|
+
var s = opts.saltSource();
|
|
25
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
26
|
+
throw new AuthError("auth-sd-jwt-vc/bad-salt",
|
|
27
|
+
"saltSource must return a non-empty string");
|
|
28
|
+
}
|
|
29
|
+
return s;
|
|
30
|
+
}
|
|
31
|
+
var bytes = nodeCrypto.randomBytes(DEFAULT_SALT_BYTES);
|
|
32
|
+
return bytes.toString("base64url");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function encode(claimName, claimValue, opts) {
|
|
36
|
+
if (typeof claimName !== "string" || claimName.length === 0) {
|
|
37
|
+
throw new AuthError("auth-sd-jwt-vc/bad-claim-name",
|
|
38
|
+
"encode: claimName must be a non-empty string");
|
|
39
|
+
}
|
|
40
|
+
if (claimValue === undefined) {
|
|
41
|
+
throw new AuthError("auth-sd-jwt-vc/bad-claim-value",
|
|
42
|
+
"encode: claimValue must not be undefined");
|
|
43
|
+
}
|
|
44
|
+
var salt = _newSalt(opts);
|
|
45
|
+
var arr = [salt, claimName, claimValue];
|
|
46
|
+
var jsonStr = safeJson.stringify(arr);
|
|
47
|
+
return Buffer.from(jsonStr, "utf8").toString("base64url");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function encodeArrayElement(elementValue, opts) {
|
|
51
|
+
if (elementValue === undefined) {
|
|
52
|
+
throw new AuthError("auth-sd-jwt-vc/bad-element-value",
|
|
53
|
+
"encodeArrayElement: elementValue must not be undefined");
|
|
54
|
+
}
|
|
55
|
+
var salt = _newSalt(opts);
|
|
56
|
+
var arr = [salt, elementValue];
|
|
57
|
+
var jsonStr = safeJson.stringify(arr);
|
|
58
|
+
return Buffer.from(jsonStr, "utf8").toString("base64url");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decode(disclosureStr) {
|
|
62
|
+
if (typeof disclosureStr !== "string" || disclosureStr.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
var jsonStr;
|
|
66
|
+
try { jsonStr = Buffer.from(disclosureStr, "base64url").toString("utf8"); }
|
|
67
|
+
catch (_e) { return null; }
|
|
68
|
+
var parsed;
|
|
69
|
+
try { parsed = safeJson.parse(jsonStr, { maxBytes: 64 * 1024 }); } // allow:raw-byte-literal — disclosure cap (64 KB)
|
|
70
|
+
catch (_e) { return null; }
|
|
71
|
+
if (!Array.isArray(parsed)) return null;
|
|
72
|
+
if (parsed.length === 3) {
|
|
73
|
+
return {
|
|
74
|
+
salt: parsed[0],
|
|
75
|
+
name: parsed[1],
|
|
76
|
+
value: parsed[2],
|
|
77
|
+
isElement: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (parsed.length === 2) {
|
|
81
|
+
return {
|
|
82
|
+
salt: parsed[0],
|
|
83
|
+
value: parsed[1],
|
|
84
|
+
isElement: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
encode: encode,
|
|
92
|
+
encodeArrayElement: encodeArrayElement,
|
|
93
|
+
decode: decode,
|
|
94
|
+
DEFAULT_SALT_BYTES: DEFAULT_SALT_BYTES,
|
|
95
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.auth.sdJwtVc.holder — operator-side holder/wallet helper.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the lower-level present() function with key-management +
|
|
6
|
+
* stored-credential lookup + per-presentation audit emission.
|
|
7
|
+
* Operators running a wallet (mobile app backend, OIDC4VP relying
|
|
8
|
+
* party with holder role) instantiate one of these per holder.
|
|
9
|
+
*
|
|
10
|
+
* var holder = b.auth.sdJwtVc.holder.create({
|
|
11
|
+
* storage: holderStorageBackend, // operator-side persistence
|
|
12
|
+
* holderKey: keyPemOrJwk,
|
|
13
|
+
* algorithm: "ES256",
|
|
14
|
+
* auditOn: true,
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Save a credential the wallet just received from an issuer
|
|
18
|
+
* await holder.store({
|
|
19
|
+
* id: "cred-1",
|
|
20
|
+
* sdJwt: receivedFromIssuer.token,
|
|
21
|
+
* vct: "https://example.com/vct/identity",
|
|
22
|
+
* issuer: "https://issuer.example.com",
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Build a presentation for a verifier request
|
|
26
|
+
* var presentation = await holder.present({
|
|
27
|
+
* credentialId: "cred-1",
|
|
28
|
+
* disclosedClaimNames: ["given_name"],
|
|
29
|
+
* audience: "https://verifier.example.com",
|
|
30
|
+
* nonce: nonceFromVerifier,
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // List stored credentials
|
|
34
|
+
* var creds = await holder.list();
|
|
35
|
+
*
|
|
36
|
+
* // Delete a credential (on revocation / user request / DSR erasure)
|
|
37
|
+
* await holder.delete("cred-1");
|
|
38
|
+
*
|
|
39
|
+
* Storage shape (operator implements):
|
|
40
|
+
* { put(id, record), get(id), list(), delete(id) }
|
|
41
|
+
*
|
|
42
|
+
* The framework also ships `memoryStorage()` for development /
|
|
43
|
+
* tests — production operators wire b.db / b.objectStore.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
var lazyRequire = require("../lazy-require");
|
|
47
|
+
var validateOpts = require("../validate-opts");
|
|
48
|
+
var { AuthError } = require("../framework-error");
|
|
49
|
+
|
|
50
|
+
var sdJwtVcCore = lazyRequire(function () { return require("./sd-jwt-vc"); });
|
|
51
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
52
|
+
var observability = lazyRequire(function () { return require("../observability"); });
|
|
53
|
+
|
|
54
|
+
function _validateStorage(storage) {
|
|
55
|
+
if (!storage || typeof storage !== "object") return false;
|
|
56
|
+
return ["put", "get", "list", "delete"].every(function (m) {
|
|
57
|
+
return typeof storage[m] === "function";
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function memoryStorage() {
|
|
62
|
+
var byId = new Map();
|
|
63
|
+
return {
|
|
64
|
+
put: async function (id, record) {
|
|
65
|
+
byId.set(id, Object.assign({}, record, { id: id }));
|
|
66
|
+
},
|
|
67
|
+
get: async function (id) {
|
|
68
|
+
var r = byId.get(id);
|
|
69
|
+
return r ? Object.assign({}, r) : null;
|
|
70
|
+
},
|
|
71
|
+
list: async function () {
|
|
72
|
+
return Array.from(byId.values()).map(function (r) {
|
|
73
|
+
return Object.assign({}, r);
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
delete: async function (id) {
|
|
77
|
+
var existed = byId.has(id);
|
|
78
|
+
byId.delete(id);
|
|
79
|
+
return existed;
|
|
80
|
+
},
|
|
81
|
+
_size: function () { return byId.size; },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function create(opts) {
|
|
86
|
+
validateOpts.requireObject(opts, "auth.sdJwtVc.holder.create", AuthError);
|
|
87
|
+
validateOpts(opts, [
|
|
88
|
+
"storage", "holderKey", "algorithm", "auditOn",
|
|
89
|
+
], "auth.sdJwtVc.holder.create");
|
|
90
|
+
|
|
91
|
+
if (!_validateStorage(opts.storage)) {
|
|
92
|
+
throw new AuthError("auth-sd-jwt-vc/bad-storage",
|
|
93
|
+
"holder.create: storage must implement { put, get, list, delete }");
|
|
94
|
+
}
|
|
95
|
+
if (!opts.holderKey) {
|
|
96
|
+
throw new AuthError("auth-sd-jwt-vc/no-key",
|
|
97
|
+
"holder.create: holderKey required");
|
|
98
|
+
}
|
|
99
|
+
var algorithm = opts.algorithm || "ES256";
|
|
100
|
+
var auditOn = opts.auditOn !== false;
|
|
101
|
+
|
|
102
|
+
function _emitAudit(action, outcome, metadata) {
|
|
103
|
+
if (!auditOn) return;
|
|
104
|
+
try {
|
|
105
|
+
audit().safeEmit({
|
|
106
|
+
action: action,
|
|
107
|
+
outcome: outcome,
|
|
108
|
+
metadata: metadata || {},
|
|
109
|
+
});
|
|
110
|
+
} catch (_e) { /* drop-silent */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _emitMetric(verb) {
|
|
114
|
+
try { observability().safeEvent("auth.sdJwtVc.holder." + verb, 1, {}); }
|
|
115
|
+
catch (_e) { /* drop-silent */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function store(spec) {
|
|
119
|
+
if (!spec || typeof spec !== "object") {
|
|
120
|
+
throw new AuthError("auth-sd-jwt-vc/bad-spec",
|
|
121
|
+
"holder.store: spec must be an object");
|
|
122
|
+
}
|
|
123
|
+
if (typeof spec.id !== "string" || spec.id.length === 0) {
|
|
124
|
+
throw new AuthError("auth-sd-jwt-vc/bad-id",
|
|
125
|
+
"holder.store: id is required");
|
|
126
|
+
}
|
|
127
|
+
if (typeof spec.sdJwt !== "string") {
|
|
128
|
+
throw new AuthError("auth-sd-jwt-vc/bad-token",
|
|
129
|
+
"holder.store: sdJwt is required");
|
|
130
|
+
}
|
|
131
|
+
var record = {
|
|
132
|
+
id: spec.id,
|
|
133
|
+
sdJwt: spec.sdJwt,
|
|
134
|
+
vct: spec.vct || null,
|
|
135
|
+
issuer: spec.issuer || null,
|
|
136
|
+
receivedAt: Date.now(),
|
|
137
|
+
};
|
|
138
|
+
await opts.storage.put(spec.id, record);
|
|
139
|
+
_emitAudit("auth.sdJwtVc.holder.stored", "success", {
|
|
140
|
+
id: spec.id, vct: record.vct, issuer: record.issuer,
|
|
141
|
+
});
|
|
142
|
+
_emitMetric("stored");
|
|
143
|
+
return record;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function present(spec) {
|
|
147
|
+
if (!spec || typeof spec !== "object") {
|
|
148
|
+
throw new AuthError("auth-sd-jwt-vc/bad-spec",
|
|
149
|
+
"holder.present: spec must be an object");
|
|
150
|
+
}
|
|
151
|
+
var record = await opts.storage.get(spec.credentialId);
|
|
152
|
+
if (!record) {
|
|
153
|
+
throw new AuthError("auth-sd-jwt-vc/credential-not-found",
|
|
154
|
+
"holder.present: credentialId \"" + spec.credentialId + "\" not found in storage");
|
|
155
|
+
}
|
|
156
|
+
var presentation = sdJwtVcCore().present({
|
|
157
|
+
sdJwt: record.sdJwt,
|
|
158
|
+
disclosedClaimNames: spec.disclosedClaimNames || [],
|
|
159
|
+
audience: spec.audience || null,
|
|
160
|
+
nonce: spec.nonce || null,
|
|
161
|
+
holderKey: opts.holderKey,
|
|
162
|
+
algorithm: algorithm,
|
|
163
|
+
});
|
|
164
|
+
_emitAudit("auth.sdJwtVc.holder.presented", "success", {
|
|
165
|
+
credentialId: spec.credentialId,
|
|
166
|
+
audience: spec.audience || null,
|
|
167
|
+
disclosed: (spec.disclosedClaimNames || []).length,
|
|
168
|
+
});
|
|
169
|
+
_emitMetric("presented");
|
|
170
|
+
return presentation;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function list() {
|
|
174
|
+
var rows = await opts.storage.list();
|
|
175
|
+
return Array.isArray(rows) ? rows : [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function get(id) {
|
|
179
|
+
return await opts.storage.get(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function _delete(id) {
|
|
183
|
+
var existed = await opts.storage.delete(id);
|
|
184
|
+
if (existed) {
|
|
185
|
+
_emitAudit("auth.sdJwtVc.holder.deleted", "success", { id: id });
|
|
186
|
+
_emitMetric("deleted");
|
|
187
|
+
}
|
|
188
|
+
return existed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
store: store,
|
|
193
|
+
present: present,
|
|
194
|
+
list: list,
|
|
195
|
+
get: get,
|
|
196
|
+
delete: _delete,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
create: create,
|
|
202
|
+
memoryStorage: memoryStorage,
|
|
203
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.auth.sdJwtVc.issuer — operator-side SD-JWT VC issuer factory.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the lower-level issue() function with key-management + key-id
|
|
6
|
+
* (kid) + per-issuance audit emission. Operators running an issuer
|
|
7
|
+
* service (EUDI Wallet provider, OIDC4VCI issuer, internal credential
|
|
8
|
+
* service) instantiate one of these per signing key.
|
|
9
|
+
*
|
|
10
|
+
* var issuer = b.auth.sdJwtVc.issuer.create({
|
|
11
|
+
* issuerUrl: "https://issuer.example.com",
|
|
12
|
+
* keys: [
|
|
13
|
+
* { kid: "issuer-2026-q2", privateKey: pem, algorithm: "ES256" },
|
|
14
|
+
* ],
|
|
15
|
+
* activeKid: "issuer-2026-q2",
|
|
16
|
+
* defaultTtlMs: C.TIME.days(90),
|
|
17
|
+
* auditOn: true,
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* var sdJwt = await issuer.issue({
|
|
21
|
+
* vct: "https://example.com/vct/identity",
|
|
22
|
+
* subject: "did:web:alice",
|
|
23
|
+
* claims: {
|
|
24
|
+
* given_name: "Alice",
|
|
25
|
+
* family_name: "Smith",
|
|
26
|
+
* birthdate: "1990-01-15",
|
|
27
|
+
* },
|
|
28
|
+
* selectivelyDisclosed: ["given_name", "family_name", "birthdate"],
|
|
29
|
+
* holderKey: holderJwk,
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Key rollover (rotate to a new signing key)
|
|
33
|
+
* issuer.rotateKey({ kid: "issuer-2026-q3", privateKey: pem2 });
|
|
34
|
+
*
|
|
35
|
+
* // Operator-side audit / metering
|
|
36
|
+
* var stats = issuer.stats(); // { issued, lastIssuedAt, keys: [...] }
|
|
37
|
+
*
|
|
38
|
+
* Audit emissions (audit namespace `auth`):
|
|
39
|
+
* auth.sdJwtVc.issued — every successful issue() call
|
|
40
|
+
* auth.sdJwtVc.key_rotated — every rotateKey() invocation
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
var C = require("../constants");
|
|
44
|
+
var lazyRequire = require("../lazy-require");
|
|
45
|
+
var validateOpts = require("../validate-opts");
|
|
46
|
+
var { AuthError } = require("../framework-error");
|
|
47
|
+
|
|
48
|
+
// Lazy-required to avoid the issuer ↔ core circular load: sd-jwt-vc.js
|
|
49
|
+
// requires sd-jwt-vc-issuer.js for its module.exports surface, and
|
|
50
|
+
// the issuer needs sd-jwt-vc.js's issue() function for the actual
|
|
51
|
+
// signing path.
|
|
52
|
+
var sdJwtVcCore = lazyRequire(function () { return require("./sd-jwt-vc"); });
|
|
53
|
+
|
|
54
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
55
|
+
var observability = lazyRequire(function () { return require("../observability"); });
|
|
56
|
+
|
|
57
|
+
function create(opts) {
|
|
58
|
+
validateOpts.requireObject(opts, "auth.sdJwtVc.issuer.create", AuthError);
|
|
59
|
+
validateOpts(opts, [
|
|
60
|
+
"issuerUrl", "keys", "activeKid",
|
|
61
|
+
"defaultTtlMs", "defaultHashAlg", "auditOn",
|
|
62
|
+
], "auth.sdJwtVc.issuer.create");
|
|
63
|
+
|
|
64
|
+
validateOpts.requireNonEmptyString(opts.issuerUrl,
|
|
65
|
+
"issuer.create: issuerUrl", AuthError, "auth-sd-jwt-vc/bad-opts");
|
|
66
|
+
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
67
|
+
throw new AuthError("auth-sd-jwt-vc/no-keys",
|
|
68
|
+
"issuer.create: keys must be a non-empty array");
|
|
69
|
+
}
|
|
70
|
+
for (var i = 0; i < opts.keys.length; i++) {
|
|
71
|
+
var k = opts.keys[i];
|
|
72
|
+
if (!k || typeof k !== "object") {
|
|
73
|
+
throw new AuthError("auth-sd-jwt-vc/bad-key",
|
|
74
|
+
"issuer.create: keys[" + i + "] must be an object");
|
|
75
|
+
}
|
|
76
|
+
if (typeof k.kid !== "string" || k.kid.length === 0) {
|
|
77
|
+
throw new AuthError("auth-sd-jwt-vc/bad-key",
|
|
78
|
+
"issuer.create: keys[" + i + "].kid is required");
|
|
79
|
+
}
|
|
80
|
+
if (!k.privateKey) {
|
|
81
|
+
throw new AuthError("auth-sd-jwt-vc/bad-key",
|
|
82
|
+
"issuer.create: keys[" + i + "].privateKey is required");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
validateOpts.optionalPositiveFinite(opts.defaultTtlMs,
|
|
86
|
+
"issuer.create: defaultTtlMs", AuthError, "auth-sd-jwt-vc/bad-opts");
|
|
87
|
+
|
|
88
|
+
var keysByKid = Object.create(null);
|
|
89
|
+
for (var j = 0; j < opts.keys.length; j++) {
|
|
90
|
+
keysByKid[opts.keys[j].kid] = opts.keys[j];
|
|
91
|
+
}
|
|
92
|
+
var activeKid = opts.activeKid || opts.keys[0].kid;
|
|
93
|
+
if (!keysByKid[activeKid]) {
|
|
94
|
+
throw new AuthError("auth-sd-jwt-vc/bad-active-kid",
|
|
95
|
+
"issuer.create: activeKid \"" + activeKid + "\" is not in the keys array");
|
|
96
|
+
}
|
|
97
|
+
var defaultTtlMs = opts.defaultTtlMs || C.TIME.days(90);
|
|
98
|
+
var defaultHashAlg = opts.defaultHashAlg || "sha-256";
|
|
99
|
+
var auditOn = opts.auditOn !== false;
|
|
100
|
+
|
|
101
|
+
var stats = {
|
|
102
|
+
issued: 0,
|
|
103
|
+
lastIssuedAt: null,
|
|
104
|
+
keysRotated: 0,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function _emitAudit(action, outcome, metadata) {
|
|
108
|
+
if (!auditOn) return;
|
|
109
|
+
try {
|
|
110
|
+
audit().safeEmit({
|
|
111
|
+
action: action,
|
|
112
|
+
outcome: outcome,
|
|
113
|
+
metadata: metadata || {},
|
|
114
|
+
});
|
|
115
|
+
} catch (_e) { /* drop-silent */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _emitMetric(verb) {
|
|
119
|
+
try { observability().safeEvent("auth.sdJwtVc.issuer." + verb, 1, {}); }
|
|
120
|
+
catch (_e) { /* drop-silent */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function issue(spec) {
|
|
124
|
+
if (!spec || typeof spec !== "object") {
|
|
125
|
+
throw new AuthError("auth-sd-jwt-vc/bad-spec",
|
|
126
|
+
"issuer.issue: spec must be an object");
|
|
127
|
+
}
|
|
128
|
+
if (typeof spec.vct !== "string") {
|
|
129
|
+
throw new AuthError("auth-sd-jwt-vc/bad-spec",
|
|
130
|
+
"issuer.issue: vct is required");
|
|
131
|
+
}
|
|
132
|
+
var key = keysByKid[activeKid];
|
|
133
|
+
var issued = sdJwtVcCore().issue({
|
|
134
|
+
issuer: opts.issuerUrl,
|
|
135
|
+
subject: spec.subject || null,
|
|
136
|
+
vct: spec.vct,
|
|
137
|
+
claims: spec.claims || {},
|
|
138
|
+
selectivelyDisclosed: spec.selectivelyDisclosed || [],
|
|
139
|
+
issuerKey: key.privateKey,
|
|
140
|
+
algorithm: key.algorithm || "ES256",
|
|
141
|
+
hashAlg: spec.hashAlg || defaultHashAlg,
|
|
142
|
+
ttlMs: spec.ttlMs || defaultTtlMs,
|
|
143
|
+
holderKey: spec.holderKey || null,
|
|
144
|
+
issuedAt: spec.issuedAt,
|
|
145
|
+
extraHeader: { kid: activeKid },
|
|
146
|
+
});
|
|
147
|
+
stats.issued += 1;
|
|
148
|
+
stats.lastIssuedAt = Date.now();
|
|
149
|
+
_emitAudit("auth.sdJwtVc.issued", "success", {
|
|
150
|
+
kid: activeKid,
|
|
151
|
+
vct: spec.vct,
|
|
152
|
+
subject: spec.subject || null,
|
|
153
|
+
disclosed: (spec.selectivelyDisclosed || []).length,
|
|
154
|
+
});
|
|
155
|
+
_emitMetric("issued");
|
|
156
|
+
return issued;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function rotateKey(newKey) {
|
|
160
|
+
if (!newKey || typeof newKey !== "object" ||
|
|
161
|
+
typeof newKey.kid !== "string" || !newKey.privateKey) {
|
|
162
|
+
throw new AuthError("auth-sd-jwt-vc/bad-key",
|
|
163
|
+
"rotateKey: must pass { kid, privateKey, algorithm? }");
|
|
164
|
+
}
|
|
165
|
+
keysByKid[newKey.kid] = newKey;
|
|
166
|
+
activeKid = newKey.kid;
|
|
167
|
+
stats.keysRotated += 1;
|
|
168
|
+
_emitAudit("auth.sdJwtVc.key_rotated", "success", {
|
|
169
|
+
newKid: newKey.kid,
|
|
170
|
+
});
|
|
171
|
+
_emitMetric("key_rotated");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function listKids() { return Object.keys(keysByKid); }
|
|
175
|
+
|
|
176
|
+
function statsSnapshot() {
|
|
177
|
+
return {
|
|
178
|
+
issued: stats.issued,
|
|
179
|
+
lastIssuedAt: stats.lastIssuedAt,
|
|
180
|
+
keysRotated: stats.keysRotated,
|
|
181
|
+
activeKid: activeKid,
|
|
182
|
+
kids: listKids(),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
issue: issue,
|
|
188
|
+
rotateKey: rotateKey,
|
|
189
|
+
listKids: listKids,
|
|
190
|
+
stats: statsSnapshot,
|
|
191
|
+
issuerUrl: opts.issuerUrl,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
create: create,
|
|
197
|
+
};
|