@agent-network-protocol/dsh-anp-identity 0.1.2 → 0.1.3-rc.2

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.
Files changed (75) hide show
  1. package/BOUNDARY.md +14 -2
  2. package/README.md +33 -4
  3. package/README.zh.md +10 -2
  4. package/V7-MANAGEMENT.md +90 -0
  5. package/cordis.patch.yml +4 -0
  6. package/lib/authorization-types.d.ts +150 -0
  7. package/lib/authorization-types.d.ts.map +1 -0
  8. package/lib/authorization-types.js +2 -0
  9. package/lib/authorization-types.js.map +1 -0
  10. package/lib/authorization.d.ts +45 -0
  11. package/lib/authorization.d.ts.map +1 -0
  12. package/lib/authorization.js +718 -0
  13. package/lib/authorization.js.map +1 -0
  14. package/lib/catalog.d.ts +7 -2
  15. package/lib/catalog.d.ts.map +1 -1
  16. package/lib/catalog.js +45 -23
  17. package/lib/catalog.js.map +1 -1
  18. package/lib/client/controller.d.ts +78 -0
  19. package/lib/client/controller.d.ts.map +1 -0
  20. package/lib/client/controller.js +365 -0
  21. package/lib/client/controller.js.map +1 -0
  22. package/lib/client/identity-icon.d.ts +3 -0
  23. package/lib/client/identity-icon.d.ts.map +1 -0
  24. package/lib/client/identity-icon.js +6 -0
  25. package/lib/client/identity-icon.js.map +1 -0
  26. package/lib/client/index.d.ts +7 -0
  27. package/lib/client/index.d.ts.map +1 -0
  28. package/lib/client/index.js +51 -0
  29. package/lib/client/index.js.map +1 -0
  30. package/lib/client/settings-icon-slot.d.ts +13 -0
  31. package/lib/client/settings-icon-slot.d.ts.map +1 -0
  32. package/lib/client/settings-icon-slot.js +2 -0
  33. package/lib/client/settings-icon-slot.js.map +1 -0
  34. package/lib/client/styles.d.ts +4 -0
  35. package/lib/client/styles.d.ts.map +1 -0
  36. package/lib/client/styles.js +39 -0
  37. package/lib/client/styles.js.map +1 -0
  38. package/lib/client/views.d.ts +11 -0
  39. package/lib/client/views.d.ts.map +1 -0
  40. package/lib/client/views.js +287 -0
  41. package/lib/client/views.js.map +1 -0
  42. package/lib/client.js +21008 -0
  43. package/lib/client.js.map +7 -0
  44. package/lib/index.d.ts +26 -0
  45. package/lib/index.d.ts.map +1 -1
  46. package/lib/index.js +413 -58
  47. package/lib/index.js.map +1 -1
  48. package/lib/management-remote.d.ts +23 -0
  49. package/lib/management-remote.d.ts.map +1 -0
  50. package/lib/management-remote.js +126 -0
  51. package/lib/management-remote.js.map +1 -0
  52. package/lib/management-types.d.ts +50 -0
  53. package/lib/management-types.d.ts.map +1 -0
  54. package/lib/management-types.js +2 -0
  55. package/lib/management-types.js.map +1 -0
  56. package/lib/provider-api.d.ts +3 -0
  57. package/lib/provider-api.d.ts.map +1 -1
  58. package/lib/provider-api.js.map +1 -1
  59. package/lib/remote.d.ts +4 -0
  60. package/lib/remote.d.ts.map +1 -0
  61. package/lib/remote.js +86 -0
  62. package/lib/remote.js.map +1 -0
  63. package/lib/user-client.d.ts +41 -0
  64. package/lib/user-client.d.ts.map +1 -0
  65. package/lib/user-client.js +6 -0
  66. package/lib/user-client.js.map +1 -0
  67. package/lib/user-operation.d.ts +7 -0
  68. package/lib/user-operation.d.ts.map +1 -0
  69. package/lib/user-operation.js +87 -0
  70. package/lib/user-operation.js.map +1 -0
  71. package/lib/user-service.d.ts +26 -0
  72. package/lib/user-service.d.ts.map +1 -0
  73. package/lib/user-service.js +140 -0
  74. package/lib/user-service.js.map +1 -0
  75. 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.catalogStore.mutate((catalog) => {
220
- const entry = findEntry(catalog, resolved);
221
- if (entry === undefined)
222
- throw pluginError('identity_not_found');
223
- if (entry.state !== 'active')
224
- throw pluginError('identity_deleting');
225
- if (entry.grantedConsumers.some(consumer => consumer !== lease.consumer)) {
226
- throw pluginError('identity_in_use');
227
- }
228
- return replaceEntry(catalog, entry, { ...entry, state: 'deleting' });
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
- const native = await nativeCall(() => lease.native.recover());
239
- const report = await this.reconcile(lease.slot, lease.native, true, false);
240
- return { ...native, ...report };
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.catalogStore.mutate((catalog) => {
275
- const entry = findEntry(catalog, reference);
276
- if (entry === undefined)
277
- throw pluginError('identity_not_found');
278
- if (entry.state === 'deleting')
279
- throw pluginError('identity_deleting');
280
- return replaceEntry(catalog, entry, {
281
- ...entry,
282
- state: 'active',
283
- grantedConsumers: [...new Set([...entry.grantedConsumers, consumer])].sort(),
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
- let catalogEntry;
340
- try {
341
- const catalog = await this.loadCatalog(slot);
342
- catalogEntry = findEntry(catalog, reference);
343
- }
344
- catch (error) {
345
- if (!isPluginCode(error, 'catalog_corrupt'))
346
- throw error;
347
- throw pluginError('catalog_corrupt');
348
- }
349
- if (catalogEntry !== undefined) {
350
- if (catalogEntry.grantedConsumers.some(value => value !== consumer)) {
351
- throw pluginError('identity_in_use');
352
- }
353
- if (catalogEntry.state !== 'deleting') {
354
- await this.catalogStore.mutate(catalog => {
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) {