@aep-foundation/service 0.1.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 +16 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/index.cjs +850 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +245 -0
- package/dist/index.d.ts +245 -0
- package/dist/index.js +821 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
AEP_AUTH_SCHEME,
|
|
4
|
+
AEP_BINDINGS,
|
|
5
|
+
AEP_BUILT_IN_GRANT_TYPES,
|
|
6
|
+
AEP_IDENTITY_METHOD_DID_WEB,
|
|
7
|
+
AEP_MEDIA_TYPE,
|
|
8
|
+
AEP_PROBLEM_MEDIA_TYPE,
|
|
9
|
+
AEP_SIGNING_ALGORITHMS,
|
|
10
|
+
AEP_VERSION,
|
|
11
|
+
DEFAULT_HTTP_ENDPOINT_BASE,
|
|
12
|
+
createProblemDetails,
|
|
13
|
+
decodeJwtUnverified,
|
|
14
|
+
parseBuiltInGrantResponse,
|
|
15
|
+
parseClientAssertionClaims,
|
|
16
|
+
parseEnrollRequest,
|
|
17
|
+
parseGrantRequest,
|
|
18
|
+
parseInspectDocument,
|
|
19
|
+
parseRevokeRequest,
|
|
20
|
+
resolveDidWebPublicKey,
|
|
21
|
+
verifyClientAssertionJwt
|
|
22
|
+
} from "@aep-foundation/core";
|
|
23
|
+
function createAepService(options) {
|
|
24
|
+
const inspectDocument = buildInspectDocument(options);
|
|
25
|
+
const commandIdempotencyStore = options.commandIdempotencyStore ?? createInMemoryCommandIdempotencyStore();
|
|
26
|
+
const enrollmentPolicy = options.enrollmentPolicy ?? createStaticEnrollmentPolicy();
|
|
27
|
+
const enrollmentStore = options.enrollmentStore ?? createInMemoryEnrollmentStore();
|
|
28
|
+
const grantHandlers = createGrantHandlerMap(options.grantTypes ?? []);
|
|
29
|
+
const replayStore = options.replayStore ?? createInMemoryClientAssertionReplayStore();
|
|
30
|
+
const signingAlgorithms = [...options.signingAlgorithms ?? AEP_SIGNING_ALGORITHMS];
|
|
31
|
+
const authenticationOptions = () => ({
|
|
32
|
+
replayStore,
|
|
33
|
+
serviceDid: options.serviceDid,
|
|
34
|
+
signingAlgorithms,
|
|
35
|
+
...options.clientAssertion === void 0 ? {} : { config: options.clientAssertion },
|
|
36
|
+
...options.clientAssertionVerifier === void 0 ? {} : { verifier: options.clientAssertionVerifier }
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
enroll: async (request, commandOptions) => {
|
|
40
|
+
const authentication = await authenticateClientAssertion(
|
|
41
|
+
"enroll",
|
|
42
|
+
commandOptions,
|
|
43
|
+
authenticationOptions()
|
|
44
|
+
);
|
|
45
|
+
if (!authentication.ok) {
|
|
46
|
+
return authentication.response;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
if (parseEnrollRequest(request).agent_did !== authentication.agentDid) {
|
|
50
|
+
return problem("not_recognized", "Not recognized", 401);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
return problem("invalid_request", "Invalid request", 400);
|
|
54
|
+
}
|
|
55
|
+
return handleEnrollRequest(request, {
|
|
56
|
+
commandIdempotencyStore,
|
|
57
|
+
store: enrollmentStore,
|
|
58
|
+
...options.clock === void 0 ? {} : { clock: options.clock },
|
|
59
|
+
policy: enrollmentPolicy,
|
|
60
|
+
...commandOptions.idempotencyKey === void 0 ? {} : { idempotencyKey: commandOptions.idempotencyKey }
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
grant: async (request, commandOptions) => {
|
|
64
|
+
const authentication = await authenticateClientAssertion(
|
|
65
|
+
"grant",
|
|
66
|
+
commandOptions,
|
|
67
|
+
authenticationOptions()
|
|
68
|
+
);
|
|
69
|
+
if (!authentication.ok) {
|
|
70
|
+
return authentication.response;
|
|
71
|
+
}
|
|
72
|
+
return handleGrantRequest(request, {
|
|
73
|
+
agentDid: authentication.agentDid,
|
|
74
|
+
commandIdempotencyStore,
|
|
75
|
+
handlers: grantHandlers,
|
|
76
|
+
store: enrollmentStore,
|
|
77
|
+
...commandOptions.idempotencyKey === void 0 ? {} : { idempotencyKey: commandOptions.idempotencyKey }
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
inspectDocument: () => structuredClone(inspectDocument),
|
|
81
|
+
revoke: async (request, commandOptions) => {
|
|
82
|
+
const authentication = await authenticateClientAssertion(
|
|
83
|
+
"revoke",
|
|
84
|
+
commandOptions,
|
|
85
|
+
authenticationOptions()
|
|
86
|
+
);
|
|
87
|
+
if (!authentication.ok) {
|
|
88
|
+
return authentication.response;
|
|
89
|
+
}
|
|
90
|
+
return handleRevokeRequest(request, {
|
|
91
|
+
agentDid: authentication.agentDid,
|
|
92
|
+
commandIdempotencyStore,
|
|
93
|
+
handlers: grantHandlers,
|
|
94
|
+
store: enrollmentStore,
|
|
95
|
+
...commandOptions.idempotencyKey === void 0 ? {} : { idempotencyKey: commandOptions.idempotencyKey }
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
status: async (commandOptions) => {
|
|
99
|
+
const authentication = await authenticateClientAssertion(
|
|
100
|
+
"status",
|
|
101
|
+
commandOptions,
|
|
102
|
+
authenticationOptions()
|
|
103
|
+
);
|
|
104
|
+
if (!authentication.ok) {
|
|
105
|
+
return authentication.response;
|
|
106
|
+
}
|
|
107
|
+
return handleStatusRequest(authentication.agentDid, {
|
|
108
|
+
store: enrollmentStore
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function buildInspectDocument(options) {
|
|
114
|
+
const identityMethods = uniqueBy(options.identityMethods, "method", "identity method").map(
|
|
115
|
+
(definition) => definition.method
|
|
116
|
+
);
|
|
117
|
+
const grantTypes = uniqueBy(options.grantTypes ?? [], "grantType", "grant type");
|
|
118
|
+
const supportedCommands = grantTypes.length > 0 ? ["enroll", "grant", "inspect", "revoke", "status"] : ["enroll", "inspect", "status"];
|
|
119
|
+
if (identityMethods.length === 0) {
|
|
120
|
+
throw new TypeError("AEP Services must enable at least one identity method.");
|
|
121
|
+
}
|
|
122
|
+
const grantTypeConfig = Object.fromEntries(
|
|
123
|
+
grantTypes.filter((definition) => definition.config !== void 0).map((definition) => [definition.grantType, definition.config])
|
|
124
|
+
);
|
|
125
|
+
const document = {
|
|
126
|
+
aep_version: AEP_VERSION,
|
|
127
|
+
bindings: {
|
|
128
|
+
supported: [...AEP_BINDINGS]
|
|
129
|
+
},
|
|
130
|
+
claims: {
|
|
131
|
+
required: [...options.claims?.required ?? []],
|
|
132
|
+
preferred: [...options.claims?.preferred ?? []],
|
|
133
|
+
optional: [...options.claims?.optional ?? []]
|
|
134
|
+
},
|
|
135
|
+
commands: {
|
|
136
|
+
supported: supportedCommands,
|
|
137
|
+
...grantTypes.length > 0 ? {
|
|
138
|
+
grant_types: grantTypes.map((definition) => definition.grantType)
|
|
139
|
+
} : {},
|
|
140
|
+
...Object.keys(grantTypeConfig).length > 0 ? {
|
|
141
|
+
grant_types_config: grantTypeConfig
|
|
142
|
+
} : {}
|
|
143
|
+
},
|
|
144
|
+
core: {
|
|
145
|
+
signing_algorithms: [...options.signingAlgorithms ?? AEP_SIGNING_ALGORITHMS]
|
|
146
|
+
},
|
|
147
|
+
extensions: {
|
|
148
|
+
supported: [...options.extensions ?? []]
|
|
149
|
+
},
|
|
150
|
+
http: {
|
|
151
|
+
endpoint_base: options.endpointBase ?? DEFAULT_HTTP_ENDPOINT_BASE
|
|
152
|
+
},
|
|
153
|
+
identity: {
|
|
154
|
+
methods: identityMethods
|
|
155
|
+
},
|
|
156
|
+
service: {
|
|
157
|
+
did: options.serviceDid
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return parseInspectDocument(document);
|
|
161
|
+
}
|
|
162
|
+
function didWebIdentityMethod() {
|
|
163
|
+
return {
|
|
164
|
+
method: AEP_IDENTITY_METHOD_DID_WEB
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function oauthBearerGrantType(config) {
|
|
168
|
+
return grantType(AEP_BUILT_IN_GRANT_TYPES[0], config);
|
|
169
|
+
}
|
|
170
|
+
function apiKeyGrantType(config) {
|
|
171
|
+
return grantType(AEP_BUILT_IN_GRANT_TYPES[1], config);
|
|
172
|
+
}
|
|
173
|
+
function basicGrantType(config) {
|
|
174
|
+
return grantType(AEP_BUILT_IN_GRANT_TYPES[2], config);
|
|
175
|
+
}
|
|
176
|
+
function grantType(grantType2, config, handler) {
|
|
177
|
+
return {
|
|
178
|
+
grantType: grantType2,
|
|
179
|
+
...config === void 0 ? {} : { config },
|
|
180
|
+
...handler === void 0 ? {} : { handler }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function storedOAuthBearerGrantType(options) {
|
|
184
|
+
return storedBuiltInGrantType(AEP_BUILT_IN_GRANT_TYPES[0], options);
|
|
185
|
+
}
|
|
186
|
+
function storedApiKeyGrantType(options) {
|
|
187
|
+
return storedBuiltInGrantType(AEP_BUILT_IN_GRANT_TYPES[1], options);
|
|
188
|
+
}
|
|
189
|
+
function storedBasicGrantType(options) {
|
|
190
|
+
return storedBuiltInGrantType(AEP_BUILT_IN_GRANT_TYPES[2], options);
|
|
191
|
+
}
|
|
192
|
+
function createInMemoryEnrollmentStore(records = []) {
|
|
193
|
+
const enrollments = /* @__PURE__ */ new Map();
|
|
194
|
+
records.forEach((record) => enrollments.set(record.agentDid, cloneEnrollmentRecord(record)));
|
|
195
|
+
return {
|
|
196
|
+
findEnrollment(agentDid) {
|
|
197
|
+
const record = enrollments.get(agentDid);
|
|
198
|
+
return record === void 0 ? void 0 : cloneEnrollmentRecord(record);
|
|
199
|
+
},
|
|
200
|
+
saveEnrollment(record) {
|
|
201
|
+
const cloned = cloneEnrollmentRecord(record);
|
|
202
|
+
enrollments.set(cloned.agentDid, cloned);
|
|
203
|
+
return cloneEnrollmentRecord(cloned);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function createInMemoryClientAssertionReplayStore() {
|
|
208
|
+
const records = /* @__PURE__ */ new Map();
|
|
209
|
+
return {
|
|
210
|
+
consumeReplay(record, now) {
|
|
211
|
+
for (const [key, existing] of records) {
|
|
212
|
+
if (existing.expiresAt <= now) {
|
|
213
|
+
records.delete(key);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (records.has(replayKey(record.sub, record.jti))) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
records.set(replayKey(record.sub, record.jti), { ...record });
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function createInMemoryCommandIdempotencyStore(records = []) {
|
|
225
|
+
const idempotency = /* @__PURE__ */ new Map();
|
|
226
|
+
const pending = /* @__PURE__ */ new Map();
|
|
227
|
+
records.forEach(
|
|
228
|
+
(record) => idempotency.set(
|
|
229
|
+
idempotencyLookupKey(record.agentDid, record.command, record.idempotencyKey),
|
|
230
|
+
cloneIdempotencyResponseRecord(record)
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
return {
|
|
234
|
+
async executeIdempotentCommand(input, execute) {
|
|
235
|
+
const key = idempotencyLookupKey(input.agentDid, input.command, input.idempotencyKey);
|
|
236
|
+
for (; ; ) {
|
|
237
|
+
const existing = idempotency.get(key);
|
|
238
|
+
if (existing !== void 0) {
|
|
239
|
+
if (existing.requestHash !== input.requestHash) {
|
|
240
|
+
return {
|
|
241
|
+
state: "conflict"
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
record: cloneIdempotencyResponseRecord(existing),
|
|
246
|
+
state: "replayed"
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const pendingCommand2 = pending.get(key);
|
|
250
|
+
if (pendingCommand2 === void 0) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
await pendingCommand2;
|
|
254
|
+
}
|
|
255
|
+
let releasePending;
|
|
256
|
+
const pendingCommand = new Promise((resolve) => {
|
|
257
|
+
releasePending = resolve;
|
|
258
|
+
});
|
|
259
|
+
pending.set(key, pendingCommand);
|
|
260
|
+
try {
|
|
261
|
+
const response = await execute();
|
|
262
|
+
const record = {
|
|
263
|
+
agentDid: input.agentDid,
|
|
264
|
+
body: structuredClone(response.body),
|
|
265
|
+
command: input.command,
|
|
266
|
+
contentType: response.contentType,
|
|
267
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
268
|
+
idempotencyKey: input.idempotencyKey,
|
|
269
|
+
requestHash: input.requestHash,
|
|
270
|
+
status: response.status
|
|
271
|
+
};
|
|
272
|
+
idempotency.set(key, cloneIdempotencyResponseRecord(record));
|
|
273
|
+
return {
|
|
274
|
+
response,
|
|
275
|
+
state: "created"
|
|
276
|
+
};
|
|
277
|
+
} finally {
|
|
278
|
+
pending.delete(key);
|
|
279
|
+
releasePending?.();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function createStaticEnrollmentPolicy(decision = {}) {
|
|
285
|
+
return {
|
|
286
|
+
decideEnrollment: () => ({
|
|
287
|
+
ownerActionRequired: decision.ownerActionRequired ?? false,
|
|
288
|
+
requirementsPending: [...decision.requirementsPending ?? []],
|
|
289
|
+
status: decision.status ?? "active"
|
|
290
|
+
})
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function createJwtClientAssertionVerifier(options) {
|
|
294
|
+
return (clientAssertion, context) => verifyClientAssertionJwt(clientAssertion, {
|
|
295
|
+
algorithms: options.algorithms ?? context.signingAlgorithms,
|
|
296
|
+
audience: context.serviceDid,
|
|
297
|
+
key: options.key,
|
|
298
|
+
...options.clockTolerance === void 0 ? {} : { clockTolerance: options.clockTolerance },
|
|
299
|
+
...options.currentDate === void 0 ? {} : { currentDate: options.currentDate }
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function createDidWebClientAssertionVerifier(options = {}) {
|
|
303
|
+
return async (clientAssertion, context) => {
|
|
304
|
+
const untrusted = decodeJwtUnverified(clientAssertion);
|
|
305
|
+
const issuer = stringField(untrusted.payload, "iss");
|
|
306
|
+
const key = await resolveDidWebPublicKey({
|
|
307
|
+
did: issuer,
|
|
308
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch },
|
|
309
|
+
...typeof untrusted.header["kid"] === "string" ? { kid: untrusted.header["kid"] } : {}
|
|
310
|
+
});
|
|
311
|
+
return verifyClientAssertionJwt(clientAssertion, {
|
|
312
|
+
algorithms: context.signingAlgorithms,
|
|
313
|
+
audience: context.serviceDid,
|
|
314
|
+
key
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function createHostedPlatformClientAssertionVerifier(options) {
|
|
319
|
+
const fetchImpl = platformVerificationFetch(options.fetch);
|
|
320
|
+
return async (clientAssertion, context) => {
|
|
321
|
+
const response = await fetchImpl(options.endpoint, {
|
|
322
|
+
body: JSON.stringify({
|
|
323
|
+
client_assertion: clientAssertion,
|
|
324
|
+
op: context.command,
|
|
325
|
+
service_did: context.serviceDid
|
|
326
|
+
}),
|
|
327
|
+
headers: {
|
|
328
|
+
Accept: AEP_MEDIA_TYPE,
|
|
329
|
+
...options.authorization === void 0 ? {} : { Authorization: options.authorization },
|
|
330
|
+
"Content-Type": AEP_MEDIA_TYPE
|
|
331
|
+
},
|
|
332
|
+
method: "POST"
|
|
333
|
+
});
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
throw new Error(`AEP hosted verification failed with HTTP ${response.status}.`);
|
|
336
|
+
}
|
|
337
|
+
const verification = parseHostedPlatformVerificationResponse(await response.json());
|
|
338
|
+
const claims = parseClientAssertionClaims(decodeJwtUnverified(clientAssertion).payload);
|
|
339
|
+
if (!verification.verified || verification.agent_did !== claims.sub || verification.op !== context.command || verification.service_did !== context.serviceDid) {
|
|
340
|
+
throw new Error("AEP hosted verification did not recognize the client assertion.");
|
|
341
|
+
}
|
|
342
|
+
return claims;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function clientAssertionFromAepAuthorization(authorization) {
|
|
346
|
+
const prefix = `${AEP_AUTH_SCHEME} `;
|
|
347
|
+
if (authorization?.startsWith(prefix)) {
|
|
348
|
+
return authorization.slice(prefix.length);
|
|
349
|
+
}
|
|
350
|
+
return "";
|
|
351
|
+
}
|
|
352
|
+
async function authenticateProtectedResource(service, authorization) {
|
|
353
|
+
return service.status({
|
|
354
|
+
clientAssertion: clientAssertionFromAepAuthorization(authorization)
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function isActiveProtectedResourceAuthentication(result) {
|
|
358
|
+
return isRecord(result.body) && result.body["status"] === "active";
|
|
359
|
+
}
|
|
360
|
+
function createInMemoryServiceCredentialStore(records = []) {
|
|
361
|
+
const credentials = /* @__PURE__ */ new Map();
|
|
362
|
+
records.forEach(
|
|
363
|
+
(record) => credentials.set(serviceCredentialKey(record), cloneCredential(record))
|
|
364
|
+
);
|
|
365
|
+
return {
|
|
366
|
+
findCredential(agentDid, grantTypeName, credentialId) {
|
|
367
|
+
const record = credentials.get(
|
|
368
|
+
serviceCredentialLookupKey(agentDid, grantTypeName, credentialId)
|
|
369
|
+
);
|
|
370
|
+
return record === void 0 ? void 0 : cloneCredential(record);
|
|
371
|
+
},
|
|
372
|
+
listCredentials(agentDid, grantTypeName) {
|
|
373
|
+
return [...credentials.values()].filter(
|
|
374
|
+
(record) => record.agentDid === agentDid && (grantTypeName === void 0 || record.grantType === grantTypeName)
|
|
375
|
+
).map(cloneCredential);
|
|
376
|
+
},
|
|
377
|
+
revokeCredential(agentDid, grantTypeName, credentialId, revokedAt) {
|
|
378
|
+
const key = serviceCredentialLookupKey(agentDid, grantTypeName, credentialId);
|
|
379
|
+
const record = credentials.get(key);
|
|
380
|
+
if (record !== void 0) {
|
|
381
|
+
credentials.set(key, {
|
|
382
|
+
...record,
|
|
383
|
+
revokedAt
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
revokeGrantType(agentDid, grantTypeName, revokedAt) {
|
|
388
|
+
for (const [key, record] of credentials) {
|
|
389
|
+
if (record.agentDid === agentDid && record.grantType === grantTypeName) {
|
|
390
|
+
credentials.set(key, {
|
|
391
|
+
...record,
|
|
392
|
+
revokedAt
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
saveCredential(record) {
|
|
398
|
+
const parsed = credentialRecordWithParsedCredential(record);
|
|
399
|
+
credentials.set(serviceCredentialKey(parsed), cloneCredential(parsed));
|
|
400
|
+
return cloneCredential(parsed);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async function handleEnrollRequest(request, options) {
|
|
405
|
+
let parsed;
|
|
406
|
+
try {
|
|
407
|
+
parsed = parseEnrollRequest(request);
|
|
408
|
+
} catch {
|
|
409
|
+
return problem("invalid_request", "Invalid request", 400);
|
|
410
|
+
}
|
|
411
|
+
if (options.idempotencyKey !== void 0 && options.idempotencyKey !== parsed.idempotency_key) {
|
|
412
|
+
return problem("invalid_request", "Invalid request", 400);
|
|
413
|
+
}
|
|
414
|
+
return withIdempotency(
|
|
415
|
+
"enroll",
|
|
416
|
+
parsed.agent_did,
|
|
417
|
+
parsed.idempotency_key,
|
|
418
|
+
parsed,
|
|
419
|
+
options.commandIdempotencyStore,
|
|
420
|
+
async () => {
|
|
421
|
+
const now = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
422
|
+
const nowDate = now();
|
|
423
|
+
const nowIso = nowDate.toISOString();
|
|
424
|
+
const decision = await options.policy.decideEnrollment(parsed, { now: nowDate });
|
|
425
|
+
const record = await options.store.saveEnrollment({
|
|
426
|
+
agentDid: parsed.agent_did,
|
|
427
|
+
claims: structuredClone(parsed.claims ?? {}),
|
|
428
|
+
createdAt: nowIso,
|
|
429
|
+
ownerActionRequired: decision.ownerActionRequired ?? false,
|
|
430
|
+
requirementsPending: [...decision.requirementsPending ?? []],
|
|
431
|
+
since: nowIso,
|
|
432
|
+
status: decision.status ?? "active",
|
|
433
|
+
updatedAt: nowIso
|
|
434
|
+
});
|
|
435
|
+
return aepResponse(200, enrollmentResponseFromRecord(record));
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
async function handleStatusRequest(agentDid, options) {
|
|
440
|
+
const record = await options.store.findEnrollment(agentDid);
|
|
441
|
+
if (record === void 0) {
|
|
442
|
+
return problem("not_recognized", "Not recognized", 401);
|
|
443
|
+
}
|
|
444
|
+
return aepResponse(200, statusResponseFromRecord(record));
|
|
445
|
+
}
|
|
446
|
+
async function handleGrantRequest(request, options) {
|
|
447
|
+
let parsed;
|
|
448
|
+
try {
|
|
449
|
+
parsed = parseGrantRequest(request);
|
|
450
|
+
} catch {
|
|
451
|
+
return problem("invalid_request", "Invalid request", 400);
|
|
452
|
+
}
|
|
453
|
+
const enrollment = await options.store.findEnrollment(options.agentDid);
|
|
454
|
+
if (enrollment === void 0) {
|
|
455
|
+
return problem("not_recognized", "Not recognized", 401);
|
|
456
|
+
}
|
|
457
|
+
if (enrollment.status !== "active") {
|
|
458
|
+
return problem("verification_pending", "Verification pending", 403);
|
|
459
|
+
}
|
|
460
|
+
const handler = options.handlers.get(parsed.grant_type);
|
|
461
|
+
if (handler === void 0) {
|
|
462
|
+
return problem("unsupported_grant_type", "Unsupported grant type", 400);
|
|
463
|
+
}
|
|
464
|
+
return withIdempotency(
|
|
465
|
+
"grant",
|
|
466
|
+
options.agentDid,
|
|
467
|
+
options.idempotencyKey,
|
|
468
|
+
parsed,
|
|
469
|
+
options.commandIdempotencyStore,
|
|
470
|
+
async () => aepResponse(
|
|
471
|
+
200,
|
|
472
|
+
await handler.grant(parsed, {
|
|
473
|
+
agentDid: options.agentDid,
|
|
474
|
+
enrollment,
|
|
475
|
+
grantType: parsed.grant_type
|
|
476
|
+
})
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
async function handleRevokeRequest(request, options) {
|
|
481
|
+
let parsed;
|
|
482
|
+
try {
|
|
483
|
+
parsed = parseRevokeRequest(request);
|
|
484
|
+
} catch {
|
|
485
|
+
return problem("invalid_request", "Invalid request", 400);
|
|
486
|
+
}
|
|
487
|
+
const enrollment = await options.store.findEnrollment(options.agentDid);
|
|
488
|
+
if (enrollment === void 0) {
|
|
489
|
+
return problem("not_recognized", "Not recognized", 401);
|
|
490
|
+
}
|
|
491
|
+
if ("grant_type" in parsed) {
|
|
492
|
+
const grantTypeName = parsed.grant_type;
|
|
493
|
+
const handler = options.handlers.get(grantTypeName);
|
|
494
|
+
if (handler === void 0) {
|
|
495
|
+
return problem("unsupported_grant_type", "Unsupported grant type", 400);
|
|
496
|
+
}
|
|
497
|
+
return withIdempotency(
|
|
498
|
+
"revoke",
|
|
499
|
+
options.agentDid,
|
|
500
|
+
options.idempotencyKey,
|
|
501
|
+
parsed,
|
|
502
|
+
options.commandIdempotencyStore,
|
|
503
|
+
async () => {
|
|
504
|
+
await handler.revoke(parsed, {
|
|
505
|
+
agentDid: options.agentDid,
|
|
506
|
+
enrollment,
|
|
507
|
+
grantType: grantTypeName
|
|
508
|
+
});
|
|
509
|
+
return aepResponse(200, {});
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return withIdempotency(
|
|
514
|
+
"revoke",
|
|
515
|
+
options.agentDid,
|
|
516
|
+
options.idempotencyKey,
|
|
517
|
+
parsed,
|
|
518
|
+
options.commandIdempotencyStore,
|
|
519
|
+
async () => {
|
|
520
|
+
for (const [grantTypeName, handler] of options.handlers) {
|
|
521
|
+
await handler.revoke(parsed, {
|
|
522
|
+
agentDid: options.agentDid,
|
|
523
|
+
enrollment,
|
|
524
|
+
grantType: grantTypeName
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return aepResponse(200, {});
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
async function authenticateClientAssertion(command, commandOptions, options) {
|
|
532
|
+
if (options.verifier === void 0) {
|
|
533
|
+
return notRecognized();
|
|
534
|
+
}
|
|
535
|
+
let claims;
|
|
536
|
+
try {
|
|
537
|
+
claims = parseClientAssertionClaims(
|
|
538
|
+
await options.verifier(commandOptions.clientAssertion, {
|
|
539
|
+
clientAssertion: commandOptions.clientAssertion,
|
|
540
|
+
command,
|
|
541
|
+
serviceDid: options.serviceDid,
|
|
542
|
+
signingAlgorithms: options.signingAlgorithms
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
} catch {
|
|
546
|
+
return notRecognized();
|
|
547
|
+
}
|
|
548
|
+
if (!validateClientAssertionClaimsForCommand(claims, command, options)) {
|
|
549
|
+
return notRecognized();
|
|
550
|
+
}
|
|
551
|
+
const clock = options.config?.clock ?? (() => /* @__PURE__ */ new Date());
|
|
552
|
+
const now = Math.floor(clock().getTime() / 1e3);
|
|
553
|
+
const consumed = await options.replayStore.consumeReplay(
|
|
554
|
+
{
|
|
555
|
+
expiresAt: claims.exp + (options.config?.clockSkewSeconds ?? 30),
|
|
556
|
+
jti: claims.jti,
|
|
557
|
+
sub: claims.sub
|
|
558
|
+
},
|
|
559
|
+
now
|
|
560
|
+
);
|
|
561
|
+
if (!consumed) {
|
|
562
|
+
return notRecognized();
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
agentDid: claims.sub,
|
|
566
|
+
claims,
|
|
567
|
+
ok: true
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function validateClientAssertionClaimsForCommand(claims, command, options) {
|
|
571
|
+
const clock = options.config?.clock ?? (() => /* @__PURE__ */ new Date());
|
|
572
|
+
const clockSkewSeconds = options.config?.clockSkewSeconds ?? 30;
|
|
573
|
+
const maxTtlSeconds = options.config?.maxTtlSeconds ?? 300;
|
|
574
|
+
const now = Math.floor(clock().getTime() / 1e3);
|
|
575
|
+
return claims.iss === claims.sub && claims.aud === options.serviceDid && claims.op === command && claims.exp > claims.iat && claims.exp - claims.iat <= maxTtlSeconds && claims.iat <= now + clockSkewSeconds && claims.exp >= now - clockSkewSeconds;
|
|
576
|
+
}
|
|
577
|
+
function platformVerificationFetch(fetchImpl) {
|
|
578
|
+
const resolved = fetchImpl ?? globalThis.fetch;
|
|
579
|
+
if (typeof resolved !== "function") {
|
|
580
|
+
throw new TypeError("AEP hosted Platform verification requires a fetch implementation.");
|
|
581
|
+
}
|
|
582
|
+
return resolved;
|
|
583
|
+
}
|
|
584
|
+
function parseHostedPlatformVerificationResponse(value) {
|
|
585
|
+
if (!isRecord(value)) {
|
|
586
|
+
throw new TypeError("AEP hosted verification response must be an object.");
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
...typeof value["agent_did"] === "string" ? { agent_did: value["agent_did"] } : {},
|
|
590
|
+
...typeof value["agent_identity_id"] === "string" ? { agent_identity_id: value["agent_identity_id"] } : {},
|
|
591
|
+
...isAuthenticatedServiceCommand(value["op"]) ? { op: value["op"] } : {},
|
|
592
|
+
reason: stringField(value, "reason"),
|
|
593
|
+
service_did: stringField(value, "service_did"),
|
|
594
|
+
...typeof value["status"] === "string" ? { status: value["status"] } : {},
|
|
595
|
+
verified: booleanField(value, "verified")
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function uniqueBy(items, key, label) {
|
|
599
|
+
const seen = /* @__PURE__ */ new Set();
|
|
600
|
+
return items.map((item) => {
|
|
601
|
+
const value = item[key];
|
|
602
|
+
if (seen.has(value)) {
|
|
603
|
+
throw new TypeError(`Duplicate AEP ${label}: ${value}.`);
|
|
604
|
+
}
|
|
605
|
+
seen.add(value);
|
|
606
|
+
return item;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function createGrantHandlerMap(grantTypes) {
|
|
610
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
611
|
+
for (const definition of grantTypes) {
|
|
612
|
+
if (definition.handler !== void 0) {
|
|
613
|
+
handlers.set(definition.grantType, definition.handler);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return handlers;
|
|
617
|
+
}
|
|
618
|
+
function storedBuiltInGrantType(grantTypeName, options) {
|
|
619
|
+
return grantType(grantTypeName, options.config, {
|
|
620
|
+
async grant(request, context) {
|
|
621
|
+
const credential = parseBuiltInGrantResponse(
|
|
622
|
+
grantTypeName,
|
|
623
|
+
await options.issue(request, context)
|
|
624
|
+
);
|
|
625
|
+
const record = await options.store.saveCredential({
|
|
626
|
+
agentDid: context.agentDid,
|
|
627
|
+
createdAt: (options.clock ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
|
|
628
|
+
credential,
|
|
629
|
+
credentialId: credential.credential_id,
|
|
630
|
+
expiresAt: credential.expires_at,
|
|
631
|
+
grantType: grantTypeName
|
|
632
|
+
});
|
|
633
|
+
return structuredClone(record.credential);
|
|
634
|
+
},
|
|
635
|
+
async revoke(request, context) {
|
|
636
|
+
const revokedAt = (options.clock ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
637
|
+
if ("credential_id" in request) {
|
|
638
|
+
await options.store.revokeCredential(
|
|
639
|
+
context.agentDid,
|
|
640
|
+
grantTypeName,
|
|
641
|
+
request.credential_id,
|
|
642
|
+
revokedAt
|
|
643
|
+
);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
await options.store.revokeGrantType(context.agentDid, grantTypeName, revokedAt);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
function enrollmentResponseFromRecord(record) {
|
|
651
|
+
return {
|
|
652
|
+
status: record.status,
|
|
653
|
+
...record.ownerActionRequired ? {
|
|
654
|
+
owner_action_required: "true"
|
|
655
|
+
} : {},
|
|
656
|
+
...record.requirementsPending.length > 0 ? {
|
|
657
|
+
requirements_pending: [...record.requirementsPending]
|
|
658
|
+
} : {}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function statusResponseFromRecord(record) {
|
|
662
|
+
return {
|
|
663
|
+
owner_action_required: record.ownerActionRequired ? "true" : "false",
|
|
664
|
+
requirements_pending: [...record.requirementsPending],
|
|
665
|
+
since: record.since,
|
|
666
|
+
status: record.status
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function aepResponse(status, body) {
|
|
670
|
+
return {
|
|
671
|
+
body,
|
|
672
|
+
contentType: AEP_MEDIA_TYPE,
|
|
673
|
+
status
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function problem(code, title, status) {
|
|
677
|
+
return {
|
|
678
|
+
body: createProblemDetails({
|
|
679
|
+
code,
|
|
680
|
+
status,
|
|
681
|
+
title
|
|
682
|
+
}),
|
|
683
|
+
contentType: AEP_PROBLEM_MEDIA_TYPE,
|
|
684
|
+
status
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function notRecognized() {
|
|
688
|
+
return {
|
|
689
|
+
ok: false,
|
|
690
|
+
response: problem("not_recognized", "Not recognized", 401)
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async function withIdempotency(command, agentDid, idempotencyKey, request, store, execute) {
|
|
694
|
+
if (idempotencyKey === void 0 || store === void 0) {
|
|
695
|
+
return execute();
|
|
696
|
+
}
|
|
697
|
+
const requestHash = hashRequest(request);
|
|
698
|
+
const result = await store.executeIdempotentCommand(
|
|
699
|
+
{
|
|
700
|
+
agentDid,
|
|
701
|
+
command,
|
|
702
|
+
idempotencyKey,
|
|
703
|
+
requestHash
|
|
704
|
+
},
|
|
705
|
+
execute
|
|
706
|
+
);
|
|
707
|
+
if (result.state === "replayed") {
|
|
708
|
+
return {
|
|
709
|
+
body: structuredClone(result.record.body),
|
|
710
|
+
contentType: result.record.contentType,
|
|
711
|
+
status: result.record.status
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (result.state === "conflict") {
|
|
715
|
+
return problem("idempotency_conflict", "Idempotency conflict", 409);
|
|
716
|
+
}
|
|
717
|
+
return result.response;
|
|
718
|
+
}
|
|
719
|
+
function replayKey(sub, jti) {
|
|
720
|
+
return `${sub}\0${jti}`;
|
|
721
|
+
}
|
|
722
|
+
function idempotencyLookupKey(agentDid, command, idempotencyKey) {
|
|
723
|
+
return command === "enroll" ? `${command}\0${idempotencyKey}` : `${agentDid}\0${command}\0${idempotencyKey}`;
|
|
724
|
+
}
|
|
725
|
+
function cloneEnrollmentRecord(record) {
|
|
726
|
+
return {
|
|
727
|
+
...record,
|
|
728
|
+
claims: structuredClone(record.claims),
|
|
729
|
+
requirementsPending: [...record.requirementsPending]
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function cloneIdempotencyResponseRecord(record) {
|
|
733
|
+
return {
|
|
734
|
+
...record,
|
|
735
|
+
body: structuredClone(record.body)
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function serviceCredentialLookupKey(agentDid, grantTypeName, credentialId) {
|
|
739
|
+
return `${agentDid}\0${grantTypeName}\0${credentialId}`;
|
|
740
|
+
}
|
|
741
|
+
function serviceCredentialKey(record) {
|
|
742
|
+
return serviceCredentialLookupKey(record.agentDid, record.grantType, record.credentialId);
|
|
743
|
+
}
|
|
744
|
+
function cloneCredential(record) {
|
|
745
|
+
return {
|
|
746
|
+
...record,
|
|
747
|
+
credential: structuredClone(record.credential)
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function credentialRecordWithParsedCredential(record) {
|
|
751
|
+
const credential = parseBuiltInGrantResponse(record.grantType, record.credential);
|
|
752
|
+
if (credential.credential_id !== record.credentialId) {
|
|
753
|
+
throw new TypeError("AEP credential record credentialId does not match credential body.");
|
|
754
|
+
}
|
|
755
|
+
if (credential.expires_at !== record.expiresAt) {
|
|
756
|
+
throw new TypeError("AEP credential record expiresAt does not match credential body.");
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
...record,
|
|
760
|
+
credential
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function hashRequest(request) {
|
|
764
|
+
return `json:${stableStringify(request)}`;
|
|
765
|
+
}
|
|
766
|
+
function stableStringify(value) {
|
|
767
|
+
if (Array.isArray(value)) {
|
|
768
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
769
|
+
}
|
|
770
|
+
if (value !== null && typeof value === "object") {
|
|
771
|
+
const record = value;
|
|
772
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
773
|
+
}
|
|
774
|
+
return JSON.stringify(value);
|
|
775
|
+
}
|
|
776
|
+
function stringField(value, field) {
|
|
777
|
+
if (!isRecord(value) || typeof value[field] !== "string") {
|
|
778
|
+
throw new Error(`Expected string field: ${field}`);
|
|
779
|
+
}
|
|
780
|
+
return value[field];
|
|
781
|
+
}
|
|
782
|
+
function booleanField(value, field) {
|
|
783
|
+
if (!isRecord(value) || typeof value[field] !== "boolean") {
|
|
784
|
+
throw new Error(`Expected boolean field: ${field}`);
|
|
785
|
+
}
|
|
786
|
+
return value[field];
|
|
787
|
+
}
|
|
788
|
+
function isAuthenticatedServiceCommand(value) {
|
|
789
|
+
return value === "enroll" || value === "grant" || value === "revoke" || value === "status";
|
|
790
|
+
}
|
|
791
|
+
function isRecord(value) {
|
|
792
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
793
|
+
}
|
|
794
|
+
export {
|
|
795
|
+
apiKeyGrantType,
|
|
796
|
+
authenticateProtectedResource,
|
|
797
|
+
basicGrantType,
|
|
798
|
+
buildInspectDocument,
|
|
799
|
+
clientAssertionFromAepAuthorization,
|
|
800
|
+
createAepService,
|
|
801
|
+
createDidWebClientAssertionVerifier,
|
|
802
|
+
createHostedPlatformClientAssertionVerifier,
|
|
803
|
+
createInMemoryClientAssertionReplayStore,
|
|
804
|
+
createInMemoryCommandIdempotencyStore,
|
|
805
|
+
createInMemoryEnrollmentStore,
|
|
806
|
+
createInMemoryServiceCredentialStore,
|
|
807
|
+
createJwtClientAssertionVerifier,
|
|
808
|
+
createStaticEnrollmentPolicy,
|
|
809
|
+
didWebIdentityMethod,
|
|
810
|
+
grantType,
|
|
811
|
+
handleEnrollRequest,
|
|
812
|
+
handleGrantRequest,
|
|
813
|
+
handleRevokeRequest,
|
|
814
|
+
handleStatusRequest,
|
|
815
|
+
isActiveProtectedResourceAuthentication,
|
|
816
|
+
oauthBearerGrantType,
|
|
817
|
+
storedApiKeyGrantType,
|
|
818
|
+
storedBasicGrantType,
|
|
819
|
+
storedOAuthBearerGrantType
|
|
820
|
+
};
|
|
821
|
+
//# sourceMappingURL=index.js.map
|