@interop/was-react 0.1.2 → 0.1.3

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 (149) hide show
  1. package/README.md +9 -1
  2. package/dist/auth/chapi.d.ts +3 -1
  3. package/dist/auth/chapi.d.ts.map +1 -1
  4. package/dist/auth/chapi.js +6 -2
  5. package/dist/auth/chapi.js.map +1 -1
  6. package/dist/auth/loginFlow.d.ts +30 -10
  7. package/dist/auth/loginFlow.d.ts.map +1 -1
  8. package/dist/auth/loginFlow.js +3 -1
  9. package/dist/auth/loginFlow.js.map +1 -1
  10. package/dist/auth/loginRequest.d.ts +9 -3
  11. package/dist/auth/loginRequest.d.ts.map +1 -1
  12. package/dist/auth/loginRequest.js +9 -3
  13. package/dist/auth/loginRequest.js.map +1 -1
  14. package/dist/auth/verifyResponse.d.ts +9 -3
  15. package/dist/auth/verifyResponse.d.ts.map +1 -1
  16. package/dist/auth/verifyResponse.js +6 -2
  17. package/dist/auth/verifyResponse.js.map +1 -1
  18. package/dist/config.d.ts +60 -20
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +12 -4
  21. package/dist/config.js.map +1 -1
  22. package/dist/dev/cli.d.ts.map +1 -1
  23. package/dist/dev/cli.js +4 -7
  24. package/dist/dev/cli.js.map +1 -1
  25. package/dist/dev/provisionDevGrants.d.ts +15 -5
  26. package/dist/dev/provisionDevGrants.d.ts.map +1 -1
  27. package/dist/dev/provisionDevGrants.js +2 -2
  28. package/dist/dev/provisionDevGrants.js.map +1 -1
  29. package/dist/grants.d.ts +7 -19
  30. package/dist/grants.d.ts.map +1 -1
  31. package/dist/grants.js +28 -7
  32. package/dist/grants.js.map +1 -1
  33. package/dist/identity/appSession.d.ts +9 -3
  34. package/dist/identity/appSession.d.ts.map +1 -1
  35. package/dist/identity/appSession.js +3 -1
  36. package/dist/identity/appSession.js.map +1 -1
  37. package/dist/identity/documentLoader.d.ts +3 -1
  38. package/dist/identity/documentLoader.d.ts.map +1 -1
  39. package/dist/identity/documentLoader.js +6 -2
  40. package/dist/identity/documentLoader.js.map +1 -1
  41. package/dist/identity/seedCredential.d.ts +9 -3
  42. package/dist/identity/seedCredential.d.ts.map +1 -1
  43. package/dist/identity/seedCredential.js +9 -15
  44. package/dist/identity/seedCredential.js.map +1 -1
  45. package/dist/identity/seedStore.d.ts +18 -6
  46. package/dist/identity/seedStore.d.ts.map +1 -1
  47. package/dist/identity/seedStore.js.map +1 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/react/WasSessionProvider.d.ts.map +1 -1
  53. package/dist/react/WasSessionProvider.js +10 -1
  54. package/dist/react/WasSessionProvider.js.map +1 -1
  55. package/dist/react/hooks.d.ts +10 -5
  56. package/dist/react/hooks.d.ts.map +1 -1
  57. package/dist/react/hooks.js +6 -30
  58. package/dist/react/hooks.js.map +1 -1
  59. package/dist/session/authStore.d.ts +29 -7
  60. package/dist/session/authStore.d.ts.map +1 -1
  61. package/dist/session/authStore.js +123 -63
  62. package/dist/session/authStore.js.map +1 -1
  63. package/dist/storage/entityStore.d.ts +32 -9
  64. package/dist/storage/entityStore.d.ts.map +1 -1
  65. package/dist/storage/entityStore.js +88 -48
  66. package/dist/storage/entityStore.js.map +1 -1
  67. package/dist/storage/localStore.d.ts +13 -0
  68. package/dist/storage/localStore.d.ts.map +1 -1
  69. package/dist/storage/localStore.js +17 -0
  70. package/dist/storage/localStore.js.map +1 -1
  71. package/dist/storage/rehydrate.d.ts.map +1 -1
  72. package/dist/storage/rehydrate.js +3 -1
  73. package/dist/storage/rehydrate.js.map +1 -1
  74. package/dist/storage/syncController.d.ts +7 -3
  75. package/dist/storage/syncController.d.ts.map +1 -1
  76. package/dist/storage/syncController.js +28 -15
  77. package/dist/storage/syncController.js.map +1 -1
  78. package/dist/storage/syncStatusStore.d.ts +26 -1
  79. package/dist/storage/syncStatusStore.d.ts.map +1 -1
  80. package/dist/storage/syncStatusStore.js +40 -0
  81. package/dist/storage/syncStatusStore.js.map +1 -1
  82. package/dist/storage/wasRemoteStore.d.ts +3 -1
  83. package/dist/storage/wasRemoteStore.d.ts.map +1 -1
  84. package/dist/storage/wasRemoteStore.js +3 -3
  85. package/dist/storage/wasRemoteStore.js.map +1 -1
  86. package/dist/sync/feedMasterPort.d.ts +21 -19
  87. package/dist/sync/feedMasterPort.d.ts.map +1 -1
  88. package/dist/sync/feedMasterPort.js +18 -5
  89. package/dist/sync/feedMasterPort.js.map +1 -1
  90. package/dist/sync/index.d.ts +3 -3
  91. package/dist/sync/index.d.ts.map +1 -1
  92. package/dist/sync/index.js +2 -2
  93. package/dist/sync/index.js.map +1 -1
  94. package/dist/sync/lwwConflictHandler.d.ts +35 -2
  95. package/dist/sync/lwwConflictHandler.d.ts.map +1 -1
  96. package/dist/sync/lwwConflictHandler.js +47 -9
  97. package/dist/sync/lwwConflictHandler.js.map +1 -1
  98. package/dist/sync/pushWrites.d.ts +27 -8
  99. package/dist/sync/pushWrites.d.ts.map +1 -1
  100. package/dist/sync/pushWrites.js +43 -15
  101. package/dist/sync/pushWrites.js.map +1 -1
  102. package/dist/sync/types.d.ts +49 -16
  103. package/dist/sync/types.d.ts.map +1 -1
  104. package/dist/sync/types.js +17 -0
  105. package/dist/sync/types.js.map +1 -1
  106. package/dist/sync/wasReplication.d.ts.map +1 -1
  107. package/dist/sync/wasReplication.js +38 -1
  108. package/dist/sync/wasReplication.js.map +1 -1
  109. package/dist/sync/wasSyncPort.d.ts +29 -6
  110. package/dist/sync/wasSyncPort.d.ts.map +1 -1
  111. package/dist/sync/wasSyncPort.js +105 -101
  112. package/dist/sync/wasSyncPort.js.map +1 -1
  113. package/package.json +4 -3
  114. package/dist/auth/verifyResponse.test.d.ts +0 -2
  115. package/dist/auth/verifyResponse.test.d.ts.map +0 -1
  116. package/dist/auth/verifyResponse.test.js +0 -282
  117. package/dist/auth/verifyResponse.test.js.map +0 -1
  118. package/dist/dev/cli.test.d.ts +0 -2
  119. package/dist/dev/cli.test.d.ts.map +0 -1
  120. package/dist/dev/cli.test.js +0 -39
  121. package/dist/dev/cli.test.js.map +0 -1
  122. package/dist/grants.test.d.ts +0 -2
  123. package/dist/grants.test.d.ts.map +0 -1
  124. package/dist/grants.test.js +0 -79
  125. package/dist/grants.test.js.map +0 -1
  126. package/dist/identity/seedCredential.test.d.ts +0 -2
  127. package/dist/identity/seedCredential.test.d.ts.map +0 -1
  128. package/dist/identity/seedCredential.test.js +0 -205
  129. package/dist/identity/seedCredential.test.js.map +0 -1
  130. package/dist/storage/rehydrate.test.d.ts +0 -2
  131. package/dist/storage/rehydrate.test.d.ts.map +0 -1
  132. package/dist/storage/rehydrate.test.js +0 -172
  133. package/dist/storage/rehydrate.test.js.map +0 -1
  134. package/dist/sync/changesQuery.test.d.ts +0 -2
  135. package/dist/sync/changesQuery.test.d.ts.map +0 -1
  136. package/dist/sync/changesQuery.test.js +0 -145
  137. package/dist/sync/changesQuery.test.js.map +0 -1
  138. package/dist/sync/lww.test.d.ts +0 -2
  139. package/dist/sync/lww.test.d.ts.map +0 -1
  140. package/dist/sync/lww.test.js +0 -28
  141. package/dist/sync/lww.test.js.map +0 -1
  142. package/dist/sync/lwwConflictHandler.test.d.ts +0 -2
  143. package/dist/sync/lwwConflictHandler.test.d.ts.map +0 -1
  144. package/dist/sync/lwwConflictHandler.test.js +0 -78
  145. package/dist/sync/lwwConflictHandler.test.js.map +0 -1
  146. package/dist/sync/pushWrites.test.d.ts +0 -2
  147. package/dist/sync/pushWrites.test.d.ts.map +0 -1
  148. package/dist/sync/pushWrites.test.js +0 -287
  149. package/dist/sync/pushWrites.test.js.map +0 -1
@@ -3,8 +3,11 @@
3
3
  */
4
4
  /**
5
5
  * The app-side seam between the generic sync adapter and `@interop/was-client`.
6
- * Implements the `WasSyncPort` interface for one remote WAS Collection using the
7
- * raw, signed `was.request()` escape hatch.
6
+ * Implements the `WasSyncBasePort` interface (query + conditional writes) for one
7
+ * remote WAS Collection using the raw, signed `was.request()` escape hatch. The
8
+ * 412 conflict re-read (`get`) is supplied separately by `withFeedMasterRead`,
9
+ * which resolves the master state from the changes-feed body -- origin-
10
+ * independent, unlike a cross-origin `GET` ETag header.
8
11
  *
9
12
  * Using `request()` (rather than the `Resource` / `Collection` handles) is
10
13
  * deliberate: it moves the stored body VERBATIM, bypassing the encryption codec.
@@ -20,9 +23,29 @@
20
23
  * for plaintext and encrypted resources alike -- so there is no plaintext-vs-
21
24
  * encrypted fork here.
22
25
  */
23
- import type { WasClient } from '@interop/was-client';
26
+ import { type WasClient } from '@interop/was-client';
24
27
  import type { IZcap } from '@interop/data-integrity-core';
25
- import { type WasSyncPort } from './types.js';
28
+ import { type WasSyncBasePort } from './types.js';
29
+ /**
30
+ * Extracts an HTTP status from a raw ky/ezcap error. `was.request()` rejects on
31
+ * any non-2xx with `err.status` set (see `@interop/http-client`'s error
32
+ * normaliser); this reads it defensively from either location. Shared with the
33
+ * remote store's marker PUT and the dev-grant provisioner, which need the raw
34
+ * status for diagnostics rather than the mapped sync error.
35
+ *
36
+ * @param err {unknown}
37
+ * @returns {number | undefined}
38
+ */
39
+ export declare function errorStatus(err: unknown): number | undefined;
40
+ /**
41
+ * Normalizes an unknown caught error into a display string: the `Error`'s
42
+ * `message` when it is one, else its `String(...)` coercion. Shared by the
43
+ * remote store's marker PUT and the session activation error path.
44
+ *
45
+ * @param err {unknown}
46
+ * @returns {string}
47
+ */
48
+ export declare function errorMessage(err: unknown): string;
26
49
  /**
27
50
  * Builds a `WasSyncPort` bound to a single Space + Collection on the remote WAS
28
51
  * server, backed by the session's signed `WasClient`.
@@ -34,12 +57,12 @@ import { type WasSyncPort } from './types.js';
34
57
  * @param [options.capability] {IZcap} the delegated session capability for
35
58
  * this collection (a restored `delegated` tier session); absent in the full
36
59
  * tier, where requests invoke root capabilities
37
- * @returns {WasSyncPort}
60
+ * @returns {WasSyncBasePort}
38
61
  */
39
62
  export declare function createWasSyncPort({ was, spaceId, collectionId, capability }: {
40
63
  was: WasClient;
41
64
  spaceId: string;
42
65
  collectionId: string;
43
66
  capability?: IZcap;
44
- }): WasSyncPort;
67
+ }): WasSyncBasePort;
45
68
  //# sourceMappingURL=wasSyncPort.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wasSyncPort.d.ts","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AACzD,OAAO,EAKL,KAAK,WAAW,EAEjB,MAAM,YAAY,CAAA;AAgCnB;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,GAAG,EACH,OAAO,EACP,YAAY,EACZ,UAAU,EACX,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,KAAK,CAAA;CACnB,GAAG,WAAW,CA0Jd"}
1
+ {"version":3,"file":"wasSyncPort.d.ts","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAA0B,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AACzD,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAK5D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEjD;AAuCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,GAAG,EACH,OAAO,EACP,YAAY,EACZ,UAAU,EACX,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,KAAK,CAAA;CACnB,GAAG,eAAe,CAsFlB"}
@@ -1,25 +1,85 @@
1
- import { WasSyncConflictError } from './types.js';
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
4
+ /**
5
+ * The app-side seam between the generic sync adapter and `@interop/was-client`.
6
+ * Implements the `WasSyncBasePort` interface (query + conditional writes) for one
7
+ * remote WAS Collection using the raw, signed `was.request()` escape hatch. The
8
+ * 412 conflict re-read (`get`) is supplied separately by `withFeedMasterRead`,
9
+ * which resolves the master state from the changes-feed body -- origin-
10
+ * independent, unlike a cross-origin `GET` ETag header.
11
+ *
12
+ * Using `request()` (rather than the `Resource` / `Collection` handles) is
13
+ * deliberate: it moves the stored body VERBATIM, bypassing the encryption codec.
14
+ * The `changes` feed already ships opaque stored bodies (plaintext for a
15
+ * plaintext collection, the EDV envelope for an encrypted one), and the push
16
+ * side must write those same bytes back unchanged -- running them through
17
+ * `resource.put()` would re-encrypt an already-encrypted envelope. Encrypt /
18
+ * decrypt stays a read/write-time concern above this layer; this port is
19
+ * collection-agnostic and never touches keys.
20
+ *
21
+ * Conditional writes ride the server's monotonic `version` (content) and
22
+ * `metaVersion` (metadata) ETags, which the reference server enforces uniformly
23
+ * for plaintext and encrypted resources alike -- so there is no plaintext-vs-
24
+ * encrypted fork here.
25
+ */
26
+ import { readEtag, writeHeaders } from '@interop/was-client';
27
+ import { WasSyncAuthError, WasSyncConflictError } from './types.js';
2
28
  /**
3
29
  * Extracts an HTTP status from a raw ky/ezcap error. `was.request()` rejects on
4
30
  * any non-2xx with `err.status` set (see `@interop/http-client`'s error
5
- * normaliser); this reads it defensively from either location.
31
+ * normaliser); this reads it defensively from either location. Shared with the
32
+ * remote store's marker PUT and the dev-grant provisioner, which need the raw
33
+ * status for diagnostics rather than the mapped sync error.
6
34
  *
7
35
  * @param err {unknown}
8
36
  * @returns {number | undefined}
9
37
  */
10
- function errorStatus(err) {
38
+ export function errorStatus(err) {
11
39
  return (err.status ??
12
40
  err.response?.status);
13
41
  }
14
42
  /**
15
- * Parses a quoted strong ETag (`"3"`) into its numeric revision, or `undefined`
16
- * when the header is absent (the resource has no such revision yet).
43
+ * Normalizes an unknown caught error into a display string: the `Error`'s
44
+ * `message` when it is one, else its `String(...)` coercion. Shared by the
45
+ * remote store's marker PUT and the session activation error path.
46
+ *
47
+ * @param err {unknown}
48
+ * @returns {string}
49
+ */
50
+ export function errorMessage(err) {
51
+ return err instanceof Error ? err.message : String(err);
52
+ }
53
+ /**
54
+ * Maps a raw `was.request()` rejection to the sync layer's typed errors: `412`
55
+ * to {@link WasSyncConflictError} (a lost-update conflict) and `401` / `403` to
56
+ * {@link WasSyncAuthError} (expired/revoked storage access). Every other error
57
+ * is returned unchanged so RxDB's retry/backoff handles it. Returns the error to
58
+ * throw (the caller re-throws) rather than throwing itself.
59
+ *
60
+ * @param err {unknown}
61
+ * @returns {unknown}
62
+ */
63
+ function toPortError(err) {
64
+ const status = errorStatus(err);
65
+ if (status === 412) {
66
+ return new WasSyncConflictError();
67
+ }
68
+ if (status === 401 || status === 403) {
69
+ return new WasSyncAuthError(status);
70
+ }
71
+ return err;
72
+ }
73
+ /**
74
+ * Parses a quoted strong ETag (`"3"`, as read by `readEtag`) into its numeric
75
+ * revision, or `undefined` when the header was absent (the resource has no such
76
+ * revision yet).
17
77
  *
18
- * @param etag {string | null}
78
+ * @param etag {string | undefined}
19
79
  * @returns {number | undefined}
20
80
  */
21
81
  function parseEtag(etag) {
22
- if (!etag) {
82
+ if (etag === undefined) {
23
83
  return undefined;
24
84
  }
25
85
  const revision = Number(etag.replace(/"/g, ''));
@@ -36,134 +96,78 @@ function parseEtag(etag) {
36
96
  * @param [options.capability] {IZcap} the delegated session capability for
37
97
  * this collection (a restored `delegated` tier session); absent in the full
38
98
  * tier, where requests invoke root capabilities
39
- * @returns {WasSyncPort}
99
+ * @returns {WasSyncBasePort}
40
100
  */
41
101
  export function createWasSyncPort({ was, spaceId, collectionId, capability }) {
42
102
  const collectionPath = `/space/${spaceId}/${collectionId}`;
43
103
  const resourcePath = (id) => `${collectionPath}/${encodeURIComponent(id)}`;
44
- /**
45
- * Builds the conditional-write headers from the port's precondition options.
46
- */
47
- const writeHeaders = ({ ifMatch, ifNoneMatch }) => {
48
- const headers = {};
49
- if (ifMatch !== undefined) {
50
- headers['if-match'] = ifMatch;
51
- }
52
- if (ifNoneMatch) {
53
- headers['if-none-match'] = '*';
54
- }
55
- return Object.keys(headers).length > 0 ? headers : undefined;
56
- };
57
104
  /**
58
105
  * Runs a conditional write, mapping the server's `412 precondition-failed`
59
- * into the core's `WasSyncConflictError` and letting all else propagate.
106
+ * into the core's `WasSyncConflictError` and a `401` / `403` into
107
+ * `WasSyncAuthError` (see {@link toPortError}), and letting all else propagate.
108
+ * Returns the accepted write's new revision parsed from the response ETag
109
+ * (`version` for content writes, `metaVersion` for `/meta` writes), or
110
+ * `undefined` when the response carries no ETag.
60
111
  */
61
- const conditionalWrite = async (run) => {
112
+ const conditionalWrite = async (
113
+ // Typed off readEtag's parameter (was-client's HttpResponse) so the type
114
+ // needn't be imported from @interop/http-client, which is not a direct
115
+ // dependency here.
116
+ run) => {
62
117
  try {
63
- await run();
118
+ const response = await run();
119
+ return parseEtag(readEtag(response));
64
120
  }
65
121
  catch (err) {
66
- if (errorStatus(err) === 412) {
67
- throw new WasSyncConflictError();
68
- }
69
- throw err;
122
+ throw toPortError(err);
70
123
  }
71
124
  };
72
125
  return {
73
126
  async query({ checkpoint, limit }) {
74
- const response = await was.request({
75
- capability,
76
- path: `${collectionPath}/query`,
77
- method: 'POST',
78
- json: {
79
- profile: 'changes',
80
- ...(checkpoint !== undefined && { checkpoint }),
81
- limit
82
- }
83
- });
84
- return response.data;
127
+ try {
128
+ const response = await was.request({
129
+ capability,
130
+ path: `${collectionPath}/query`,
131
+ method: 'POST',
132
+ json: {
133
+ profile: 'changes',
134
+ ...(checkpoint !== undefined && { checkpoint }),
135
+ limit
136
+ }
137
+ });
138
+ return response.data;
139
+ }
140
+ catch (err) {
141
+ // Map a pull-path 401/403 to WasSyncAuthError too, so expired access is
142
+ // recognised whether it surfaces on the pull or the push side.
143
+ throw toPortError(err);
144
+ }
85
145
  },
86
146
  async putContent({ id, data, ifMatch, ifNoneMatch }) {
87
- await conditionalWrite(() => was.request({
147
+ return conditionalWrite(() => was.request({
88
148
  capability,
89
149
  path: resourcePath(id),
90
150
  method: 'PUT',
91
151
  json: data,
92
- headers: writeHeaders({ ifMatch, ifNoneMatch })
152
+ headers: writeHeaders({ precondition: { ifMatch, ifNoneMatch } })
93
153
  }));
94
154
  },
95
155
  async deleteContent({ id, ifMatch }) {
96
- await conditionalWrite(() => was.request({
156
+ return conditionalWrite(() => was.request({
97
157
  capability,
98
158
  path: resourcePath(id),
99
159
  method: 'DELETE',
100
- headers: writeHeaders({ ifMatch })
160
+ headers: writeHeaders({ precondition: { ifMatch } })
101
161
  }));
102
162
  },
103
163
  async putMeta({ id, custom, ifMatch, ifNoneMatch }) {
104
- await conditionalWrite(() => was.request({
164
+ return conditionalWrite(() => was.request({
105
165
  capability,
106
166
  path: `${resourcePath(id)}/meta`,
107
167
  method: 'PUT',
108
168
  json: { custom },
109
- headers: writeHeaders({ ifMatch, ifNoneMatch })
169
+ headers: writeHeaders({ precondition: { ifMatch, ifNoneMatch } })
110
170
  }));
111
- },
112
- async get({ id }) {
113
- // Content re-read: raw GET returns the stored body verbatim (no decrypt)
114
- // and the content `version` in the ETag. A 404 means the resource is gone
115
- // (or tombstoned) -- report absent so the core synthesizes a tombstone.
116
- let contentResponse;
117
- try {
118
- contentResponse = await was.request({
119
- capability,
120
- path: resourcePath(id),
121
- method: 'GET'
122
- });
123
- }
124
- catch (err) {
125
- if (errorStatus(err) === 404) {
126
- return null;
127
- }
128
- throw err;
129
- }
130
- const master = {
131
- version: parseEtag(contentResponse.headers.get('etag')) ?? 0,
132
- // Filled from the `/meta` body's server-managed `updatedAt` below; the
133
- // change feed remains the authority on ordering, so this only feeds the
134
- // one-off conflict entry and is corrected on the next pull.
135
- updatedAt: '',
136
- deleted: false,
137
- data: contentResponse.data
138
- };
139
- // Metadata re-read: the `/meta` body carries the server-managed
140
- // `updatedAt` plus the user-writable `custom` (opaque), and its own
141
- // `metaVersion` ETag (absent until metadata has been written).
142
- try {
143
- const metaResponse = await was.request({
144
- capability,
145
- path: `${resourcePath(id)}/meta`,
146
- method: 'GET'
147
- });
148
- const metaBody = metaResponse.data;
149
- if (metaBody?.updatedAt) {
150
- master.updatedAt = metaBody.updatedAt;
151
- }
152
- const metaVersion = parseEtag(metaResponse.headers.get('etag'));
153
- if (metaVersion !== undefined) {
154
- master.metaVersion = metaVersion;
155
- }
156
- if (metaBody?.custom !== undefined) {
157
- master.custom = metaBody.custom;
158
- }
159
- }
160
- catch (err) {
161
- // Metadata is optional; only a hard error (not 404) should propagate.
162
- if (errorStatus(err) !== 404) {
163
- throw err;
164
- }
165
- }
166
- return master;
167
171
  }
168
172
  };
169
173
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wasSyncPort.js","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wasSyncPort.js","sourceRoot":"","sources":["../../src/sync/wasSyncPort.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@interop/was-react",
3
3
  "description": "React library for building 'Bring Your Own Everything' (BYOE) apps on Wallet Attached Storage: DID Auth login via CHAPI wallet, local-first encrypted storage, and background sync to a WAS server.",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "pnpm run clear && tsc",
@@ -55,9 +55,10 @@
55
55
  "@interop/security-document-loader": "^9.4.1",
56
56
  "@interop/vc": "^11.0.5",
57
57
  "@interop/verifier-core": "^3.3.1",
58
- "@interop/was-client": "^0.13.3",
58
+ "@interop/was-client": "^0.14.5",
59
59
  "@interop/webkms-client": "^14.7.1",
60
- "@interop/x25519-key-agreement-key": "^5.1.2",
60
+ "@interop/x25519-key-agreement-key": "^5.2.0",
61
+ "@scure/base": "^2.2.0",
61
62
  "credential-handler-polyfill": "^4.1.0",
62
63
  "uuidv7": "^1.2.1"
63
64
  },
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=verifyResponse.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"verifyResponse.test.d.ts","sourceRoot":"","sources":["../../src/auth/verifyResponse.test.ts"],"names":[],"mappings":""}
@@ -1,282 +0,0 @@
1
- /*!
2
- * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
- */
4
- /**
5
- * verifyResponse tests. A mock "wallet" (its own did:key identity, distinct
6
- * from the app's) signs presentations exactly the way freewallet does --
7
- * Ed25519Signature2020 DIDAuth proof over a VP with the app-key VC embedded
8
- * and/or a `zcap` grant array (bare term added to the context) -- and the
9
- * RP-side checks are exercised against good and crafted-bad inputs.
10
- */
11
- import { beforeAll, describe, expect, it } from 'vitest';
12
- import * as vc from '@interop/vc';
13
- import { Ed25519Signature2020 } from '@interop/ed25519-signature';
14
- import { CapabilityAgent } from '@interop/webkms-client';
15
- import { issueSeedCredential } from '../identity/seedCredential.js';
16
- import { createDocumentLoader } from '../identity/documentLoader.js';
17
- import { deriveIdentity } from '../identity/agents.js';
18
- import { checkGrants, grantsOf, verifyLoginPresentation } from './verifyResponse.js';
19
- const ORIGIN = 'http://localhost:5173';
20
- const SERVER_URL = 'http://localhost:3999';
21
- const SPACE_URL = `${SERVER_URL}/space/e2e-space`;
22
- const CONFIG = {
23
- credentialType: 'TestAppKey',
24
- vocabBase: 'urn:test-app:vocab#'
25
- };
26
- const TEST_COLLECTIONS = [
27
- 'action-items',
28
- 'projects',
29
- 'goals',
30
- 'questions',
31
- 'answers',
32
- 'web-links',
33
- 'thoughts',
34
- 'current-focus'
35
- ];
36
- const documentLoader = createDocumentLoader();
37
- const ZCAP_TERM_CONTEXT = {
38
- '@protected': true,
39
- zcap: { '@id': 'urn:freewallet:vocab#zcap', '@container': '@set' }
40
- };
41
- let wallet;
42
- let appDid;
43
- let appSeed;
44
- beforeAll(async () => {
45
- const agent = await CapabilityAgent.fromSeed({
46
- seed: crypto.getRandomValues(new Uint8Array(32)),
47
- handle: 'mock-wallet',
48
- keyName: 'wallet-key'
49
- });
50
- wallet = {
51
- holder: agent.id,
52
- suite: new Ed25519Signature2020({ signer: agent.getSigner() })
53
- };
54
- appSeed = crypto.getRandomValues(new Uint8Array(32));
55
- appDid = (await deriveIdentity({ seed: appSeed })).controllerDid;
56
- });
57
- /** An UNSIGNED structural grant (checkGrants is structural, not cryptographic). */
58
- function grantFor({ collectionId, controller = appDid, expires = new Date(Date.now() + 86_400_000).toISOString(), actions = ['GET', 'HEAD', 'PUT', 'POST', 'DELETE'], spaceUrl = SPACE_URL }) {
59
- return {
60
- '@context': 'https://w3id.org/zcap/v1',
61
- id: `urn:zcap:${crypto.randomUUID()}`,
62
- controller,
63
- parentCapability: `urn:zcap:root:${encodeURIComponent(spaceUrl)}`,
64
- invocationTarget: collectionId ? `${spaceUrl}/${collectionId}` : spaceUrl,
65
- allowedAction: actions,
66
- expires
67
- };
68
- }
69
- /** The full wallet-shaped grant set: RW collection grants + space read. */
70
- function fullGrantSet() {
71
- const grants = TEST_COLLECTIONS.map(id => grantFor({ collectionId: id }));
72
- grants.push(grantFor({ actions: ['GET', 'HEAD'] }));
73
- return grants;
74
- }
75
- /** Signs a wallet-style VP with optional embedded VC and zcap array. */
76
- async function walletVp({ challenge, domain = ORIGIN, credential, zcaps }) {
77
- const presentation = vc.createPresentation({
78
- holder: wallet.holder,
79
- ...(credential && { verifiableCredential: [credential] }),
80
- verify: false,
81
- version: 1.0
82
- });
83
- if (zcaps && zcaps.length > 0) {
84
- const base = presentation['@context'];
85
- presentation['@context'] = [
86
- ...(Array.isArray(base) ? base : [base]),
87
- ZCAP_TERM_CONTEXT
88
- ];
89
- presentation.zcap = zcaps;
90
- }
91
- return (await vc.signPresentation({
92
- presentation: presentation,
93
- challenge,
94
- domain,
95
- documentLoader,
96
- suite: wallet.suite
97
- }));
98
- }
99
- describe('verifyLoginPresentation', () => {
100
- it('accepts a wallet-signed VP with an embedded app key and grants', async () => {
101
- const challenge = crypto.randomUUID();
102
- const credential = await issueSeedCredential({
103
- seed: appSeed,
104
- origin: ORIGIN,
105
- config: CONFIG,
106
- documentLoader
107
- });
108
- const presentation = await walletVp({
109
- challenge,
110
- credential,
111
- zcaps: fullGrantSet()
112
- });
113
- await expect(verifyLoginPresentation({
114
- presentation,
115
- challenge,
116
- domain: ORIGIN,
117
- documentLoader
118
- })).resolves.toBeUndefined();
119
- expect(grantsOf(presentation)).toHaveLength(TEST_COLLECTIONS.length + 1);
120
- });
121
- it('rejects a challenge mismatch', async () => {
122
- const presentation = await walletVp({ challenge: 'sent-nonce' });
123
- await expect(verifyLoginPresentation({
124
- presentation,
125
- challenge: 'other-nonce',
126
- domain: ORIGIN,
127
- documentLoader
128
- })).rejects.toThrow();
129
- });
130
- it('rejects a domain mismatch', async () => {
131
- const challenge = crypto.randomUUID();
132
- const presentation = await walletVp({
133
- challenge,
134
- domain: 'https://evil.example'
135
- });
136
- await expect(verifyLoginPresentation({
137
- presentation,
138
- challenge,
139
- domain: ORIGIN,
140
- documentLoader
141
- })).rejects.toThrow(/domain/);
142
- });
143
- it('rejects a tampered presentation', async () => {
144
- const challenge = crypto.randomUUID();
145
- const presentation = await walletVp({
146
- challenge,
147
- zcaps: fullGrantSet()
148
- });
149
- const tampered = {
150
- ...presentation,
151
- zcap: [
152
- ...presentation.zcap.slice(1),
153
- grantFor({
154
- collectionId: 'injected',
155
- controller: 'did:example:mallory'
156
- })
157
- ]
158
- };
159
- await expect(verifyLoginPresentation({
160
- presentation: tampered,
161
- challenge,
162
- domain: ORIGIN,
163
- documentLoader
164
- })).rejects.toThrow();
165
- });
166
- it('rejects an unsigned presentation', async () => {
167
- const presentation = {
168
- '@context': ['https://www.w3.org/2018/credentials/v1'],
169
- type: ['VerifiablePresentation']
170
- };
171
- await expect(verifyLoginPresentation({
172
- presentation,
173
- challenge: 'x',
174
- domain: ORIGIN,
175
- documentLoader
176
- })).rejects.toThrow();
177
- });
178
- it('rejects a tampered embedded credential', async () => {
179
- const challenge = crypto.randomUUID();
180
- const credential = await issueSeedCredential({
181
- seed: appSeed,
182
- origin: ORIGIN,
183
- config: CONFIG,
184
- documentLoader
185
- });
186
- const subject = credential.credentialSubject;
187
- const tamperedVc = {
188
- ...credential,
189
- credentialSubject: { ...subject, origin: 'https://evil.example' }
190
- };
191
- const presentation = await walletVp({ challenge });
192
- presentation.verifiableCredential =
193
- [tamperedVc];
194
- await expect(verifyLoginPresentation({
195
- presentation,
196
- challenge,
197
- domain: ORIGIN,
198
- documentLoader
199
- })).rejects.toThrow();
200
- });
201
- });
202
- describe('checkGrants', () => {
203
- it('accepts the full wallet grant set and reports topology + expiry', () => {
204
- const soon = new Date(Date.now() + 3_600_000).toISOString();
205
- const later = new Date(Date.now() + 86_400_000).toISOString();
206
- const grants = TEST_COLLECTIONS.map((id, index) => grantFor({ collectionId: id, expires: index === 0 ? soon : later }));
207
- grants.push(grantFor({ actions: ['GET', 'HEAD'], expires: later }));
208
- const checked = checkGrants({
209
- grants,
210
- controllerDid: appDid,
211
- collections: TEST_COLLECTIONS,
212
- expectedServerUrl: SERVER_URL
213
- });
214
- expect(checked.parsed.serverUrl).toBe(SERVER_URL);
215
- expect(checked.parsed.spaceId).toBe('e2e-space');
216
- expect(Object.keys(checked.parsed.byCollectionId)).toHaveLength(TEST_COLLECTIONS.length);
217
- expect(checked.expires).toBe(soon);
218
- });
219
- it('rejects an empty grant set', () => {
220
- expect(() => checkGrants({
221
- grants: [],
222
- controllerDid: appDid,
223
- collections: TEST_COLLECTIONS
224
- })).toThrow(/no storage grants/);
225
- });
226
- it('rejects a grant controlled by another DID', () => {
227
- const grants = fullGrantSet();
228
- grants[0].controller = 'did:example:mallory';
229
- expect(() => checkGrants({
230
- grants,
231
- controllerDid: appDid,
232
- collections: TEST_COLLECTIONS
233
- })).toThrow(/controlled by/);
234
- });
235
- it('rejects an expired grant', () => {
236
- const grants = fullGrantSet();
237
- grants[2].expires = new Date(Date.now() - 1000).toISOString();
238
- expect(() => checkGrants({
239
- grants,
240
- controllerDid: appDid,
241
- collections: TEST_COLLECTIONS
242
- })).toThrow(/expired/);
243
- });
244
- it('rejects a grant set on the wrong server', () => {
245
- expect(() => checkGrants({
246
- grants: fullGrantSet(),
247
- controllerDid: appDid,
248
- collections: TEST_COLLECTIONS,
249
- expectedServerUrl: 'http://localhost:4000'
250
- })).toThrow(/expected/);
251
- });
252
- it('rejects a grant set spanning two spaces', () => {
253
- const grants = fullGrantSet();
254
- grants.push(grantFor({
255
- collectionId: 'projects',
256
- spaceUrl: `${SERVER_URL}/space/other-space`
257
- }));
258
- expect(() => checkGrants({
259
- grants,
260
- controllerDid: appDid,
261
- collections: TEST_COLLECTIONS
262
- })).toThrow(/two spaces/);
263
- });
264
- it('rejects a set missing a collection', () => {
265
- const grants = TEST_COLLECTIONS.slice(1).map(id => grantFor({ collectionId: id }));
266
- expect(() => checkGrants({
267
- grants,
268
- controllerDid: appDid,
269
- collections: TEST_COLLECTIONS
270
- })).toThrow(new RegExp(`No grant covers the "${TEST_COLLECTIONS[0]}"`));
271
- });
272
- it('rejects a collection grant with insufficient actions', () => {
273
- const grants = fullGrantSet();
274
- grants[0].allowedAction = ['GET', 'HEAD'];
275
- expect(() => checkGrants({
276
- grants,
277
- controllerDid: appDid,
278
- collections: TEST_COLLECTIONS
279
- })).toThrow(/lacks required actions/);
280
- });
281
- });
282
- //# sourceMappingURL=verifyResponse.test.js.map