@agent-network-protocol/dsh-anp-identity 0.1.2 → 0.1.3-rc.1
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/BOUNDARY.md +14 -2
- package/README.md +37 -4
- package/README.zh.md +10 -2
- package/V7-MANAGEMENT.md +90 -0
- package/cordis.patch.yml +4 -0
- package/lib/authorization-types.d.ts +150 -0
- package/lib/authorization-types.d.ts.map +1 -0
- package/lib/authorization-types.js +2 -0
- package/lib/authorization-types.js.map +1 -0
- package/lib/authorization.d.ts +45 -0
- package/lib/authorization.d.ts.map +1 -0
- package/lib/authorization.js +718 -0
- package/lib/authorization.js.map +1 -0
- package/lib/catalog.d.ts +7 -2
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +45 -23
- package/lib/catalog.js.map +1 -1
- package/lib/client/controller.d.ts +78 -0
- package/lib/client/controller.d.ts.map +1 -0
- package/lib/client/controller.js +365 -0
- package/lib/client/controller.js.map +1 -0
- package/lib/client/identity-icon.d.ts +3 -0
- package/lib/client/identity-icon.d.ts.map +1 -0
- package/lib/client/identity-icon.js +6 -0
- package/lib/client/identity-icon.js.map +1 -0
- package/lib/client/index.d.ts +7 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +51 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/settings-icon-slot.d.ts +13 -0
- package/lib/client/settings-icon-slot.d.ts.map +1 -0
- package/lib/client/settings-icon-slot.js +2 -0
- package/lib/client/settings-icon-slot.js.map +1 -0
- package/lib/client/styles.d.ts +4 -0
- package/lib/client/styles.d.ts.map +1 -0
- package/lib/client/styles.js +39 -0
- package/lib/client/styles.js.map +1 -0
- package/lib/client/views.d.ts +11 -0
- package/lib/client/views.d.ts.map +1 -0
- package/lib/client/views.js +287 -0
- package/lib/client/views.js.map +1 -0
- package/lib/client.js +21008 -0
- package/lib/client.js.map +7 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +413 -58
- package/lib/index.js.map +1 -1
- package/lib/management-remote.d.ts +23 -0
- package/lib/management-remote.d.ts.map +1 -0
- package/lib/management-remote.js +126 -0
- package/lib/management-remote.js.map +1 -0
- package/lib/management-types.d.ts +50 -0
- package/lib/management-types.d.ts.map +1 -0
- package/lib/management-types.js +2 -0
- package/lib/management-types.js.map +1 -0
- package/lib/provider-api.d.ts +3 -0
- package/lib/provider-api.d.ts.map +1 -1
- package/lib/provider-api.js.map +1 -1
- package/lib/remote.d.ts +4 -0
- package/lib/remote.d.ts.map +1 -0
- package/lib/remote.js +86 -0
- package/lib/remote.js.map +1 -0
- package/lib/user-client.d.ts +41 -0
- package/lib/user-client.d.ts.map +1 -0
- package/lib/user-client.js +6 -0
- package/lib/user-client.js.map +1 -0
- package/lib/user-operation.d.ts +7 -0
- package/lib/user-operation.d.ts.map +1 -0
- package/lib/user-operation.js +87 -0
- package/lib/user-operation.js.map +1 -0
- package/lib/user-service.d.ts +26 -0
- package/lib/user-service.d.ts.map +1 -0
- package/lib/user-service.js +140 -0
- package/lib/user-service.js.map +1 -0
- package/package.json +113 -10
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
7
7
|
import { CatalogStore, CATALOG_SCHEMA, CREATE_INTENT_SCHEMA, emptyCatalog, entryReference, findEntry, normalizeHandle, } from './catalog.js';
|
|
8
8
|
import { pluginError, AnpIdentityPluginError } from './errors.js';
|
|
9
9
|
import { createAuthenticatedHttp } from './http-auth.js';
|
|
10
|
+
import { UserIdentityService, requestSigningKid } from './user-service.js';
|
|
10
11
|
import { ANP_IDENTITY_NATIVE_PROVIDER_PROTOCOL, ANP_IDENTITY_PROVIDER_PROTOCOL, ANP_IDENTITY_SERVICE_PROTOCOL, } from './provider-api.js';
|
|
11
12
|
export { AnpIdentityPluginError } from './errors.js';
|
|
12
13
|
export { ANP_IDENTITY_HTTP_MAX_BODY_BYTES, } from './http-auth.js';
|
|
@@ -17,7 +18,15 @@ export const Config = z.object({
|
|
|
17
18
|
allowProviderConsumers: z.array(z.string()).default([]),
|
|
18
19
|
httpAllowedOrigins: z.dict(z.array(z.string())).default({}),
|
|
19
20
|
recoveryOnOpen: z.boolean().default(true),
|
|
21
|
+
userConsumers: z.array(z.string()).default([]),
|
|
20
22
|
});
|
|
23
|
+
/** The Loader retains Registry.plugin's awaitable wrapper, not its raw fiber. */
|
|
24
|
+
function sameFiber(left, right) {
|
|
25
|
+
if (!left || !right)
|
|
26
|
+
return false;
|
|
27
|
+
const original = (fiber) => fiber.ctx?.fiber ?? fiber;
|
|
28
|
+
return original(left) === original(right);
|
|
29
|
+
}
|
|
21
30
|
const CLIENT_CAPABILITIES = new Set([
|
|
22
31
|
'identity:read',
|
|
23
32
|
'identity:create',
|
|
@@ -47,11 +56,54 @@ export class AnpIdentityService extends Service {
|
|
|
47
56
|
resolvedConfig;
|
|
48
57
|
catalogStore;
|
|
49
58
|
providerSlot;
|
|
59
|
+
users;
|
|
60
|
+
hostContext;
|
|
61
|
+
disabledFences = new Set();
|
|
62
|
+
lifecycleWrites = new Set();
|
|
50
63
|
constructor(ctx, config) {
|
|
51
64
|
super(ctx, 'anpIdentity');
|
|
52
65
|
this.resolvedConfig = resolveConfig(config);
|
|
53
66
|
this.catalogStore = new CatalogStore(this.resolvedConfig.stateRoot);
|
|
67
|
+
this.hostContext = ctx;
|
|
68
|
+
this.users = new UserIdentityService(this.resolvedConfig.stateRoot, {
|
|
69
|
+
ensureReady: async () => { await this.awaitProvider(this.requireSlot()); },
|
|
70
|
+
resolveCaller: context => this.resolveInstalledCaller(context),
|
|
71
|
+
validateCaller: async (caller) => { this.assertInstalledCaller(caller); },
|
|
72
|
+
resolveSnapshot: (caller, reference) => this.userSnapshot(caller, reference),
|
|
73
|
+
createIdentity: (id, caller, parameters) => this.createConfirmed(id, caller, parameters),
|
|
74
|
+
reconcileCreation: (id, caller, parameters) => this.reconcileConfirmed(id, caller, parameters),
|
|
75
|
+
withNative: operation => this.withUserNative(operation),
|
|
76
|
+
});
|
|
77
|
+
const observe = ctx.on.bind(ctx);
|
|
78
|
+
observe('loader/partial-dispose', (rawEntry, rawPrevious, active) => {
|
|
79
|
+
if (ctx.fiber.state > 2)
|
|
80
|
+
return;
|
|
81
|
+
const entry = rawEntry;
|
|
82
|
+
const previous = rawPrevious;
|
|
83
|
+
if (active === false || entry?.disabled)
|
|
84
|
+
this.cancelDisabledRequests(previous?.name ?? entry?.options?.name);
|
|
85
|
+
try {
|
|
86
|
+
for (const candidate of this.installedEntries()) {
|
|
87
|
+
if (candidate.disabled)
|
|
88
|
+
this.cancelDisabledRequests(candidate.options.name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { /* The Host may not have installed a loader yet. */ }
|
|
92
|
+
});
|
|
93
|
+
ctx.on('internal/plugin', fiber => {
|
|
94
|
+
if (ctx.fiber.state > 2)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
for (const entry of this.installedEntries()) {
|
|
98
|
+
if (sameFiber(entry.fiber, fiber) && entry.disabled)
|
|
99
|
+
this.cancelDisabledRequests(entry.options.name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch { /* A transient missing loader is not an uninstall decision. */ }
|
|
103
|
+
});
|
|
54
104
|
ctx.effect(() => async () => {
|
|
105
|
+
await this.users.ready.catch(() => { });
|
|
106
|
+
await Promise.allSettled([...this.lifecycleWrites]);
|
|
55
107
|
const slot = this.providerSlot;
|
|
56
108
|
if (slot !== undefined)
|
|
57
109
|
await this.disposeSlot(slot);
|
|
@@ -109,7 +161,9 @@ export class AnpIdentityService extends Service {
|
|
|
109
161
|
return () => this.disposeSlot(slot);
|
|
110
162
|
}
|
|
111
163
|
async acquireClient(input) {
|
|
164
|
+
this.assertLegacyCallerContext(input.consumer);
|
|
112
165
|
const request = validateClientRequest(input, this.resolvedConfig);
|
|
166
|
+
this.assertLegacyConsumer(request.consumer);
|
|
113
167
|
const slot = this.requireSlot();
|
|
114
168
|
const provider = await this.awaitProvider(slot);
|
|
115
169
|
const capabilities = nativeClientCapabilities(request.capabilities);
|
|
@@ -129,6 +183,7 @@ export class AnpIdentityService extends Service {
|
|
|
129
183
|
return lease;
|
|
130
184
|
}
|
|
131
185
|
acquireProvider(input) {
|
|
186
|
+
this.assertLegacyCallerContext(input.consumer);
|
|
132
187
|
const request = validateProviderRequest(input, this.resolvedConfig);
|
|
133
188
|
const slot = this.requireSlot();
|
|
134
189
|
const provider = slot.provider;
|
|
@@ -151,6 +206,7 @@ export class AnpIdentityService extends Service {
|
|
|
151
206
|
}
|
|
152
207
|
async listForClient(lease, input) {
|
|
153
208
|
lease.assertCapability('identity:read');
|
|
209
|
+
this.assertLegacyConsumer(lease.consumer);
|
|
154
210
|
const catalog = await this.loadCatalog(lease.slot);
|
|
155
211
|
const native = await nativeCall(() => lease.native.list());
|
|
156
212
|
const byId = new Map(native.map(value => [value.reference.identityId, value]));
|
|
@@ -172,6 +228,7 @@ export class AnpIdentityService extends Service {
|
|
|
172
228
|
}
|
|
173
229
|
async createForClient(lease, input) {
|
|
174
230
|
lease.assertCapability('identity:create');
|
|
231
|
+
this.assertLegacyConsumer(lease.consumer);
|
|
175
232
|
validateCreateInput(input);
|
|
176
233
|
const requestId = input.requestId ?? randomUUID();
|
|
177
234
|
const createdAt = new Date().toISOString();
|
|
@@ -188,6 +245,7 @@ export class AnpIdentityService extends Service {
|
|
|
188
245
|
try {
|
|
189
246
|
identity = await this.catalogStore.withNativeCreateLock(async () => {
|
|
190
247
|
lease.assertCapability('identity:create');
|
|
248
|
+
await this.assertNoUnresolvedUserCreate();
|
|
191
249
|
const baseline = await nativeCall(() => lease.native.list());
|
|
192
250
|
await this.catalogStore.updateIntent(requestId, current => ({
|
|
193
251
|
...current,
|
|
@@ -216,28 +274,35 @@ export class AnpIdentityService extends Service {
|
|
|
216
274
|
async deleteForClient(lease, reference, _input) {
|
|
217
275
|
lease.assertCapability('identity:delete');
|
|
218
276
|
const resolved = await this.resolveAuthorized(lease, reference);
|
|
219
|
-
await this.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
277
|
+
await this.users.ready;
|
|
278
|
+
await this.users.engine.withIdentityDeletion(resolved, async () => {
|
|
279
|
+
await this.catalogStore.mutate((catalog) => {
|
|
280
|
+
const entry = findEntry(catalog, resolved);
|
|
281
|
+
if (entry === undefined)
|
|
282
|
+
throw pluginError('identity_not_found');
|
|
283
|
+
if (entry.state !== 'active')
|
|
284
|
+
throw pluginError('identity_deleting');
|
|
285
|
+
if (entry.grantedConsumers.some(consumer => consumer !== lease.consumer)) {
|
|
286
|
+
throw pluginError('identity_in_use');
|
|
287
|
+
}
|
|
288
|
+
return replaceEntry(catalog, entry, { ...entry, state: 'deleting' });
|
|
289
|
+
});
|
|
290
|
+
}, async () => {
|
|
291
|
+
await nativeCall(() => lease.native.delete(resolved));
|
|
292
|
+
await this.catalogStore.mutate(catalog => ({
|
|
293
|
+
...catalog,
|
|
294
|
+
entries: catalog.entries.filter(entry => entry.identityId !== resolved.identityId),
|
|
295
|
+
}));
|
|
229
296
|
});
|
|
230
|
-
await nativeCall(() => lease.native.delete(resolved));
|
|
231
|
-
await this.catalogStore.mutate(catalog => ({
|
|
232
|
-
...catalog,
|
|
233
|
-
entries: catalog.entries.filter(entry => entry.identityId !== resolved.identityId),
|
|
234
|
-
}));
|
|
235
297
|
}
|
|
236
298
|
async recoverForClient(lease) {
|
|
237
299
|
lease.assertCapability('identity:recover');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
300
|
+
this.assertLegacyConsumer(lease.consumer);
|
|
301
|
+
return this.catalogStore.withNativeCreateLock(async () => {
|
|
302
|
+
const native = await nativeCall(() => lease.native.recover());
|
|
303
|
+
const report = await this.reconcile(lease.slot, lease.native, true, false);
|
|
304
|
+
return { ...native, ...report };
|
|
305
|
+
});
|
|
241
306
|
}
|
|
242
307
|
async setHandleForClient(lease, reference, handle) {
|
|
243
308
|
lease.assertCapability('identity:handle');
|
|
@@ -260,6 +325,7 @@ export class AnpIdentityService extends Service {
|
|
|
260
325
|
}
|
|
261
326
|
async assertManagedAccess(lease, reference) {
|
|
262
327
|
lease.assertActive();
|
|
328
|
+
this.assertLegacyConsumer(lease.consumer);
|
|
263
329
|
const catalog = await this.loadCatalog(lease.slot);
|
|
264
330
|
assertGrantedEntry(findEntry(catalog, reference), lease.consumer);
|
|
265
331
|
}
|
|
@@ -270,17 +336,21 @@ export class AnpIdentityService extends Service {
|
|
|
270
336
|
}
|
|
271
337
|
async grantConsumer(slot, reference, consumer) {
|
|
272
338
|
this.assertAllowedConsumer(consumer);
|
|
339
|
+
this.assertLegacyConsumer(consumer);
|
|
273
340
|
this.assertSlot(slot);
|
|
274
|
-
await this.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
341
|
+
await this.users.ready;
|
|
342
|
+
await this.users.engine.withIdentityAccess(reference, async () => {
|
|
343
|
+
await this.catalogStore.mutate((catalog) => {
|
|
344
|
+
const entry = findEntry(catalog, reference);
|
|
345
|
+
if (entry === undefined)
|
|
346
|
+
throw pluginError('identity_not_found');
|
|
347
|
+
if (entry.state === 'deleting')
|
|
348
|
+
throw pluginError('identity_deleting');
|
|
349
|
+
return replaceEntry(catalog, entry, {
|
|
350
|
+
...entry,
|
|
351
|
+
state: 'active',
|
|
352
|
+
grantedConsumers: [...new Set([...entry.grantedConsumers, consumer])].sort(),
|
|
353
|
+
});
|
|
284
354
|
});
|
|
285
355
|
});
|
|
286
356
|
}
|
|
@@ -314,6 +384,7 @@ export class AnpIdentityService extends Service {
|
|
|
314
384
|
try {
|
|
315
385
|
const identity = await this.catalogStore.withNativeCreateLock(async () => {
|
|
316
386
|
this.assertSlot(slot);
|
|
387
|
+
await this.assertNoUnresolvedUserCreate();
|
|
317
388
|
const baseline = await nativeCall(() => native.list());
|
|
318
389
|
await this.catalogStore.updateIntent(requestId, current => ({
|
|
319
390
|
...current,
|
|
@@ -336,39 +407,27 @@ export class AnpIdentityService extends Service {
|
|
|
336
407
|
}
|
|
337
408
|
async deleteForHost(slot, native, consumer, reference) {
|
|
338
409
|
this.assertSlot(slot);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const entry = findEntry(catalog, reference);
|
|
356
|
-
if (entry === undefined)
|
|
357
|
-
throw pluginError('identity_not_found');
|
|
358
|
-
if (entry.grantedConsumers.some(value => value !== consumer)) {
|
|
359
|
-
throw pluginError('identity_in_use');
|
|
360
|
-
}
|
|
361
|
-
return replaceEntry(catalog, entry, { ...entry, state: 'deleting' });
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
await deleteNativeWithRecovery(native, reference);
|
|
366
|
-
if (catalogEntry !== undefined) {
|
|
410
|
+
await this.users.ready;
|
|
411
|
+
await this.users.engine.withIdentityDeletion(reference, async () => {
|
|
412
|
+
await this.loadCatalog(slot);
|
|
413
|
+
await this.catalogStore.mutate(catalog => {
|
|
414
|
+
const entry = findEntry(catalog, reference);
|
|
415
|
+
if (entry === undefined)
|
|
416
|
+
return catalog;
|
|
417
|
+
if (entry.grantedConsumers.some(value => value !== consumer)) {
|
|
418
|
+
throw pluginError('identity_in_use');
|
|
419
|
+
}
|
|
420
|
+
if (entry.state === 'deleting')
|
|
421
|
+
return catalog;
|
|
422
|
+
return replaceEntry(catalog, entry, { ...entry, state: 'deleting' });
|
|
423
|
+
});
|
|
424
|
+
}, async () => {
|
|
425
|
+
await deleteNativeWithRecovery(native, reference);
|
|
367
426
|
await this.catalogStore.mutate(catalog => ({
|
|
368
427
|
...catalog,
|
|
369
428
|
entries: catalog.entries.filter(entry => entry.identityId !== reference.identityId),
|
|
370
429
|
}));
|
|
371
|
-
}
|
|
430
|
+
});
|
|
372
431
|
}
|
|
373
432
|
async createNativeWithRecovery(native, request, requestId, baselineIdentityIds) {
|
|
374
433
|
try {
|
|
@@ -396,6 +455,7 @@ export class AnpIdentityService extends Service {
|
|
|
396
455
|
slot.leases.delete(lease);
|
|
397
456
|
}
|
|
398
457
|
async initializeSlot(slot, provider) {
|
|
458
|
+
await this.users.ready;
|
|
399
459
|
try {
|
|
400
460
|
await this.catalogStore.initialize();
|
|
401
461
|
slot.catalog = 'ready';
|
|
@@ -414,7 +474,7 @@ export class AnpIdentityService extends Service {
|
|
|
414
474
|
try {
|
|
415
475
|
if (this.resolvedConfig.recoveryOnOpen)
|
|
416
476
|
await nativeCall(() => native.recover());
|
|
417
|
-
await this.reconcile(slot, native, false, false);
|
|
477
|
+
await this.catalogStore.withNativeCreateLock(() => this.reconcile(slot, native, false, false));
|
|
418
478
|
}
|
|
419
479
|
finally {
|
|
420
480
|
native.dispose();
|
|
@@ -482,6 +542,7 @@ export class AnpIdentityService extends Service {
|
|
|
482
542
|
catalog = await this.catalogStore.load();
|
|
483
543
|
}
|
|
484
544
|
catalog = await this.catalogStore.load();
|
|
545
|
+
const catalogFences = catalog.entries.filter(value => value.state === 'deleting').map(entryReference);
|
|
485
546
|
for (const entry of catalog.entries.filter(value => value.state === 'deleting')) {
|
|
486
547
|
const reference = storeById.get(entry.identityId);
|
|
487
548
|
if (reference !== undefined) {
|
|
@@ -501,6 +562,15 @@ export class AnpIdentityService extends Service {
|
|
|
501
562
|
known.add(reference.identityId);
|
|
502
563
|
}
|
|
503
564
|
}
|
|
565
|
+
await this.users.engine.reconcileDeletions({
|
|
566
|
+
catalogFences,
|
|
567
|
+
isAbsent: async (reference) => {
|
|
568
|
+
const currentNative = await nativeCall(() => native.list());
|
|
569
|
+
const currentCatalog = await this.catalogStore.load();
|
|
570
|
+
return !currentNative.some(value => sameReference(value.reference, reference))
|
|
571
|
+
&& !findEntry(currentCatalog, reference);
|
|
572
|
+
},
|
|
573
|
+
});
|
|
504
574
|
return {
|
|
505
575
|
catalogRebuilt: false,
|
|
506
576
|
unclaimedIdentityCount: catalog.entries.filter(value => value.state === 'unclaimed').length,
|
|
@@ -508,6 +578,7 @@ export class AnpIdentityService extends Service {
|
|
|
508
578
|
}
|
|
509
579
|
async resolveAuthorized(lease, reference) {
|
|
510
580
|
lease.assertActive();
|
|
581
|
+
this.assertLegacyConsumer(lease.consumer);
|
|
511
582
|
const catalog = await this.loadCatalog(lease.slot);
|
|
512
583
|
const entry = 'handle' in reference
|
|
513
584
|
? catalog.entries.find(value => value.handle === normalizeHandle(reference.handle))
|
|
@@ -556,6 +627,283 @@ export class AnpIdentityService extends Service {
|
|
|
556
627
|
throw pluginError('provider_unavailable');
|
|
557
628
|
}
|
|
558
629
|
}
|
|
630
|
+
/** Host-bound ordinary facade; request inputs never select a caller. */
|
|
631
|
+
bindUserConsumer(context) {
|
|
632
|
+
return this.users.bind(context);
|
|
633
|
+
}
|
|
634
|
+
/** Host-only manager; the ordinary consumer facade never exposes this object. */
|
|
635
|
+
acquireManagement() {
|
|
636
|
+
if (!sameFiber(this.ctx.fiber, this.hostContext.fiber) && !sameFiber(this.ctx.fiber, this.hostContext.root.fiber)) {
|
|
637
|
+
throw pluginError('consumer_forbidden');
|
|
638
|
+
}
|
|
639
|
+
const ready = async () => { await this.users.ready; await this.awaitProvider(this.requireSlot()); };
|
|
640
|
+
return Object.freeze({
|
|
641
|
+
listIdentities: async () => { await ready(); return this.managementIdentities(); },
|
|
642
|
+
listRequests: async (history = false) => {
|
|
643
|
+
await ready();
|
|
644
|
+
return (await this.users.engine.listRequests()).filter(request => history ? request.status !== 'pending' : request.status === 'pending');
|
|
645
|
+
},
|
|
646
|
+
getRequest: async (id) => {
|
|
647
|
+
await ready();
|
|
648
|
+
let request = (await this.users.engine.listRequests()).find(value => value.id === id);
|
|
649
|
+
if (!request)
|
|
650
|
+
throw pluginError('invalid_request');
|
|
651
|
+
if (request.kind === 'create' && request.status === 'approved'
|
|
652
|
+
&& (request.executionStatus === 'running' || request.executionStatus === 'unknown')) {
|
|
653
|
+
request = await this.users.engine.reconcileCreation(id);
|
|
654
|
+
}
|
|
655
|
+
let identities = await this.managementIdentities();
|
|
656
|
+
let unavailableReason;
|
|
657
|
+
try {
|
|
658
|
+
this.assertInstalledCaller(request.caller);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
unavailableReason = 'The requesting plugin is not available.';
|
|
662
|
+
}
|
|
663
|
+
let snapshot;
|
|
664
|
+
const snapshots = [];
|
|
665
|
+
if (request.kind === 'access' && request.identity) {
|
|
666
|
+
try {
|
|
667
|
+
snapshot = await this.userSnapshot(request.caller, request.identity);
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
unavailableReason = 'The requested identity or plugin is not available.';
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else if (request.kind === 'access' && unavailableReason === undefined) {
|
|
674
|
+
for (const identity of identities) {
|
|
675
|
+
try {
|
|
676
|
+
snapshots.push({ identity: identity.reference, snapshot: await this.userSnapshot(request.caller, identity.reference) });
|
|
677
|
+
}
|
|
678
|
+
catch { /* Only compatible identities may be selected for ordinary use. */ }
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (request.kind === 'access')
|
|
682
|
+
identities = identities.filter(identity => snapshots.some(item => sameReference(item.identity, identity.reference))
|
|
683
|
+
|| (snapshot !== undefined && request.identity !== undefined && sameReference(request.identity, identity.reference)));
|
|
684
|
+
const allGrants = await this.users.engine.listGrants();
|
|
685
|
+
const grant = request.kind === 'access' ? allGrants.find(value => value.id === request.grantId) : undefined;
|
|
686
|
+
const permanentIdentities = allGrants.filter(grant => grant.caller.consumer === request.caller.consumer
|
|
687
|
+
&& grant.mode === 'permanent' && grant.status === 'active').map(grant => grant.identity);
|
|
688
|
+
return { request, identities, snapshots, permanentIdentities, ...(snapshot === undefined ? {} : { snapshot }),
|
|
689
|
+
...(grant === undefined ? {} : { grant }),
|
|
690
|
+
...(unavailableReason === undefined ? {} : { unavailableReason }) };
|
|
691
|
+
},
|
|
692
|
+
decide: async (input) => { await ready(); return this.users.engine.decide(input); },
|
|
693
|
+
reauthorize: async (input) => { await ready(); return this.users.engine.reauthorize(input.requestId, input.expectedVersion); },
|
|
694
|
+
grants: async (reference) => { await ready(); return (await this.users.engine.listGrants(reference)).filter(grant => grant.status === 'active'); },
|
|
695
|
+
revoke: async (input) => { await ready(); await this.users.engine.revoke(input.grantId, input.expectedVersion); },
|
|
696
|
+
publicDocument: async (reference) => {
|
|
697
|
+
await ready();
|
|
698
|
+
return this.withUserNative(async (native) => structuredClone((await native.publicIdentity(reference)).document));
|
|
699
|
+
},
|
|
700
|
+
deleteIdentity: async (input) => {
|
|
701
|
+
await ready();
|
|
702
|
+
// Preserve the stable creation result before removing its catalog provenance.
|
|
703
|
+
const entry = findEntry(await this.catalogStore.load(), input.reference);
|
|
704
|
+
if (entry?.creationOperationId) {
|
|
705
|
+
const creation = (await this.users.engine.listRequests()).find(request => request.kind === 'create'
|
|
706
|
+
&& request.operationId === entry.creationOperationId);
|
|
707
|
+
if (creation?.kind === 'create' && creation.result === undefined) {
|
|
708
|
+
const reconciled = await this.users.engine.reconcileCreation(creation.id);
|
|
709
|
+
if (!reconciled.result || !sameReference(reconciled.result.reference, input.reference))
|
|
710
|
+
throw pluginError('identity_in_use');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
await this.users.engine.withIdentityDeletion(input.reference, async () => {
|
|
714
|
+
await this.catalogStore.mutate(catalog => {
|
|
715
|
+
const entry = findEntry(catalog, input.reference);
|
|
716
|
+
if (!entry)
|
|
717
|
+
throw pluginError('identity_not_found');
|
|
718
|
+
if (input.confirmationName !== (entry.label ?? entry.did))
|
|
719
|
+
throw pluginError('invalid_request');
|
|
720
|
+
this.assertNoHostAssociations(entry);
|
|
721
|
+
return replaceEntry(catalog, entry, { ...entry, state: 'deleting' });
|
|
722
|
+
});
|
|
723
|
+
}, async () => {
|
|
724
|
+
await this.withUserNative(native => native.delete(input.reference));
|
|
725
|
+
await this.catalogStore.mutate(catalog => ({ ...catalog, entries: catalog.entries.filter(entry => !sameReference(entryReference(entry), input.reference)) }));
|
|
726
|
+
});
|
|
727
|
+
},
|
|
728
|
+
markPrompted: async (input) => {
|
|
729
|
+
await ready();
|
|
730
|
+
await this.users.engine.markPrompted(input.requestId, input.expectedVersion);
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
assertNoHostAssociations(entry) {
|
|
735
|
+
if (entry.grantedConsumers.some(consumer => !this.resolvedConfig.userConsumers.has(consumer))
|
|
736
|
+
|| (!entry.creationOperationId && this.resolvedConfig.allowProviderConsumers.size > 0)) {
|
|
737
|
+
throw pluginError('identity_in_use');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async managementIdentities() {
|
|
741
|
+
const slot = this.requireSlot();
|
|
742
|
+
const catalog = await this.loadCatalog(slot);
|
|
743
|
+
const descriptors = await this.withUserNative(native => native.list());
|
|
744
|
+
const grants = await this.users.engine.listGrants();
|
|
745
|
+
return descriptors.map(descriptor => {
|
|
746
|
+
const entry = findEntry(catalog, descriptor.reference);
|
|
747
|
+
const integrations = (entry?.grantedConsumers ?? []).filter(consumer => !this.resolvedConfig.userConsumers.has(consumer)).map(consumer => ({
|
|
748
|
+
consumer, displayName: consumer,
|
|
749
|
+
kind: this.resolvedConfig.allowProviderConsumers.has(consumer) ? 'provider' : 'legacy',
|
|
750
|
+
}));
|
|
751
|
+
let deleteBlockedReason;
|
|
752
|
+
if (entry?.state === 'deleting')
|
|
753
|
+
deleteBlockedReason = 'Identity deletion is in progress.';
|
|
754
|
+
else if (integrations.length || (!entry?.creationOperationId && this.resolvedConfig.allowProviderConsumers.size > 0)) {
|
|
755
|
+
deleteBlockedReason = 'This identity has a legacy or Host-managed association.';
|
|
756
|
+
}
|
|
757
|
+
else if (grants.some(grant => grant.status === 'active' && sameReference(grant.identity, descriptor.reference))) {
|
|
758
|
+
deleteBlockedReason = 'Revoke the effective plugin grants before deleting this identity.';
|
|
759
|
+
}
|
|
760
|
+
return { ...descriptor, ...(entry?.label === undefined ? {} : { label: entry.label }),
|
|
761
|
+
...(entry?.handle === undefined ? {} : { handle: entry.handle }),
|
|
762
|
+
catalogState: entry?.state ?? 'unclaimed', integrations,
|
|
763
|
+
...(deleteBlockedReason === undefined ? {} : { deleteBlockedReason }) };
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
installedEntries() {
|
|
767
|
+
const loader = this.hostContext.get('loader');
|
|
768
|
+
if (typeof loader?.entries !== 'function')
|
|
769
|
+
throw pluginError('consumer_forbidden');
|
|
770
|
+
return [...loader.entries()];
|
|
771
|
+
}
|
|
772
|
+
resolveInstalledCaller(context) {
|
|
773
|
+
const entry = this.installedEntries().find(value => sameFiber(value.fiber, context.fiber));
|
|
774
|
+
if (!entry || entry.disabled || !this.resolvedConfig.userConsumers.has(entry.options.name)
|
|
775
|
+
|| ![1, 2].includes(entry.fiber.state))
|
|
776
|
+
throw pluginError('consumer_forbidden');
|
|
777
|
+
return { consumer: entry.options.name, displayName: entry.options.name };
|
|
778
|
+
}
|
|
779
|
+
assertInstalledCaller(caller) {
|
|
780
|
+
if (this.disabledFences.has(caller.consumer))
|
|
781
|
+
throw pluginError('consumer_forbidden');
|
|
782
|
+
const entry = this.installedEntries().find(value => value.options.name === caller.consumer
|
|
783
|
+
&& !value.disabled && value.fiber !== undefined && [1, 2].includes(value.fiber.state));
|
|
784
|
+
if (!entry || !this.resolvedConfig.userConsumers.has(caller.consumer))
|
|
785
|
+
throw pluginError('consumer_forbidden');
|
|
786
|
+
}
|
|
787
|
+
cancelDisabledRequests(consumer) {
|
|
788
|
+
if (!consumer || !this.resolvedConfig.userConsumers.has(consumer) || this.disabledFences.has(consumer))
|
|
789
|
+
return;
|
|
790
|
+
this.disabledFences.add(consumer);
|
|
791
|
+
const pending = this.users.ready.then(() => this.users.engine.cancelPendingForConsumer(consumer)).then(() => {
|
|
792
|
+
this.disabledFences.delete(consumer);
|
|
793
|
+
}).catch(() => {
|
|
794
|
+
// Keep the admission fence on a persistence failure; never silently revive pending approval.
|
|
795
|
+
this.hostContext.logger.error('ANP Identity could not persist plugin request cancellation.');
|
|
796
|
+
});
|
|
797
|
+
this.lifecycleWrites.add(pending);
|
|
798
|
+
void pending.finally(() => { this.lifecycleWrites.delete(pending); });
|
|
799
|
+
}
|
|
800
|
+
assertLegacyConsumer(consumer) {
|
|
801
|
+
if (this.resolvedConfig.userConsumers.has(consumer))
|
|
802
|
+
throw pluginError('capability_forbidden');
|
|
803
|
+
}
|
|
804
|
+
assertLegacyCallerContext(consumer) {
|
|
805
|
+
if (this.resolvedConfig.userConsumers.size === 0
|
|
806
|
+
|| sameFiber(this.ctx.fiber, this.hostContext.fiber) || sameFiber(this.ctx.fiber, this.hostContext.root.fiber))
|
|
807
|
+
return;
|
|
808
|
+
const installed = this.installedEntries().find(entry => sameFiber(entry.fiber, this.ctx.fiber));
|
|
809
|
+
if (!installed || installed.disabled || installed.options.name !== consumer
|
|
810
|
+
|| !installed.fiber || ![1, 2].includes(installed.fiber.state)
|
|
811
|
+
|| this.resolvedConfig.userConsumers.has(installed.options.name))
|
|
812
|
+
throw pluginError('consumer_forbidden');
|
|
813
|
+
}
|
|
814
|
+
async withUserNative(operation) {
|
|
815
|
+
const slot = this.requireSlot();
|
|
816
|
+
const provider = await this.awaitProvider(slot);
|
|
817
|
+
const native = provider.acquireLease({ consumer: '@agent-network-protocol/dsh-anp-identity',
|
|
818
|
+
capabilities: ['IDENTITY_READ', 'IDENTITY_CREATE', 'IDENTITY_SIGN', 'IDENTITY_HTTP_SIGNATURE', 'IDENTITY_DELETE'], ttlSeconds: 300 });
|
|
819
|
+
try {
|
|
820
|
+
this.assertSlot(slot);
|
|
821
|
+
return await operation(native);
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
native.dispose();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async userSnapshot(caller, reference) {
|
|
828
|
+
this.assertInstalledCaller(caller);
|
|
829
|
+
const catalog = await this.loadCatalog(this.requireSlot());
|
|
830
|
+
const entry = findEntry(catalog, reference);
|
|
831
|
+
if (!entry)
|
|
832
|
+
throw pluginError('identity_not_found');
|
|
833
|
+
if (entry.state === 'deleting')
|
|
834
|
+
throw pluginError('identity_deleting');
|
|
835
|
+
this.assertNoHostAssociations(entry);
|
|
836
|
+
const identity = await this.withUserNative(native => native.publicIdentity(reference));
|
|
837
|
+
if (identity.state !== 'active' || !identity.capabilities.didWba)
|
|
838
|
+
throw pluginError('provider_incompatible');
|
|
839
|
+
requestSigningKid(identity);
|
|
840
|
+
return { version: 'ordinary-use/1', capabilities: ['identity:read', 'identity:sign', 'identity:http-auth'],
|
|
841
|
+
signingPurposes: ['authentication', 'application_assertion'],
|
|
842
|
+
httpOrigins: [...(this.resolvedConfig.httpAllowedOrigins.get(caller.consumer) ?? [])].sort() };
|
|
843
|
+
}
|
|
844
|
+
async createConfirmed(operationId, caller, parameters) {
|
|
845
|
+
this.assertInstalledCaller(caller);
|
|
846
|
+
return this.catalogStore.withNativeCreateLock(async () => this.withUserNative(async (native) => {
|
|
847
|
+
const catalog = await this.loadCatalog(this.requireSlot());
|
|
848
|
+
const existing = catalog.entries.find(entry => entry.creationOperationId === operationId);
|
|
849
|
+
if (existing)
|
|
850
|
+
return { reference: entryReference(existing), label: existing.label ?? parameters.label };
|
|
851
|
+
const prior = (await this.catalogStore.listIntents()).find(intent => intent.requestId === operationId);
|
|
852
|
+
if (prior) {
|
|
853
|
+
const result = await this.recoverConfirmedIntent(operationId, parameters, native);
|
|
854
|
+
if (result)
|
|
855
|
+
return result;
|
|
856
|
+
throw pluginError('catalog_conflict');
|
|
857
|
+
}
|
|
858
|
+
if ((await this.catalogStore.listIntents()).some(intent => intent.reference === undefined)) {
|
|
859
|
+
throw pluginError('catalog_conflict');
|
|
860
|
+
}
|
|
861
|
+
const baseline = await native.list();
|
|
862
|
+
await this.catalogStore.reserveIntent({ schema: CREATE_INTENT_SCHEMA, requestId: operationId,
|
|
863
|
+
consumer: caller.consumer, label: parameters.label, createdAt: new Date().toISOString(),
|
|
864
|
+
...(parameters.handle === undefined ? {} : { handle: parameters.handle }),
|
|
865
|
+
authorizationMode: 'user', baselineIdentityIds: baseline.map(item => item.reference.identityId) });
|
|
866
|
+
const identity = await native.create({ profile: 'e1', domain: parameters.domain,
|
|
867
|
+
pathSegments: parameters.path.split('/').filter(Boolean), capabilities: { didWba: true },
|
|
868
|
+
managedKeys: [{ fragment: 'root', role: 'root_control' }, { fragment: 'request', role: 'request_signing' }] });
|
|
869
|
+
await this.catalogStore.updateIntent(operationId, intent => ({ ...intent, reference: identity.reference }));
|
|
870
|
+
const entry = await this.catalogStore.commitIntent(operationId);
|
|
871
|
+
await this.catalogStore.deleteIntent(operationId);
|
|
872
|
+
return { reference: entryReference(entry), label: entry.label ?? parameters.label };
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
async reconcileConfirmed(operationId, _caller, parameters) {
|
|
876
|
+
return this.catalogStore.withNativeCreateLock(async () => this.withUserNative(async (native) => {
|
|
877
|
+
const existing = (await this.loadCatalog(this.requireSlot())).entries.find(entry => entry.creationOperationId === operationId);
|
|
878
|
+
if (existing)
|
|
879
|
+
return { reference: entryReference(existing), label: existing.label ?? parameters.label };
|
|
880
|
+
return this.recoverConfirmedIntent(operationId, parameters, native);
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
async recoverConfirmedIntent(operationId, parameters, native) {
|
|
884
|
+
const intent = (await this.catalogStore.listIntents()).find(value => value.requestId === operationId && value.authorizationMode === 'user');
|
|
885
|
+
if (!intent)
|
|
886
|
+
return undefined;
|
|
887
|
+
let reference = intent.reference;
|
|
888
|
+
if (!reference) {
|
|
889
|
+
const known = new Set((await this.catalogStore.load()).entries.map(entry => entry.identityId));
|
|
890
|
+
const candidates = (await native.list()).filter(item => !intent.baselineIdentityIds.includes(item.reference.identityId) && !known.has(item.reference.identityId));
|
|
891
|
+
if (candidates.length !== 1)
|
|
892
|
+
return undefined;
|
|
893
|
+
reference = candidates[0].reference;
|
|
894
|
+
const recovered = reference;
|
|
895
|
+
await this.catalogStore.updateIntent(operationId, current => ({ ...current, reference: recovered }));
|
|
896
|
+
}
|
|
897
|
+
await native.publicIdentity(reference);
|
|
898
|
+
const entry = await this.catalogStore.commitIntent(operationId);
|
|
899
|
+
await this.catalogStore.deleteIntent(operationId);
|
|
900
|
+
return { reference: entryReference(entry), label: entry.label ?? parameters.label };
|
|
901
|
+
}
|
|
902
|
+
async assertNoUnresolvedUserCreate() {
|
|
903
|
+
if ((await this.catalogStore.listIntents()).some(intent => intent.authorizationMode === 'user' && !intent.reference)) {
|
|
904
|
+
throw pluginError('catalog_conflict');
|
|
905
|
+
}
|
|
906
|
+
}
|
|
559
907
|
async disposeSlot(slot) {
|
|
560
908
|
if (slot.disposed)
|
|
561
909
|
return;
|
|
@@ -885,6 +1233,12 @@ function resolveConfig(config) {
|
|
|
885
1233
|
throw new TypeError('anp-identity: stateRoot must be absolute');
|
|
886
1234
|
const allowConsumers = new Set((config.allowConsumers ?? []).map(validateConsumer));
|
|
887
1235
|
const allowProviderConsumers = new Set((config.allowProviderConsumers ?? []).map(validateConsumer));
|
|
1236
|
+
const userConsumers = new Set((config.userConsumers ?? []).map(validateConsumer));
|
|
1237
|
+
for (const consumer of userConsumers) {
|
|
1238
|
+
if (!allowConsumers.has(consumer) || allowProviderConsumers.has(consumer)) {
|
|
1239
|
+
throw new TypeError('anp-identity: User consumers must be allowed and cannot be trusted Providers');
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
888
1242
|
for (const consumer of allowProviderConsumers) {
|
|
889
1243
|
if (!allowConsumers.has(consumer)) {
|
|
890
1244
|
throw new TypeError('anp-identity: Host Provider consumers must also be allowed clients');
|
|
@@ -907,6 +1261,7 @@ function resolveConfig(config) {
|
|
|
907
1261
|
allowProviderConsumers,
|
|
908
1262
|
httpAllowedOrigins,
|
|
909
1263
|
recoveryOnOpen: config.recoveryOnOpen ?? true,
|
|
1264
|
+
userConsumers,
|
|
910
1265
|
};
|
|
911
1266
|
}
|
|
912
1267
|
function validateRegistration(value) {
|