@enbox/api 0.2.3 → 0.3.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.
Files changed (73) hide show
  1. package/README.md +235 -35
  2. package/dist/browser.mjs +13 -13
  3. package/dist/browser.mjs.map +4 -4
  4. package/dist/esm/dwn-api.js +24 -10
  5. package/dist/esm/dwn-api.js.map +1 -1
  6. package/dist/esm/index.js +6 -0
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/esm/live-query.js +34 -5
  9. package/dist/esm/live-query.js.map +1 -1
  10. package/dist/esm/permission-grant.js +3 -6
  11. package/dist/esm/permission-grant.js.map +1 -1
  12. package/dist/esm/permission-request.js +4 -7
  13. package/dist/esm/permission-request.js.map +1 -1
  14. package/dist/esm/record-data.js +131 -0
  15. package/dist/esm/record-data.js.map +1 -0
  16. package/dist/esm/record-types.js +9 -0
  17. package/dist/esm/record-types.js.map +1 -0
  18. package/dist/esm/record.js +58 -184
  19. package/dist/esm/record.js.map +1 -1
  20. package/dist/esm/repository-types.js +13 -0
  21. package/dist/esm/repository-types.js.map +1 -0
  22. package/dist/esm/repository.js +347 -0
  23. package/dist/esm/repository.js.map +1 -0
  24. package/dist/esm/typed-live-query.js +129 -0
  25. package/dist/esm/typed-live-query.js.map +1 -0
  26. package/dist/esm/typed-record.js +227 -0
  27. package/dist/esm/typed-record.js.map +1 -0
  28. package/dist/esm/typed-web5.js +134 -23
  29. package/dist/esm/typed-web5.js.map +1 -1
  30. package/dist/esm/web5.js +83 -22
  31. package/dist/esm/web5.js.map +1 -1
  32. package/dist/types/dwn-api.d.ts.map +1 -1
  33. package/dist/types/index.d.ts +6 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/types/live-query.d.ts +43 -4
  36. package/dist/types/live-query.d.ts.map +1 -1
  37. package/dist/types/permission-grant.d.ts +1 -1
  38. package/dist/types/permission-grant.d.ts.map +1 -1
  39. package/dist/types/permission-request.d.ts +1 -1
  40. package/dist/types/permission-request.d.ts.map +1 -1
  41. package/dist/types/record-data.d.ts +49 -0
  42. package/dist/types/record-data.d.ts.map +1 -0
  43. package/dist/types/record-types.d.ts +145 -0
  44. package/dist/types/record-types.d.ts.map +1 -0
  45. package/dist/types/record.d.ts +13 -144
  46. package/dist/types/record.d.ts.map +1 -1
  47. package/dist/types/repository-types.d.ts +137 -0
  48. package/dist/types/repository-types.d.ts.map +1 -0
  49. package/dist/types/repository.d.ts +59 -0
  50. package/dist/types/repository.d.ts.map +1 -0
  51. package/dist/types/typed-live-query.d.ts +111 -0
  52. package/dist/types/typed-live-query.d.ts.map +1 -0
  53. package/dist/types/typed-record.d.ts +179 -0
  54. package/dist/types/typed-record.d.ts.map +1 -0
  55. package/dist/types/typed-web5.d.ts +55 -24
  56. package/dist/types/typed-web5.d.ts.map +1 -1
  57. package/dist/types/web5.d.ts +54 -4
  58. package/dist/types/web5.d.ts.map +1 -1
  59. package/package.json +8 -7
  60. package/src/dwn-api.ts +30 -13
  61. package/src/index.ts +6 -0
  62. package/src/live-query.ts +71 -7
  63. package/src/permission-grant.ts +2 -3
  64. package/src/permission-request.ts +3 -4
  65. package/src/record-data.ts +155 -0
  66. package/src/record-types.ts +188 -0
  67. package/src/record.ts +86 -389
  68. package/src/repository-types.ts +249 -0
  69. package/src/repository.ts +391 -0
  70. package/src/typed-live-query.ts +200 -0
  71. package/src/typed-record.ts +309 -0
  72. package/src/typed-web5.ts +202 -49
  73. package/src/web5.ts +162 -27
package/src/web5.ts CHANGED
@@ -96,6 +96,38 @@ export type Web5AnonymousApi = {
96
96
  dwn: DwnReaderApi;
97
97
  };
98
98
 
99
+ /** Parameters passed to the onProviderAuthRequired callback. */
100
+ export type ProviderAuthParams = {
101
+ /** Full authorize URL to open in a browser (query params already appended). */
102
+ authorizeUrl: string;
103
+ /** The DWN endpoint URL this auth is for (informational). */
104
+ dwnEndpoint: string;
105
+ /** CSRF nonce — the provider will return this unchanged in the redirect. */
106
+ state: string;
107
+ };
108
+
109
+ /** Result returned by the app after the user completes provider auth. */
110
+ export type ProviderAuthResult = {
111
+ /** Authorization code from the provider's redirect. */
112
+ code: string;
113
+ /** Must match the state from ProviderAuthParams (CSRF validation). */
114
+ state: string;
115
+ };
116
+
117
+ /** Persisted registration token data for a DWN endpoint. */
118
+ export type RegistrationTokenData = {
119
+ /** Opaque registration token for POST /registration. */
120
+ registrationToken: string;
121
+ /** Refresh token for obtaining new registration tokens. */
122
+ refreshToken?: string;
123
+ /** Unix timestamp (ms) when the token expires. Undefined = never expires. */
124
+ expiresAt?: number;
125
+ /** Provider's token exchange URL (needed for code exchange). */
126
+ tokenUrl: string;
127
+ /** Provider's refresh URL (needed for token refresh). */
128
+ refreshUrl?: string;
129
+ };
130
+
99
131
  /** Optional overrides that can be provided when calling {@link Web5.connect}. */
100
132
  export type Web5ConnectOptions = {
101
133
  /**
@@ -165,8 +197,13 @@ export type Web5ConnectOptions = {
165
197
 
166
198
  /**
167
199
  * Enable synchronization of DWN records between local and remote DWNs.
168
- * Sync defaults to running every 2 minutes and can be set to any value accepted by `ms()`.
169
- * To disable sync set to 'off'.
200
+ *
201
+ * - **Omitted / `undefined`**: Live sync mode (default). Opens real-time
202
+ * `MessagesSubscribe` WebSocket subscriptions for instant pull and
203
+ * push-on-write, with a background SMT integrity check every 5 minutes.
204
+ * - **Interval string** (e.g. `'2m'`, `'30s'`): Poll mode. Performs a full
205
+ * SMT set-reconciliation sync at the specified interval.
206
+ * - **`'off'`**: Sync is disabled entirely.
170
207
  */
171
208
  sync?: string;
172
209
 
@@ -190,10 +227,29 @@ export type Web5ConnectOptions = {
190
227
  * If registration is successful, the `onSuccess` callback will be called.
191
228
  */
192
229
  registration? : {
193
- /** Called when all of the DWN registrations are successful */
194
- onSuccess: () => void;
195
- /** Called when any of the DWN registrations fail */
196
- onFailure: (error: any) => void;
230
+ /** Called when all of the DWN registrations are successful. */
231
+ onSuccess : () => void;
232
+ /** Called when any of the DWN registrations fail. */
233
+ onFailure : (error: any) => void;
234
+
235
+ /**
236
+ * Called when a DWN endpoint requires provider auth (`'provider-auth-v0'`).
237
+ * The app is responsible for opening the authorizeUrl in a browser,
238
+ * capturing the redirect back, and returning the auth code.
239
+ * If not provided, provider-auth endpoints fall back to PoW registration.
240
+ */
241
+ onProviderAuthRequired? : (params: ProviderAuthParams) => Promise<ProviderAuthResult>;
242
+
243
+ /**
244
+ * Pre-existing registration tokens from a previous session, keyed by DWN endpoint URL.
245
+ * If a valid (non-expired) token exists for an endpoint, it is used directly.
246
+ */
247
+ registrationTokens? : Record<string, RegistrationTokenData>;
248
+
249
+ /**
250
+ * Called when new registration tokens are obtained so the app can persist them.
251
+ */
252
+ onRegistrationTokens? : (tokens: Record<string, RegistrationTokenData>) => void;
197
253
  }
198
254
  };
199
255
 
@@ -449,17 +505,14 @@ export class Web5 {
449
505
  throw new Error(`Failed to connect to wallet: ${error.message}`);
450
506
  }
451
507
  } else {
452
- // No connected identity found and no connectOptions provided, use local Identities
453
- // Query the Agent's DWN tenant for identity records.
508
+ // No connected (WalletConnect) identity and no walletConnectOptions provided.
509
+ // Look for an existing local identity, or create one on first use.
454
510
  const identities = await userAgent.identity.list();
455
511
 
456
- // If an existing identity is not found found, create a new one.
457
- const existingIdentityCount = identities.length;
458
- if (existingIdentityCount === 0) {
459
- // since we are creating a new identity, we will want to register sync for the created Did
512
+ if (identities.length === 0) {
460
513
  registerSync = true;
461
514
 
462
- // Generate a new Identity for the end-user.
515
+ // First use — generate a new Identity for the end-user.
463
516
  identity = await userAgent.identity.create({
464
517
  didMethod : 'dht',
465
518
  metadata : { name: 'Default' },
@@ -489,8 +542,9 @@ export class Web5 {
489
542
  });
490
543
 
491
544
  } else {
492
- // If multiple identities are found, use the first one.
493
- // TODO: Implement selecting a connectedDid from multiple identities
545
+ // Reconnecting use the first local identity. When the agent manages
546
+ // multiple identities (e.g. created via agent.identity.create()), the
547
+ // first one returned by the store is used as the default for connect().
494
548
  identity = identities[0];
495
549
  }
496
550
  }
@@ -500,27 +554,105 @@ export class Web5 {
500
554
  // If the stored identity has a connected DID, use the identity DID as the delegated DID, otherwise it is undefined.
501
555
  delegateDid = identity.metadata.connectedDid ? identity.did.uri : undefined;
502
556
  if (registration !== undefined) {
503
- // If a registration object is passed, we attempt to register the AgentDID and the ConnectedDID with the DWN endpoints provided
557
+ const updatedTokens: Record<string, RegistrationTokenData> = {
558
+ ...(registration.registrationTokens ?? {}),
559
+ };
560
+
504
561
  try {
505
562
  for (const dwnEndpoint of serviceEndpointNodes) {
506
- // check if endpoint needs registration
507
563
  const serverInfo = await userAgent.rpc.getServerInfo(dwnEndpoint);
564
+
508
565
  if (serverInfo.registrationRequirements.length === 0) {
509
- // no registration required
510
566
  continue;
511
567
  }
512
568
 
513
- // register the agent DID
514
- await DwnRegistrar.registerTenant(dwnEndpoint, agent.agentDid.uri);
569
+ // Deduplicate DIDs to register.
570
+ const didsToRegister = [agent.agentDid.uri, connectedDid]
571
+ .filter((did, i, arr): did is string => arr.indexOf(did) === i);
572
+
573
+ const hasProviderAuth = serverInfo.registrationRequirements.includes('provider-auth-v0')
574
+ && serverInfo.providerAuth !== undefined;
575
+
576
+ if (hasProviderAuth && registration.onProviderAuthRequired) {
577
+ // --- Provider Auth Path ---
578
+ let tokenData = updatedTokens[dwnEndpoint];
579
+
580
+ // Refresh expired tokens.
581
+ if (tokenData?.expiresAt !== undefined && tokenData.expiresAt < Date.now()) {
582
+ if (tokenData.refreshUrl && tokenData.refreshToken) {
583
+ const refreshed = await DwnRegistrar.refreshRegistrationToken(
584
+ tokenData.refreshUrl, tokenData.refreshToken,
585
+ );
586
+ tokenData = {
587
+ registrationToken : refreshed.registrationToken,
588
+ refreshToken : refreshed.refreshToken,
589
+ expiresAt : refreshed.expiresIn !== undefined
590
+ ? Date.now() + (refreshed.expiresIn * 1000) : undefined,
591
+ tokenUrl : tokenData.tokenUrl,
592
+ refreshUrl : tokenData.refreshUrl,
593
+ };
594
+ updatedTokens[dwnEndpoint] = tokenData;
595
+ } else {
596
+ tokenData = undefined;
597
+ }
598
+ }
599
+
600
+ // Run the auth flow if no valid token exists.
601
+ if (tokenData === undefined) {
602
+ const state = crypto.randomUUID();
603
+ const providerAuth = serverInfo.providerAuth!;
604
+ const separator = providerAuth.authorizeUrl.includes('?') ? '&' : '?';
605
+ const authorizeUrl = `${providerAuth.authorizeUrl}${separator}`
606
+ + `redirect_uri=${encodeURIComponent(dwnEndpoint)}`
607
+ + `&state=${encodeURIComponent(state)}`;
608
+
609
+ const authResult = await registration.onProviderAuthRequired({
610
+ authorizeUrl,
611
+ dwnEndpoint,
612
+ state,
613
+ });
614
+
615
+ if (authResult.state !== state) {
616
+ throw new Error('Provider auth state mismatch — possible CSRF attack.');
617
+ }
618
+
619
+ const tokenResponse = await DwnRegistrar.exchangeAuthCode(
620
+ providerAuth.tokenUrl, authResult.code, dwnEndpoint,
621
+ );
622
+
623
+ tokenData = {
624
+ registrationToken : tokenResponse.registrationToken,
625
+ refreshToken : tokenResponse.refreshToken,
626
+ expiresAt : tokenResponse.expiresIn !== undefined
627
+ ? Date.now() + (tokenResponse.expiresIn * 1000) : undefined,
628
+ tokenUrl : providerAuth.tokenUrl,
629
+ refreshUrl : providerAuth.refreshUrl,
630
+ };
631
+ updatedTokens[dwnEndpoint] = tokenData;
632
+ }
633
+
634
+ // Register each DID using the provider auth token.
635
+ for (const did of didsToRegister) {
636
+ await DwnRegistrar.registerTenantWithToken(
637
+ dwnEndpoint, did, tokenData.registrationToken,
638
+ );
639
+ }
640
+
641
+ } else {
642
+ // --- Default Path (PoW / general registration) ---
643
+ for (const did of didsToRegister) {
644
+ await DwnRegistrar.registerTenant(dwnEndpoint, did);
645
+ }
646
+ }
647
+ }
515
648
 
516
- // register the connected Identity DID
517
- await DwnRegistrar.registerTenant(dwnEndpoint, connectedDid);
649
+ // Notify app of updated tokens for persistence.
650
+ if (registration.onRegistrationTokens) {
651
+ registration.onRegistrationTokens(updatedTokens);
518
652
  }
519
653
 
520
- // If no failures occurred, call the onSuccess callback
521
654
  registration.onSuccess();
522
655
  } catch (error) {
523
- // for any failure, call the onFailure callback with the error
524
656
  registration.onFailure(error);
525
657
  }
526
658
  }
@@ -546,8 +678,11 @@ export class Web5 {
546
678
  }
547
679
 
548
680
  // Enable sync using the specified interval or default.
549
- sync ??= '2m';
550
- userAgent.sync.startSync({ interval: sync })
681
+ // When sync is unset (undefined), default to live mode.
682
+ // When sync is an interval string (e.g. '2m', '30s'), use poll mode with that interval.
683
+ const syncMode = sync === undefined ? 'live' : 'poll';
684
+ const syncInterval = sync ?? (syncMode === 'live' ? '5m' : '2m');
685
+ userAgent.sync.startSync({ mode: syncMode, interval: syncInterval })
551
686
  .catch((error: any) => {
552
687
  console.error(`Sync failed: ${error}`);
553
688
  });
@@ -599,7 +734,7 @@ export class Web5 {
599
734
  const connectedProtocols = new Set<string>();
600
735
  for (const grantMessage of grants) {
601
736
  // use the delegateDid as the connectedDid of the grant as they do not yet support impersonation/delegation
602
- const grant = await PermissionGrant.parse({ connectedDid: delegateDid, agent, message: grantMessage });
737
+ const grant = PermissionGrant.parse({ connectedDid: delegateDid, agent, message: grantMessage });
603
738
  // store the grant as the owner of the DWN, this will allow the delegateDid to use the grant when impersonating the connectedDid
604
739
  const { status } = await grant.store(true);
605
740
  if (status.code !== 202) {