@peers-app/peers-sdk 0.26.5 → 0.27.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 (194) hide show
  1. package/README.md +3 -1
  2. package/bin/peers-package.js +13 -0
  3. package/define/package.json +4 -0
  4. package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
  5. package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/also-implements.test.js +110 -0
  7. package/dist/contracts/__tests__/builder.test.js +45 -0
  8. package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
  9. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
  10. package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
  11. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
  12. package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
  14. package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
  15. package/dist/contracts/__tests__/registry.test.js +68 -0
  16. package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
  17. package/dist/contracts/__tests__/target-dispatch.js +24 -0
  18. package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
  19. package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
  20. package/dist/contracts/__tests__/validate.test.js +177 -7
  21. package/dist/contracts/__tests__/workflow-context.test.js +0 -11
  22. package/dist/contracts/builder.d.ts +16 -2
  23. package/dist/contracts/builder.js +44 -3
  24. package/dist/contracts/contract-definition-schema.d.ts +251 -0
  25. package/dist/contracts/contract-definition-schema.js +48 -0
  26. package/dist/contracts/contract-provider-router.d.ts +2 -0
  27. package/dist/contracts/contract-provider-router.js +5 -2
  28. package/dist/contracts/contract-proxy.d.ts +52 -18
  29. package/dist/contracts/contract-proxy.js +144 -203
  30. package/dist/contracts/contract-resolvers.d.ts +6 -4
  31. package/dist/contracts/contract-resolvers.js +32 -9
  32. package/dist/contracts/frozen-contracts.d.ts +59 -0
  33. package/dist/contracts/frozen-contracts.js +86 -0
  34. package/dist/contracts/host-contract-client.d.ts +134 -0
  35. package/dist/contracts/host-contract-client.js +396 -0
  36. package/dist/contracts/host-contract-transport.d.ts +2 -0
  37. package/dist/contracts/host-contract-transport.js +8 -0
  38. package/dist/contracts/index.d.ts +6 -4
  39. package/dist/contracts/index.js +11 -5
  40. package/dist/contracts/registry.d.ts +31 -1
  41. package/dist/contracts/registry.js +100 -6
  42. package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
  43. package/dist/contracts/system/contract-catalog-contract.js +17 -5
  44. package/dist/contracts/tool-catalog-service.js +26 -6
  45. package/dist/contracts/tool-catalog.d.ts +1 -1
  46. package/dist/contracts/tool-catalog.js +4 -1
  47. package/dist/contracts/types.d.ts +32 -2
  48. package/dist/contracts/validate.d.ts +16 -1
  49. package/dist/contracts/validate.js +139 -25
  50. package/dist/data/change-tracking.d.ts +17 -5
  51. package/dist/data/change-tracking.js +22 -6
  52. package/dist/data/change-tracking.test.d.ts +1 -0
  53. package/dist/data/change-tracking.test.js +62 -0
  54. package/dist/data/group-members.d.ts +6 -1
  55. package/dist/data/group-members.js +12 -4
  56. package/dist/data/groups.d.ts +6 -1
  57. package/dist/data/groups.js +12 -4
  58. package/dist/data/orm/sql.data-source.d.ts +9 -0
  59. package/dist/data/orm/sql.data-source.js +47 -2
  60. package/dist/data/orm/sql.data-source.test.js +142 -0
  61. package/dist/data/orm/table-container.js +9 -9
  62. package/dist/data/orm/table-container.test.js +44 -0
  63. package/dist/data/orm/types.d.ts +41 -1
  64. package/dist/data/orm/types.js +159 -28
  65. package/dist/data/orm/types.test.js +129 -0
  66. package/dist/data/package-versions.d.ts +5 -1
  67. package/dist/data/package-versions.js +11 -4
  68. package/dist/data/packages.d.ts +7 -1
  69. package/dist/data/packages.js +13 -4
  70. package/dist/data/peer-types.d.ts +8 -62
  71. package/dist/data/persistent-vars.d.ts +4 -1
  72. package/dist/data/persistent-vars.js +7 -2
  73. package/dist/data/table-signing.test.d.ts +1 -0
  74. package/dist/data/table-signing.test.js +118 -0
  75. package/dist/data/tools.d.ts +29 -221
  76. package/dist/data/users.d.ts +1 -2
  77. package/dist/data/users.js +8 -5
  78. package/dist/define/also-implements.test.d.ts +1 -0
  79. package/dist/define/also-implements.test.js +123 -0
  80. package/dist/define/context.d.ts +77 -0
  81. package/dist/define/context.js +2 -0
  82. package/dist/define/contract-ref.d.ts +177 -0
  83. package/dist/define/contract-ref.js +67 -0
  84. package/dist/define/declare-contract.d.ts +167 -0
  85. package/dist/define/declare-contract.js +220 -0
  86. package/dist/define/declare-contract.test.d.ts +1 -0
  87. package/dist/define/declare-contract.test.js +573 -0
  88. package/dist/define/define-package.d.ts +203 -0
  89. package/dist/define/define-package.js +289 -0
  90. package/dist/define/define.test.d.ts +1 -0
  91. package/dist/define/define.test.js +1010 -0
  92. package/dist/define/empty-fields.d.ts +15 -0
  93. package/dist/define/empty-fields.js +134 -0
  94. package/dist/define/empty-fields.test.d.ts +1 -0
  95. package/dist/define/empty-fields.test.js +55 -0
  96. package/dist/define/events-observables.test.d.ts +1 -0
  97. package/dist/define/events-observables.test.js +1013 -0
  98. package/dist/define/events.d.ts +81 -0
  99. package/dist/define/events.js +81 -0
  100. package/dist/define/generated-field.d.ts +20 -0
  101. package/dist/define/generated-field.js +25 -0
  102. package/dist/define/index.d.ts +28 -0
  103. package/dist/define/index.js +62 -0
  104. package/dist/define/lazy-members.d.ts +8 -0
  105. package/dist/define/lazy-members.js +38 -0
  106. package/dist/define/manifest.d.ts +63 -0
  107. package/dist/define/manifest.js +653 -0
  108. package/dist/define/observables.d.ts +146 -0
  109. package/dist/define/observables.js +187 -0
  110. package/dist/define/serve-members.d.ts +21 -0
  111. package/dist/define/serve-members.js +152 -0
  112. package/dist/define/serve-members.test.d.ts +1 -0
  113. package/dist/define/serve-members.test.js +630 -0
  114. package/dist/define/serve.d.ts +22 -0
  115. package/dist/define/serve.js +58 -0
  116. package/dist/define/system-contracts.d.ts +41 -0
  117. package/dist/define/system-contracts.js +21 -0
  118. package/dist/define/table-handle.d.ts +13 -0
  119. package/dist/define/table-handle.js +36 -0
  120. package/dist/define/table.d.ts +128 -0
  121. package/dist/define/table.js +122 -0
  122. package/dist/define/tool.d.ts +129 -0
  123. package/dist/define/tool.js +91 -0
  124. package/dist/define/vars.d.ts +82 -0
  125. package/dist/define/vars.js +256 -0
  126. package/dist/define/vars.test.d.ts +1 -0
  127. package/dist/define/vars.test.js +39 -0
  128. package/dist/define/wire-value.d.ts +12 -0
  129. package/dist/define/wire-value.js +76 -0
  130. package/dist/define/worker-state.d.ts +15 -0
  131. package/dist/define/worker-state.js +45 -0
  132. package/dist/device/tx-encoding.test.js +10 -0
  133. package/dist/index.d.ts +3 -0
  134. package/dist/index.js +5 -1
  135. package/dist/observable.d.ts +42 -2
  136. package/dist/observable.js +25 -7
  137. package/dist/package-build/build-package.d.ts +60 -0
  138. package/dist/package-build/build-package.js +312 -0
  139. package/dist/package-build/build-package.test.d.ts +1 -0
  140. package/dist/package-build/build-package.test.js +204 -0
  141. package/dist/package-build/cli.d.ts +7 -0
  142. package/dist/package-build/cli.js +218 -0
  143. package/dist/package-build/contract-add.d.ts +35 -0
  144. package/dist/package-build/contract-add.js +97 -0
  145. package/dist/package-build/contract-add.test.d.ts +1 -0
  146. package/dist/package-build/contract-add.test.js +467 -0
  147. package/dist/package-build/contract-source.d.ts +59 -0
  148. package/dist/package-build/contract-source.js +471 -0
  149. package/dist/package-build/freeze-contracts.d.ts +56 -0
  150. package/dist/package-build/freeze-contracts.js +198 -0
  151. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  152. package/dist/package-build/freeze-contracts.test.js +355 -0
  153. package/dist/package-installer/package-creator.d.ts +4 -0
  154. package/dist/package-installer/package-creator.js +57 -16
  155. package/dist/package-installer/package-creator.test.js +86 -0
  156. package/dist/package-loader/contract-members.d.ts +53 -0
  157. package/dist/package-loader/contract-members.js +31 -0
  158. package/dist/package-loader/contract-package-loader.js +5 -1
  159. package/dist/package-loader/contract-package-loader.test.js +11 -0
  160. package/dist/package-loader/host-authority-loading.test.js +9 -5
  161. package/dist/package-loader/index.d.ts +1 -0
  162. package/dist/package-loader/index.js +1 -0
  163. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  164. package/dist/package-loader/isolated-artifact.js +257 -51
  165. package/dist/package-loader/isolated-artifact.test.js +294 -21
  166. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  167. package/dist/package-loader/isolated-table-gateway.js +30 -11
  168. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  169. package/dist/package-loader/package-loader.d.ts +8 -1
  170. package/dist/package-loader/package-loader.js +39 -20
  171. package/dist/package-loader/package-loader.test.js +47 -0
  172. package/dist/peer-id.d.ts +25 -0
  173. package/dist/peer-id.js +99 -0
  174. package/dist/serial-json.compat.test.d.ts +1 -0
  175. package/dist/serial-json.compat.test.js +369 -0
  176. package/dist/serial-json.d.ts +41 -5
  177. package/dist/serial-json.js +216 -157
  178. package/dist/serial-json.oracle.d.ts +21 -0
  179. package/dist/serial-json.oracle.js +222 -0
  180. package/dist/serial-json.test.js +315 -0
  181. package/dist/testing/index.d.ts +4 -0
  182. package/dist/testing/index.js +8 -0
  183. package/dist/testing/mock-host-contract.d.ts +123 -0
  184. package/dist/testing/mock-host-contract.js +434 -0
  185. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  186. package/dist/testing/mock-host-contract.test.js +782 -0
  187. package/dist/types/field-type.d.ts +35 -18
  188. package/dist/types/field-type.js +4 -1
  189. package/dist/types/zod-types.d.ts +9 -0
  190. package/dist/types/zod-types.js +8 -2
  191. package/dist/utils.d.ts +1 -25
  192. package/dist/utils.js +10 -90
  193. package/package.json +9 -2
  194. package/testing/package.json +4 -0
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const SQLiteDB = require("better-sqlite3");
4
+ const event_registry_1 = require("../data/orm/event-registry");
4
5
  const sql_data_source_1 = require("../data/orm/sql.data-source");
6
+ const table_1 = require("../data/orm/table");
5
7
  const table_container_1 = require("../data/orm/table-container");
6
8
  const package_table_identity_1 = require("../data/package-table-identity");
7
9
  const table_definitions_table_1 = require("../data/table-definitions-table");
@@ -45,6 +47,9 @@ describe("isolated package table gateway", () => {
45
47
  stateId: recordId,
46
48
  count: 1,
47
49
  });
50
+ await expect(gateway.call(declaration, "count", [{ count: { $in: [1, 3] } }])).resolves.toBe(1);
51
+ await expect(gateway.call(declaration, "count", [{ count: { $in: [2] } }])).resolves.toBe(0);
52
+ await expect(gateway.call(declaration, "list", [{ count: { $gt: 0 } }])).rejects.toThrow("equality and $in filters only");
48
53
  await gateway.register(declaration);
49
54
  await expect(gateway.get(declaration, recordId)).resolves.toEqual({
50
55
  stateId: recordId,
@@ -73,6 +78,28 @@ describe("isolated package table gateway", () => {
73
78
  });
74
79
  await expect(gateway.get({ ...owner, packageId: (0, utils_1.newid)() }, (0, utils_1.newid)())).rejects.toThrow(/Undeclared isolated table/);
75
80
  });
81
+ test("unsupported filters fail even on empty tables or after a nonmatching condition", async () => {
82
+ const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
83
+ const declaration = makeDeclaration();
84
+ await gateway.register(declaration);
85
+ for (const operation of ["list", "count", "findOne"]) {
86
+ await expect(gateway.call(declaration, operation, [{ count: { $gt: 0 } }])).rejects.toThrow("equality and $in filters only");
87
+ }
88
+ await gateway.save(declaration, { stateId: (0, utils_1.newid)(), count: 1 });
89
+ await expect(gateway.call(declaration, "list", [{ count: 2, stateId: { $ne: "missing" } }])).rejects.toThrow("equality and $in filters only");
90
+ });
91
+ test("memory table pages use the ORM's one-based page numbers", async () => {
92
+ const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
93
+ const declaration = makeDeclaration();
94
+ await gateway.register(declaration);
95
+ const rows = [1, 2, 3].map((count) => ({ stateId: (0, utils_1.newid)(), count }));
96
+ for (const row of rows)
97
+ await gateway.save(declaration, row);
98
+ for (const page of [undefined, 0, 1]) {
99
+ await expect(gateway.call(declaration, "list", [{}, { page, pageSize: 2 }])).resolves.toEqual(rows.slice(0, 2));
100
+ }
101
+ await expect(gateway.call(declaration, "list", [{}, { page: 2, pageSize: 2 }])).resolves.toEqual(rows.slice(2));
102
+ });
76
103
  test("memory gateway preserves Date and binary fields without retaining references", async () => {
77
104
  const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
78
105
  const declaration = makeDeclaration({
@@ -118,6 +145,146 @@ describe("isolated package table gateway", () => {
118
145
  await expect(gateway.register(v1)).rejects.toThrow(/schema downgrade/);
119
146
  await expect(gateway.register(makeDeclaration({ tableName: 'Unsafe"; DROP TABLE State; --' }))).rejects.toThrow(/only letters, numbers, and underscores/);
120
147
  });
148
+ test("a rebuild that adds descriptive IR keys, or keys a newer SDK knows, keeps the table version", async () => {
149
+ const declaration = makeDeclaration({
150
+ versionNumber: 1,
151
+ fields: [
152
+ { name: "stateId", type: field_type_1.FieldType.id },
153
+ { name: "mode", type: field_type_1.FieldType.string },
154
+ { name: "where", type: field_type_1.FieldType.object },
155
+ ],
156
+ });
157
+ const tableId = (0, package_table_identity_1.packageScopedTableId)(declaration.packageId, declaration.tableName);
158
+ const stored = {
159
+ tableId,
160
+ name: `${declaration.tableName}_${declaration.packageId}`,
161
+ metaData: {
162
+ tableId,
163
+ packageId: declaration.packageId,
164
+ name: declaration.tableName,
165
+ description: declaration.description,
166
+ primaryKeyName: declaration.primaryKeyName,
167
+ fields: declaration.fields,
168
+ versionNumber: 1,
169
+ },
170
+ versionNumber: 1,
171
+ };
172
+ const table = {
173
+ get: jest.fn(async (id) => (id === tableId ? stored : undefined)),
174
+ save: jest.fn(async (record) => record),
175
+ };
176
+ const registerTableDefinition = jest.fn();
177
+ const dataContext = {
178
+ tableContainer: { registerTableDefinition, getTable: jest.fn(() => table) },
179
+ };
180
+ const enriched = {
181
+ ...declaration,
182
+ fields: [
183
+ { name: "stateId", type: field_type_1.FieldType.id },
184
+ { name: "mode", type: field_type_1.FieldType.string, enumValues: ["on", "off"] },
185
+ {
186
+ name: "where",
187
+ type: field_type_1.FieldType.object,
188
+ fields: [{ name: "city", type: field_type_1.FieldType.string }],
189
+ future: true,
190
+ },
191
+ ],
192
+ };
193
+ await (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext).register(enriched);
194
+ // The stored definition stays until the table's version changes.
195
+ expect(table.save).not.toHaveBeenCalled();
196
+ expect(registerTableDefinition.mock.calls[0][0].metaData.fields).toEqual(declaration.fields);
197
+ // Every other change at the same version still needs a version bump.
198
+ await expect((0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext).register({
199
+ ...declaration,
200
+ fields: [
201
+ declaration.fields[0],
202
+ { name: "mode", type: field_type_1.FieldType.number },
203
+ declaration.fields[2],
204
+ ],
205
+ })).rejects.toThrow(/different schemas at version 1/);
206
+ });
207
+ test("switches a table between synced and local-only at a higher version, keeping its rows", async () => {
208
+ const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
209
+ const synced = makeDeclaration({ versionNumber: 1 });
210
+ const rowId = (0, utils_1.newid)();
211
+ await gateway.register(synced);
212
+ await gateway.save(synced, { stateId: rowId, count: 1 });
213
+ // Like any schema change, the flag needs a version bump.
214
+ await expect(gateway.register({ ...synced, localOnly: true })).rejects.toThrow(/different schemas at version 1/);
215
+ const local = { ...synced, versionNumber: 2, localOnly: true };
216
+ await gateway.register(local);
217
+ await expect(gateway.get(local, rowId)).resolves.toEqual({ stateId: rowId, count: 1 });
218
+ const syncedAgain = { ...synced, versionNumber: 3 };
219
+ await gateway.register(syncedAgain);
220
+ await expect(gateway.get(syncedAgain, rowId)).resolves.toEqual({ stateId: rowId, count: 1 });
221
+ });
222
+ test.each([
223
+ { localOnly: false },
224
+ { localOnly: true },
225
+ ])("switches a stored localOnly=$localOnly table only at a higher version, preflighting the batch", async ({ localOnly, }) => {
226
+ const declaration = makeDeclaration({ versionNumber: 1, localOnly });
227
+ const tableId = (0, package_table_identity_1.packageScopedTableId)(declaration.packageId, declaration.tableName);
228
+ const stored = {
229
+ tableId,
230
+ name: `${declaration.tableName}_${declaration.packageId}`,
231
+ metaData: {
232
+ tableId,
233
+ packageId: declaration.packageId,
234
+ name: declaration.tableName,
235
+ description: declaration.description,
236
+ primaryKeyName: declaration.primaryKeyName,
237
+ fields: declaration.fields,
238
+ versionNumber: 1,
239
+ ...(localOnly ? { localOnly: true } : {}),
240
+ },
241
+ versionNumber: 1,
242
+ };
243
+ const table = {
244
+ get: jest.fn(async (id) => (id === tableId ? stored : undefined)),
245
+ save: jest.fn(async (record) => record),
246
+ };
247
+ const registerTableDefinition = jest.fn();
248
+ const dataContext = {
249
+ tableContainer: { registerTableDefinition, getTable: jest.fn(() => table) },
250
+ };
251
+ const gateway = (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext);
252
+ // Synced stored definitions omit the flag, even when the incoming declaration says false.
253
+ await gateway.register(declaration);
254
+ const registered = registerTableDefinition.mock.calls[0][0].metaData;
255
+ if (localOnly)
256
+ expect(registered.localOnly).toBe(true);
257
+ else
258
+ expect(registered).not.toHaveProperty("localOnly");
259
+ // The first gateway rejects from the declaration it holds; a new one, as after a restart,
260
+ // has only the stored definition to go on.
261
+ for (const candidate of [gateway, (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext)]) {
262
+ registerTableDefinition.mockClear();
263
+ table.save.mockClear();
264
+ const activate = jest.fn();
265
+ await expect(candidate.registerBatch([
266
+ makeDeclaration({ packageId: declaration.packageId, tableName: "NewState" }),
267
+ { ...declaration, localOnly: !localOnly },
268
+ ], activate)).rejects.toThrow(/different schemas at version 1/);
269
+ expect(table.save).not.toHaveBeenCalled();
270
+ expect(registerTableDefinition).not.toHaveBeenCalled();
271
+ expect(activate).not.toHaveBeenCalled();
272
+ await expect(candidate.get({ packageId: declaration.packageId, tableName: "NewState" }, (0, utils_1.newid)())).rejects.toThrow(/Undeclared isolated table/);
273
+ }
274
+ registerTableDefinition.mockClear();
275
+ await (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext).registerBatch([
276
+ { ...declaration, versionNumber: 2, localOnly: !localOnly },
277
+ ]);
278
+ const savedMetaData = table.save.mock.calls[0][0].metaData;
279
+ const switched = registerTableDefinition.mock.calls[0][0].metaData;
280
+ for (const metaData of [savedMetaData, switched]) {
281
+ expect(metaData.versionNumber).toBe(2);
282
+ if (localOnly)
283
+ expect(metaData).not.toHaveProperty("localOnly");
284
+ else
285
+ expect(metaData.localOnly).toBe(true);
286
+ }
287
+ });
121
288
  test("rejects a table batch without publishing declarations that came before the conflict", async () => {
122
289
  const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
123
290
  const first = makeDeclaration({ versionNumber: 1 });
@@ -269,6 +436,82 @@ describe("isolated package table gateway", () => {
269
436
  expect(handlers.size).toBe(0);
270
437
  });
271
438
  });
439
+ describe("local-only isolated tables", () => {
440
+ test("reach the data source factory as local-only, here and on a device with only the synced schema", async () => {
441
+ const db = new SQLiteDB(":memory:");
442
+ const sql = {
443
+ async close() {
444
+ db.close();
445
+ },
446
+ async get(query, params = []) {
447
+ return db.prepare(query).get(params);
448
+ },
449
+ async all(query, params = []) {
450
+ return db.prepare(query).all(params);
451
+ },
452
+ async exec(query, params = []) {
453
+ return { changes: db.prepare(query).run(params).changes };
454
+ },
455
+ };
456
+ // Real tables also try to persist their definition through the user-context singleton,
457
+ // which this test doesn't set up; the gateway already saved it.
458
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
459
+ const declaration = makeDeclaration({ localOnly: true });
460
+ const fullName = `${declaration.tableName}_${declaration.packageId}`;
461
+ const factoryCalls = [];
462
+ // Two containers over one database: the second stands in for another device that has the
463
+ // synced TableDefinitions row but hasn't loaded the package.
464
+ // Builds tables the way DataContext does: the data source factory sees the metadata, which is
465
+ // where peers-device leaves local-only tables untracked.
466
+ const context = () => {
467
+ const dataContext = { dataContextId: (0, utils_1.newid)() };
468
+ const eventRegistry = new event_registry_1.EventRegistry(dataContext.dataContextId);
469
+ dataContext.tableContainer = new table_container_1.TableContainer((metaData, schema, TableClass) => {
470
+ factoryCalls.push({ name: (0, utils_1.getFullTableName)(metaData), localOnly: metaData.localOnly });
471
+ const dataSource = new sql_data_source_1.SQLDataSource(sql, metaData, schema);
472
+ return new (TableClass ?? table_1.Table)(metaData, { dataSource, eventRegistry, schema });
473
+ });
474
+ return dataContext;
475
+ };
476
+ try {
477
+ const writer = context();
478
+ const gateway = (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(writer);
479
+ await gateway.register(declaration);
480
+ const received = [];
481
+ const unsubscribe = await gateway.subscribe(declaration, "dataChanged", (event) => {
482
+ received.push(event);
483
+ });
484
+ const stateId = (0, utils_1.newid)();
485
+ await gateway.save(declaration, { stateId, count: 1 });
486
+ await gateway.call(declaration, "update", [{ stateId, count: 2 }]);
487
+ await gateway.call(declaration, "delete", [stateId]);
488
+ await new Promise((resolve) => setImmediate(resolve));
489
+ expect(received).toEqual([
490
+ { dataObject: { stateId, count: 1 }, op: "insert", source: "local" },
491
+ { dataObject: { stateId, count: 2 }, op: "update", source: "local" },
492
+ { dataObject: { stateId, count: 2 }, op: "delete", source: "local" },
493
+ ]);
494
+ unsubscribe();
495
+ await gateway.save(declaration, { stateId, count: 3 });
496
+ await new Promise((resolve) => setImmediate(resolve));
497
+ expect(received).toHaveLength(3);
498
+ expect(factoryCalls).toContainEqual({ name: fullName, localOnly: true });
499
+ expect(writer.tableContainer.getLocalOnlyTableNames()).toContain(fullName);
500
+ const stored = await (0, table_definitions_table_1.TableDefinitions)(writer).get((0, package_table_identity_1.packageScopedTableId)(declaration.packageId, declaration.tableName));
501
+ expect(stored?.metaData).toMatchObject({ localOnly: true });
502
+ const peer = context();
503
+ (0, table_definitions_table_1.TableDefinitions)(peer);
504
+ factoryCalls.length = 0;
505
+ expect(await peer.tableContainer.getFallbackTable(fullName)).toBeTruthy();
506
+ expect(factoryCalls).toContainEqual({ name: fullName, localOnly: true });
507
+ expect(peer.tableContainer.getLocalOnlyTableNames()).toContain(fullName);
508
+ }
509
+ finally {
510
+ warn.mockRestore();
511
+ db.close();
512
+ }
513
+ });
514
+ });
272
515
  describe("guarded isolated schema activation", () => {
273
516
  test("memory bindings remain unchanged when activation rejects", async () => {
274
517
  const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
@@ -7,7 +7,7 @@ import type { IConsumedContract, IRemoteContract, IRemoteContractConsumer } from
7
7
  import { type IPackage } from "../data/packages";
8
8
  import { type Observable } from "../observable";
9
9
  import type { IPeersPackage } from "../types/peers-package";
10
- import { type IIsolatedPackageRuntime } from "./isolated-runtime";
10
+ import { type IIsolatedCallPrincipal, type IIsolatedPackageRuntime } from "./isolated-runtime";
11
11
  import { type IPackageRuntimeState, type PackageRuntimeStates, type PackageRuntimeStatus } from "./package-runtime-state";
12
12
  import "../contracts/system";
13
13
  /** Options for {@link PackageLoader.unloadPackage}. */
@@ -112,6 +112,13 @@ export declare class PackageLoader {
112
112
  */
113
113
  private recordLoadFailure;
114
114
  private dispatchToolFromProvider;
115
+ /** Project a legacy caller's declared tool reach and authority for advisory catalog listings. */
116
+ hostToolPrincipal(principal: IIsolatedCallPrincipal, packageId: string, target: {
117
+ contractId: string;
118
+ version: number;
119
+ providerPackageId: string;
120
+ }): IIsolatedCallPrincipal | undefined;
121
+ private selectHostToolProvider;
115
122
  /**
116
123
  * Return the active provider selected by this data context's trusted registry.
117
124
  *
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PackageLoader = void 0;
4
4
  exports.setDefaultRequire = setDefaultRequire;
5
+ const action_dispatch_1 = require("../contracts/action-dispatch");
5
6
  const contract_provider_router_1 = require("../contracts/contract-provider-router");
6
7
  const contract_proxy_1 = require("../contracts/contract-proxy");
7
8
  const contract_resolvers_1 = require("../contracts/contract-resolvers");
@@ -227,26 +228,7 @@ class PackageLoader {
227
228
  throw new Error("Strict delegated calls cannot target another device until authority transport is supported");
228
229
  if (target.dataContextId && target.dataContextId !== this.dataContext.dataContextId)
229
230
  throw new Error("Cross-context action dispatch is denied");
230
- const own = this.contractRegistry
231
- .getRegisteredProviderIds(target.contractId, target.version)
232
- .includes(packageId);
233
- const consume = this.hostConsumes
234
- .get(packageId)
235
- ?.find((c) => c.contractId === target.contractId && c.version === target.version);
236
- if (!own && !consume)
237
- throw new Error("Undeclared contract consumption");
238
- if (own && target.providerPackageId && target.providerPackageId !== packageId && !consume)
239
- throw new Error("Provider substitution is denied for package-owned targets");
240
- const selected = own && (!target.providerPackageId || target.providerPackageId === packageId)
241
- ? packageId
242
- : consume
243
- ? this.contractRegistry.selectProvider(consume)?.providerPackageId
244
- : undefined;
245
- if (!selected)
246
- throw new Error("No declared contract provider is available");
247
- if (target.providerPackageId && target.providerPackageId !== selected)
248
- throw new Error("Provider substitution is denied");
249
- const providerPackageId = selected;
231
+ const providerPackageId = this.selectHostToolProvider(packageId, target);
250
232
  const authority = this.isolatedRuntime?.actionDispatchPrincipal?.(context.authority, packageId, target) ??
251
233
  context.authority;
252
234
  const callContext = { ...context, authority, callerPackageId: packageId };
@@ -274,6 +256,43 @@ class PackageLoader {
274
256
  endpoint.dispose();
275
257
  }
276
258
  }
259
+ /** Project a legacy caller's declared tool reach and authority for advisory catalog listings. */
260
+ hostToolPrincipal(principal, packageId, target) {
261
+ if (!principal.userId || this.isolatedRuntime?.isInstalled(packageId))
262
+ return undefined;
263
+ try {
264
+ this.selectHostToolProvider(packageId, target);
265
+ }
266
+ catch {
267
+ return undefined;
268
+ }
269
+ return (this.isolatedRuntime?.actionDispatchPrincipal?.(principal, packageId, {
270
+ ...target,
271
+ toolName: "",
272
+ }) ?? (0, action_dispatch_1.narrowActionPrincipal)(principal, true));
273
+ }
274
+ selectHostToolProvider(packageId, target) {
275
+ const own = this.contractRegistry
276
+ .getRegisteredProviderIds(target.contractId, target.version)
277
+ .includes(packageId);
278
+ const consume = this.hostConsumes
279
+ .get(packageId)
280
+ ?.find((c) => c.contractId === target.contractId && c.version === target.version);
281
+ if (!own && !consume)
282
+ throw new Error("Undeclared contract consumption");
283
+ if (own && target.providerPackageId && target.providerPackageId !== packageId && !consume)
284
+ throw new Error("Provider substitution is denied for package-owned targets");
285
+ const selected = own && (!target.providerPackageId || target.providerPackageId === packageId)
286
+ ? packageId
287
+ : consume
288
+ ? this.contractRegistry.selectProvider(consume)?.providerPackageId
289
+ : undefined;
290
+ if (!selected)
291
+ throw new Error("No declared contract provider is available");
292
+ if (target.providerPackageId && target.providerPackageId !== selected)
293
+ throw new Error("Provider substitution is denied");
294
+ return selected;
295
+ }
277
296
  /**
278
297
  * Return the active provider selected by this data context's trusted registry.
279
298
  *
@@ -60,6 +60,53 @@ const isolated_artifact_1 = require("./isolated-artifact");
60
60
  const isolated_runtime_1 = require("./isolated-runtime");
61
61
  const package_loader_1 = require("./package-loader");
62
62
  const consumedContracts = require("./resolve-consumed-contracts");
63
+ test("legacy catalog projection follows the dispatcher's declarations and selected provider", async () => {
64
+ const context = { dataContextId: (0, utils_1.newid)() };
65
+ const loader = new package_loader_1.PackageLoader(context);
66
+ const caller = (0, utils_1.newid)();
67
+ const provider = (0, utils_1.newid)();
68
+ const other = (0, utils_1.newid)();
69
+ const contractId = (0, utils_1.newid)();
70
+ const definition = {
71
+ contractId,
72
+ version: 1,
73
+ name: "Target",
74
+ description: "",
75
+ tools: [],
76
+ tables: [],
77
+ observables: [],
78
+ events: [],
79
+ };
80
+ const principal = {
81
+ userId: (0, utils_1.newid)(),
82
+ userAccessLevel: 40,
83
+ callingToolAccessLevel: 20,
84
+ };
85
+ const target = { contractId, version: 1, providerPackageId: provider };
86
+ try {
87
+ await loader.catalogReady;
88
+ loader.contractRegistry.register(provider, definition);
89
+ loader.contractRegistry.register(other, definition);
90
+ expect(loader.hostToolPrincipal(principal, caller, target)).toBeUndefined();
91
+ loader.hostConsumes.set(caller, [
92
+ { contractId, version: 1, providerPackageIds: [provider] },
93
+ ]);
94
+ expect(loader.hostToolPrincipal(principal, caller, target)).toMatchObject({
95
+ assistantAccessLevel: 20,
96
+ strictActionScope: true,
97
+ });
98
+ expect(loader.hostToolPrincipal(principal, caller, { ...target, providerPackageId: other })).toBeUndefined();
99
+ expect(loader.hostToolPrincipal({ ...principal, userId: undefined }, caller, target)).toBeUndefined();
100
+ // A provider can reach its own contract without declaring a consume.
101
+ expect(loader.hostToolPrincipal(principal, provider, target)).toMatchObject({
102
+ assistantAccessLevel: 20,
103
+ });
104
+ expect(loader.hostToolPrincipal(principal, provider, { ...target, providerPackageId: other })).toBeUndefined();
105
+ }
106
+ finally {
107
+ await loader.dispose();
108
+ }
109
+ });
63
110
  describe("host load invalidation", () => {
64
111
  it("retains legacy package identity and refuses system calls before forwarding", async () => {
65
112
  const packageId = (0, utils_1.newid)();
@@ -0,0 +1,25 @@
1
+ /** Builds a random string of base-36 characters with unbiased distribution (tweetnacl-backed). */
2
+ export declare function cryptoRandomString(length: number): string;
3
+ /**
4
+ * Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
5
+ * Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
6
+ */
7
+ export declare function newid(): string;
8
+ /** Lexicographic lower bound for valid peer ids (epoch). */
9
+ export declare const MIN_ID: string;
10
+ /** Lexicographic upper bound for valid peer ids (far future). */
11
+ export declare const MAX_ID: string;
12
+ /**
13
+ * Returns whether `id` is a 25-char base-36 string whose embedded time is not in the future (5 min skew allowed).
14
+ * @param id Candidate id (coerced to string)
15
+ */
16
+ export declare function isid(id: any): boolean;
17
+ /** Returns the `Date.now()`-compatible millisecond time encoded in the first 10 base-36 characters of a peer id. */
18
+ export declare function idTime(id: string): number;
19
+ /** Interprets a peer id’s time prefix as a `Date` (see {@link idTime}). */
20
+ export declare function idDate(id: string): Date;
21
+ /**
22
+ * Returns the 15-character random suffix of a peer id as two base-36 integers so large values stay exact in JS.
23
+ * @param id A 25-character peer id from {@link newid}
24
+ */
25
+ export declare function idRandNums(id: string): [number, number];
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_ID = exports.MIN_ID = void 0;
4
+ exports.cryptoRandomString = cryptoRandomString;
5
+ exports.newid = newid;
6
+ exports.isid = isid;
7
+ exports.idTime = idTime;
8
+ exports.idDate = idDate;
9
+ exports.idRandNums = idRandNums;
10
+ /**
11
+ * Peer ids: 25 base-36 characters, a 10-character time prefix plus a 15-character random suffix.
12
+ *
13
+ * Kept apart from `utils.ts` so code that only needs ids (such as zod schemas inside an isolated
14
+ * package) doesn't load lodash.
15
+ */
16
+ const nacl = require("tweetnacl");
17
+ /** Builds a random string of base-36 characters with unbiased distribution (tweetnacl-backed). */
18
+ function cryptoRandomString(length) {
19
+ let s = "";
20
+ let r = nacl.randomBytes(length + 6);
21
+ let i = 0;
22
+ while (s.length < length) {
23
+ if (i >= r.length) {
24
+ r = nacl.randomBytes(length - s.length);
25
+ i = 0;
26
+ }
27
+ const n = r[i];
28
+ i++;
29
+ if (n >= 252) {
30
+ // we don't want to bias the characters generated so only consider the range of numbers that spread evenly between factors of 36
31
+ // we exclude 252, 253, 254, and 255 because it would bias towards '0', '1', '2', and '3' respectively
32
+ continue;
33
+ }
34
+ s += (n % 36).toString(36);
35
+ }
36
+ return s.substring(0, length);
37
+ }
38
+ /**
39
+ * Creates a new 25-character peer id: base-36 time prefix (10 chars) + random suffix (15 chars).
40
+ * Sort order correlates with creation time. Use {@link isid} to validate user-provided values.
41
+ */
42
+ function newid() {
43
+ // modeled after mongo ids, timestamp + counter + random but we're going to skip counter and have bigger random
44
+ // people can switch to using a counter + random later if they want (or do something else with the random part).
45
+ // This id is going to be a 128 bit number so it fits naturally into memory.
46
+ // 2 ** 128 ~= 3.4e38 is the maximum it can be
47
+ // For efficient transmission, storage, presentation, and usage we're going to represent this number with base 36 [0-9a-z]
48
+ // (3.4e38).toString(36) == "f55n5nmuuaw00000000000000", this is our max in base 36, 25 characters
49
+ // Date.now() =~ 1625282091498
50
+ // but we're dealing in base 36 not base 10
51
+ // (1625282091498).toString(36) == "kqn6zrvu" (length 8)
52
+ // Number.parseInt("zzzzzzzz", 36) == 2821109907455 == year 2059. Way too soon so going up to 9 characters
53
+ // Number.parseInt("zzzzzzzzz", 36) == 101559956668415 == year 5188.
54
+ // but the max size of 9 left most can only be "f55n5nmuu" == 42720753566838 == year 3323. That could still work but only ~ 1300 years
55
+ // that leaves 16 characters to represent a random number to prevent collisions each millisecond, 36 ** 16 ~= 8e24
56
+ // Allocating one more character to the time and one less to the random number to avoid hitting max time in any conceivable future
57
+ // now our max time is new Date(Number.parseInt('f55n5nmuua',36)) == +050705-08-09T23:40:06.178Z. 50k years from now.
58
+ // We still have 15 chars for our random number. 36 ** 15 ~= 2e23
59
+ // That is still a very large number and the chance of a collision should be so small as to be effectively globally unique
60
+ // and certainly unique in any group's database.
61
+ const time = Date.now().toString(36).padStart(10, "0"); // e.g: "00kq6xh45f", length == 10
62
+ return time + cryptoRandomString(15);
63
+ }
64
+ /** Lexicographic lower bound for valid peer ids (epoch). */
65
+ exports.MIN_ID = "0".repeat(25); // 1970
66
+ /** Lexicographic upper bound for valid peer ids (far future). */
67
+ exports.MAX_ID = `f55n5nmuua${"z".repeat(15)}`; // 50k years from now
68
+ /**
69
+ * Returns whether `id` is a 25-char base-36 string whose embedded time is not in the future (5 min skew allowed).
70
+ * @param id Candidate id (coerced to string)
71
+ */
72
+ function isid(id) {
73
+ id = String(id);
74
+ const idMatch = Boolean(/^[0-9a-z]{25}$/i.exec(id));
75
+ if (idMatch) {
76
+ // only consider ids valid if they are less than current time (with 5 min buffer)
77
+ // This is to prevent people from making ids with times in the future to get them to always show up as the "newest" item and/or prematurely claim ids
78
+ return idTime(id) < Date.now() + 300000;
79
+ }
80
+ return false;
81
+ }
82
+ /** Returns the `Date.now()`-compatible millisecond time encoded in the first 10 base-36 characters of a peer id. */
83
+ function idTime(id) {
84
+ const time36 = id.substr(0, 10);
85
+ return Number.parseInt(time36, 36);
86
+ }
87
+ /** Interprets a peer id’s time prefix as a `Date` (see {@link idTime}). */
88
+ function idDate(id) {
89
+ return new Date(idTime(id));
90
+ }
91
+ /**
92
+ * Returns the 15-character random suffix of a peer id as two base-36 integers so large values stay exact in JS.
93
+ * @param id A 25-character peer id from {@link newid}
94
+ */
95
+ function idRandNums(id) {
96
+ const numPart1 = id.substr(10, 8);
97
+ const numPart2 = id.substr(18, 7);
98
+ return [Number.parseInt(numPart1, 36), Number.parseInt(numPart2, 36)];
99
+ }
@@ -0,0 +1 @@
1
+ export {};