@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
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.freezePackageContracts = freezePackageContracts;
4
+ exports.assertFrozenContracts = assertFrozenContracts;
5
+ exports.readFrozenContracts = readFrozenContracts;
6
+ const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_module_1 = require("node:module");
9
+ const path = require("node:path");
10
+ const zod_1 = require("zod");
11
+ const contract_definition_schema_1 = require("../contracts/contract-definition-schema");
12
+ const frozen_contracts_1 = require("../contracts/frozen-contracts");
13
+ const isolated_artifact_1 = require("../package-loader/isolated-artifact");
14
+ const build_package_1 = require("./build-package");
15
+ /**
16
+ * Freeze the contract versions a package provides, before releasing it as stable: add each one
17
+ * not yet in `frozen-contracts.json`, as frozen by the package's version. With `check`, or when a
18
+ * frozen version's shape changed, write nothing.
19
+ *
20
+ * @param options - The package, where its contracts come from, and whether only to check.
21
+ */
22
+ async function freezePackageContracts(options) {
23
+ const root = path.resolve(options.root ?? process.cwd());
24
+ const { contracts, alsoImplements, packageVersion } = options.bundle
25
+ ? await bundleContracts(path.resolve(options.bundle))
26
+ : manifestContracts(await (0, build_package_1.packageManifest)({ root, entry: options.entry }));
27
+ const file = path.join(root, frozen_contracts_1.FROZEN_CONTRACTS_FILE);
28
+ const current = await readFrozenContracts(root);
29
+ const check = checkServedContracts(current ?? { contracts: [] }, contracts, alsoImplements);
30
+ if (options.check) {
31
+ return { path: file, packageVersion, added: [], check, written: false };
32
+ }
33
+ if (!packageVersion)
34
+ throw new Error("The package has no version to freeze its contracts as");
35
+ if (check.changed.length > 0) {
36
+ return { path: file, packageVersion, added: [], check, written: false };
37
+ }
38
+ const result = (0, frozen_contracts_1.freezeContracts)(current, contracts, packageVersion);
39
+ const written = result.check.changed.length === 0 && (result.added.length > 0 || !current);
40
+ if (written)
41
+ await (0, promises_1.writeFile)(file, `${JSON.stringify(result.frozen, null, 2)}\n`, "utf8");
42
+ return { path: file, packageVersion, added: result.added, check, written };
43
+ }
44
+ /** Compare explicit and carried shapes; aliases still serve the versions they name. */
45
+ function checkServedContracts(frozen, contracts, alsoImplements) {
46
+ const check = (0, frozen_contracts_1.checkFrozenContracts)(frozen, contracts);
47
+ const changed = new Map(check.changed.map((change) => [
48
+ `${change.contract.contractId}@${change.contract.version}`,
49
+ change,
50
+ ]));
51
+ // A bundle may have been authored by hand or built against an older snapshot. Merely naming
52
+ // a frozen version must not let a different carried shape bypass the source-controlled guard.
53
+ for (const target of alsoImplements) {
54
+ if (!target.definition)
55
+ continue;
56
+ for (const change of (0, frozen_contracts_1.checkFrozenContracts)(frozen, [target.definition]).changed) {
57
+ changed.set(`${change.contract.contractId}@${change.contract.version}`, change);
58
+ }
59
+ }
60
+ return {
61
+ changed: [...changed.values()],
62
+ notProvided: check.notProvided.filter((contract) => !alsoImplements.some((target) => namesVersion(target, contract))),
63
+ };
64
+ }
65
+ /** Whether an `alsoImplements` declaration names a contract version (a single one or in its range). */
66
+ function namesVersion(target, contract) {
67
+ const { from, to } = typeof target.version === "number"
68
+ ? { from: target.version, to: target.version }
69
+ : target.version;
70
+ return (target.contractId === contract.contractId && contract.version >= from && contract.version <= to);
71
+ }
72
+ /**
73
+ * Refuse a manifest that changes a contract version the package's stable releases froze, as
74
+ * `peers-package build` does. A package without `frozen-contracts.json` passes.
75
+ *
76
+ * @param root - The package root.
77
+ * @param manifest - The manifest about to be built.
78
+ */
79
+ async function assertFrozenContracts(root, manifest) {
80
+ const frozen = await readFrozenContracts(root);
81
+ if (!frozen)
82
+ return;
83
+ const { contracts, alsoImplements } = manifestContracts(manifest);
84
+ const check = checkServedContracts(frozen, contracts, alsoImplements);
85
+ if (check.changed.length > 0) {
86
+ throw new Error(`This build changes a frozen contract:\n${(0, frozen_contracts_1.describeFrozenContractChanges)(check).join("\n")}`);
87
+ }
88
+ }
89
+ /**
90
+ * Read a package's `frozen-contracts.json`.
91
+ *
92
+ * @param root - The package root.
93
+ * @returns Its content, or undefined when the package has none.
94
+ */
95
+ async function readFrozenContracts(root) {
96
+ const file = path.join(root, frozen_contracts_1.FROZEN_CONTRACTS_FILE);
97
+ if (!(0, node_fs_1.existsSync)(file))
98
+ return undefined;
99
+ const frozen = JSON.parse(await (0, promises_1.readFile)(file, "utf8"));
100
+ const validated = frozenContractsSchema.safeParse(frozen);
101
+ if (!validated.success) {
102
+ throw new Error(`Invalid ${file}: ${validated.error.message}`);
103
+ }
104
+ // Validate without normalizing or stripping keys: retain the exact released shape, including
105
+ // IR additions this SDK does not yet know, for comparison and subsequent freezes.
106
+ return frozen;
107
+ }
108
+ const frozenContractsSchema = zod_1.z.object({
109
+ about: zod_1.z.string().optional(),
110
+ contracts: zod_1.z
111
+ .array(contract_definition_schema_1.contractDefinitionSchema.extend({
112
+ frozenBy: zod_1.z.string().trim().min(1),
113
+ devTag: zod_1.z.never().optional(),
114
+ }))
115
+ .superRefine((contracts, ctx) => {
116
+ const keys = new Set();
117
+ contracts.forEach((contract, index) => {
118
+ const key = `${contract.contractId}@${contract.version}`;
119
+ if (keys.has(key)) {
120
+ ctx.addIssue({
121
+ code: zod_1.z.ZodIssueCode.custom,
122
+ path: [index],
123
+ message: `Duplicate contract ${key}`,
124
+ });
125
+ }
126
+ keys.add(key);
127
+ });
128
+ }),
129
+ });
130
+ const aliasVersionSchema = contract_definition_schema_1.contractDefinitionSchema.shape.version;
131
+ const alsoImplementsSchema = zod_1.z.map(zod_1.z.string(), zod_1.z.array(zod_1.z
132
+ .object({
133
+ contractId: contract_definition_schema_1.contractDefinitionSchema.shape.contractId,
134
+ version: zod_1.z.union([
135
+ aliasVersionSchema,
136
+ zod_1.z
137
+ .object({ from: aliasVersionSchema, to: aliasVersionSchema })
138
+ .refine(({ from, to }) => from <= to, "version.from must not exceed .to"),
139
+ ]),
140
+ definition: contract_definition_schema_1.contractDefinitionSchema.optional(),
141
+ })
142
+ .refine((target) => !target.definition ||
143
+ (target.definition.contractId === target.contractId &&
144
+ target.definition.version === target.version), "definition must be the declared contract version")));
145
+ /**
146
+ * The contracts an isolated manifest provides, as the Runtime registers them, and the versions
147
+ * they also implement.
148
+ */
149
+ function manifestContracts(manifest) {
150
+ return {
151
+ contracts: manifest.provides.map((contract) => (0, isolated_artifact_1.isolatedContractToDefinition)(contract, manifest.tables ?? [])),
152
+ alsoImplements: manifest.provides.flatMap((contract) => contract.alsoImplements ?? []),
153
+ packageVersion: manifest.version,
154
+ };
155
+ }
156
+ /** The contracts a built bundle provides: an isolated artifact, or a host-backed CommonJS bundle. */
157
+ async function bundleContracts(file) {
158
+ const text = await (0, promises_1.readFile)(file, "utf8");
159
+ if ((0, isolated_artifact_1.looksLikeIsolatedPackageArtifact)(text.trimStart())) {
160
+ return manifestContracts((0, isolated_artifact_1.parseIsolatedPackageArtifact)(text.trimStart()).manifest);
161
+ }
162
+ // A host-backed bundle is CommonJS with the Peers SDK and zod as externals, as the package
163
+ // loader runs it. Its module code runs here, as it would in the host.
164
+ const sdk = require("../index");
165
+ const packageRequire = (0, node_module_1.createRequire)(file);
166
+ const module = { exports: {} };
167
+ const requireExternal = (id) => {
168
+ if (id === "PeersSDK" || id === "peers-sdk" || id === "@peers-app/peers-sdk")
169
+ return sdk;
170
+ if (id === "zod")
171
+ return require("zod");
172
+ return packageRequire(id);
173
+ };
174
+ new Function("module", "exports", "require", text)(module, module.exports, requireExternal);
175
+ const definition = module.exports.packageDefinition;
176
+ if (!Array.isArray(definition?.contracts)) {
177
+ throw new Error(`${file} isn't a package artifact and exports no packageDefinition`);
178
+ }
179
+ const validated = zod_1.z.array(contract_definition_schema_1.contractDefinitionSchema).safeParse(definition.contracts);
180
+ if (!validated.success) {
181
+ throw new Error(`Invalid contracts in ${file}: ${validated.error.message}`);
182
+ }
183
+ if (definition.alsoImplements !== undefined) {
184
+ const aliases = alsoImplementsSchema.safeParse(definition.alsoImplements);
185
+ if (!aliases.success) {
186
+ throw new Error(`Invalid alsoImplements in ${file}: ${aliases.error.message}`);
187
+ }
188
+ }
189
+ // The loader reads this map only for contracts actually provided by the bundle. Stale map
190
+ // entries for removed contracts do not serve their targets and must not quiet retirement notes.
191
+ const alsoImplements = definition.contracts.flatMap((contract) => definition.alsoImplements?.get(`${contract.contractId}@${contract.version}`) ?? []);
192
+ // Keep the original definitions: parsing would strip unknown IR keys.
193
+ return {
194
+ contracts: definition.contracts,
195
+ alsoImplements,
196
+ packageVersion: definition.version ?? "",
197
+ };
198
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promises_1 = require("node:fs/promises");
4
+ const node_os_1 = require("node:os");
5
+ const path = require("node:path");
6
+ const peer_id_1 = require("../peer-id");
7
+ const build_package_1 = require("./build-package");
8
+ const cli_1 = require("./cli");
9
+ const freeze_contracts_1 = require("./freeze-contracts");
10
+ const sdkRoot = path.resolve(__dirname, "../..");
11
+ const packageId = (0, peer_id_1.newid)();
12
+ const contractId = (0, peer_id_1.newid)();
13
+ let root;
14
+ /** A `/define` package providing Probe v1 (and v2 when asked), its tool shaped by `input`. */
15
+ function declaration(version, input, description = "Probe.", withV2 = false) {
16
+ const contract = (contractVersion) => `defineContract({
17
+ contractId: ${JSON.stringify(contractId)}, version: ${contractVersion}, name: "Probe",
18
+ tools: [defineTool({
19
+ name: "probe", usageDescription: ${JSON.stringify(description)}, accessLevel: 0,
20
+ inputSchema: z.object(${input}), outputSchema: z.object({}),
21
+ handler: () => ({}),
22
+ })],
23
+ })`;
24
+ return `
25
+ import { defineContract, definePackage, defineTool, z } from "@peers-app/peers-sdk/define";
26
+ export default definePackage((pkg) => {
27
+ pkg.packageId = ${JSON.stringify(packageId)};
28
+ pkg.version = ${JSON.stringify(version)};
29
+ pkg.provides(${contract(1)});
30
+ ${withV2 ? `pkg.provides(${contract(2)});` : ""}
31
+ });
32
+ `;
33
+ }
34
+ const frozen = async () => JSON.parse(await (0, promises_1.readFile)(path.join(root, "frozen-contracts.json"), "utf8"));
35
+ beforeEach(async () => {
36
+ root = await (0, promises_1.mkdtemp)(path.join((0, node_os_1.tmpdir)(), "peers-package-freeze-"));
37
+ await (0, promises_1.mkdir)(path.join(root, "src"));
38
+ await (0, promises_1.mkdir)(path.join(root, "node_modules/@peers-app"), { recursive: true });
39
+ await (0, promises_1.symlink)(sdkRoot, path.join(root, "node_modules/@peers-app/peers-sdk"), "dir");
40
+ await (0, promises_1.symlink)(path.dirname(require.resolve("esbuild/package.json")), path.join(root, "node_modules/esbuild"), "dir");
41
+ await (0, promises_1.writeFile)(path.join(root, "package.json"), '{"name":"freeze-probe","version":"1.0.0"}');
42
+ await (0, promises_1.writeFile)(path.join(root, "src/package.ts"), declaration("1.0.0", "{ name: z.string() }"));
43
+ });
44
+ afterEach(async () => {
45
+ await (0, promises_1.rm)(root, { recursive: true, force: true });
46
+ });
47
+ test("freezes a release's contracts, then the build refuses a shape change but not a new description", async () => {
48
+ const log = jest.spyOn(console, "log").mockImplementation(() => undefined);
49
+ const error = jest.spyOn(console, "error").mockImplementation(() => undefined);
50
+ try {
51
+ // Nothing frozen yet: the check passes, saying so, and the build runs.
52
+ await expect((0, cli_1.main)(["contract", "freeze", "--check", "--root", root])).resolves.toBe(0);
53
+ expect(log).toHaveBeenCalledWith(expect.stringContaining("nothing is frozen yet"));
54
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root })).resolves.toMatchObject({
55
+ packageVersion: "1.0.0",
56
+ added: [expect.objectContaining({ contractId, version: 1, frozenBy: "1.0.0" })],
57
+ written: true,
58
+ });
59
+ expect((await frozen()).contracts.map(({ version, frozenBy }) => [version, frozenBy])).toEqual([
60
+ [1, "1.0.0"],
61
+ ]);
62
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root })).resolves.toMatchObject({
63
+ added: [],
64
+ written: false,
65
+ });
66
+ // A new description is allowed; a changed input isn't, in the build or the check.
67
+ await (0, promises_1.writeFile)(path.join(root, "src/package.ts"), declaration("1.1.0", "{ name: z.string() }", "Probe, described again."));
68
+ await expect((0, build_package_1.buildPackage)({ root })).resolves.toMatchObject({});
69
+ await (0, promises_1.writeFile)(path.join(root, "src/package.ts"), declaration("1.1.0", "{ name: z.string(), count: z.number() }"));
70
+ await expect((0, build_package_1.buildPackage)({ root })).rejects.toThrow(`This build changes a frozen contract:\nProbe (${contractId}@1), frozen by 1.0.0: tool probe changed.`);
71
+ await expect((0, build_package_1.generatePackageManifest)({ root })).rejects.toThrow("tool probe changed");
72
+ const beforeFailure = await (0, promises_1.readFile)(path.join(root, "frozen-contracts.json"), "utf8");
73
+ await expect((0, cli_1.main)(["contract", "freeze", "--check", "--root", root])).resolves.toBe(1);
74
+ await expect((0, cli_1.main)(["contract", "freeze", "--root", root])).resolves.toBe(1);
75
+ expect(error).toHaveBeenCalledWith(expect.stringContaining("tool probe changed"));
76
+ expect((await frozen()).contracts).toHaveLength(1);
77
+ expect(await (0, promises_1.readFile)(path.join(root, "frozen-contracts.json"), "utf8")).toBe(beforeFailure);
78
+ // The change goes in a new version, which the next stable release freezes.
79
+ await (0, promises_1.writeFile)(path.join(root, "src/package.ts"), declaration("2.0.0", "{ name: z.string() }", "Probe.", true));
80
+ await expect((0, cli_1.main)(["contract", "freeze", "--root", root])).resolves.toBe(0);
81
+ expect((await frozen()).contracts.map(({ version, frozenBy }) => [version, frozenBy])).toEqual([
82
+ [1, "1.0.0"],
83
+ [2, "2.0.0"],
84
+ ]);
85
+ }
86
+ finally {
87
+ log.mockRestore();
88
+ error.mockRestore();
89
+ }
90
+ });
91
+ test("doesn't note a frozen version the package still serves through alsoImplements", async () => {
92
+ const log = jest.spyOn(console, "log").mockImplementation(() => undefined);
93
+ const v2 = (alsoImplements) => `
94
+ import { defineContract, definePackage, defineTool, z } from "@peers-app/peers-sdk/define";
95
+ export default definePackage((pkg) => {
96
+ pkg.packageId = ${JSON.stringify(packageId)};
97
+ pkg.version = "2.0.0";
98
+ pkg.provides(defineContract({
99
+ contractId: ${JSON.stringify(contractId)}, version: 2, name: "Probe",
100
+ tools: [defineTool({
101
+ name: "probe", usageDescription: "Probe.", accessLevel: 0,
102
+ inputSchema: z.object({ name: z.string(), tag: z.string().optional() }),
103
+ outputSchema: z.object({}), handler: () => ({}),
104
+ })],
105
+ ${alsoImplements}
106
+ }));
107
+ });
108
+ `;
109
+ try {
110
+ await (0, freeze_contracts_1.freezePackageContracts)({ root });
111
+ await (0, promises_1.writeFile)(path.join(root, "src/package.ts"), v2("alsoImplements: [1],"));
112
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, check: true })).resolves.toMatchObject({
113
+ check: { changed: [], notProvided: [] },
114
+ });
115
+ await expect((0, cli_1.main)(["contract", "freeze", "--root", root])).resolves.toBe(0);
116
+ expect(log).not.toHaveBeenCalledWith(expect.stringContaining("is no longer provided"));
117
+ expect((await frozen()).contracts.map(({ version, frozenBy }) => [version, frozenBy])).toEqual([
118
+ [1, "1.0.0"],
119
+ [2, "2.0.0"],
120
+ ]);
121
+ // Without the alias, v1's consumers lose this provider, and the check says so.
122
+ await (0, promises_1.writeFile)(path.join(root, "src/package.ts"), v2(""));
123
+ const checked = await (0, freeze_contracts_1.freezePackageContracts)({ root, check: true });
124
+ expect(checked.check.notProvided.map(({ version }) => version)).toEqual([1]);
125
+ }
126
+ finally {
127
+ log.mockRestore();
128
+ }
129
+ });
130
+ test("reads an isolated artifact with a cwd-relative bundle and a separate root", async () => {
131
+ const built = await (0, build_package_1.buildPackage)({ root });
132
+ await expect((0, freeze_contracts_1.freezePackageContracts)({
133
+ root,
134
+ bundle: path.relative(process.cwd(), built.artifactPath),
135
+ })).resolves.toMatchObject({ written: true, packageVersion: "1.0.0" });
136
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle: built.artifactPath, check: true })).resolves.toMatchObject({ written: false, check: { changed: [] } });
137
+ });
138
+ test("checks a bundle's carried shapes against frozen versions before writing", async () => {
139
+ await (0, freeze_contracts_1.freezePackageContracts)({ root });
140
+ const file = path.join(root, "frozen-contracts.json");
141
+ const before = await (0, promises_1.readFile)(file, "utf8");
142
+ const built = await (0, build_package_1.buildPackage)({ root });
143
+ const { frozenBy: _frozenBy, ...original } = (await frozen()).contracts[0];
144
+ const carried = {
145
+ ...original,
146
+ tools: [{ ...original.tools[0], inputFields: [] }],
147
+ };
148
+ const manifest = structuredClone(built.manifest);
149
+ manifest.provides[0].version = 2;
150
+ manifest.provides[0].tools[0].inputFields = [];
151
+ manifest.provides[0].alsoImplements = [{ contractId, version: 1, definition: carried }];
152
+ await (0, promises_1.writeFile)(built.artifactPath, `PEERS_ISOLATED_PACKAGE_V1\n${JSON.stringify({ manifest, providerSource: "void 0;" })}\n`);
153
+ for (const check of [true, false]) {
154
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle: built.artifactPath, check })).resolves.toMatchObject({
155
+ added: [],
156
+ written: false,
157
+ check: {
158
+ changed: [{ contract: { contractId, version: 1 }, changes: ["tool probe changed"] }],
159
+ },
160
+ });
161
+ expect(await (0, promises_1.readFile)(file, "utf8")).toBe(before);
162
+ }
163
+ await expect((0, freeze_contracts_1.assertFrozenContracts)(root, manifest)).rejects.toThrow("tool probe changed");
164
+ });
165
+ test("only a provided host contract's aliases quiet retirement notes, with inclusive ranges", async () => {
166
+ await (0, freeze_contracts_1.freezePackageContracts)({ root });
167
+ const { frozenBy: _frozenBy, ...original } = (await frozen()).contracts[0];
168
+ const otherId = (0, peer_id_1.newid)();
169
+ const provided = { ...original, contractId: otherId, version: 3 };
170
+ const bundle = path.join(root, "aliases.bundle.js");
171
+ const target = { contractId, version: { from: 1, to: 2 } };
172
+ const cases = [
173
+ { key: `${otherId}@3`, target, notProvided: [] },
174
+ { key: `${otherId}@2`, target, notProvided: [expect.objectContaining({ contractId })] },
175
+ {
176
+ key: `${otherId}@3`,
177
+ target: { contractId: otherId, version: { from: 1, to: 2 } },
178
+ notProvided: [expect.objectContaining({ contractId })],
179
+ },
180
+ ];
181
+ for (const { key, target, notProvided } of cases) {
182
+ await (0, promises_1.writeFile)(bundle, `exports.packageDefinition = {
183
+ version: "2.0.0", contracts: [${JSON.stringify(provided)}],
184
+ alsoImplements: new Map([[${JSON.stringify(key)}, [${JSON.stringify(target)}]]]),
185
+ };`);
186
+ for (const check of [true, false]) {
187
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check })).resolves.toMatchObject({
188
+ check: { changed: [], notProvided },
189
+ });
190
+ }
191
+ }
192
+ });
193
+ test.each([
194
+ "{}",
195
+ `new Map([[${JSON.stringify(`${contractId}@2`)}, {}]])`,
196
+ `new Map([[${JSON.stringify(`${contractId}@2`)}, [null]]])`,
197
+ `new Map([[${JSON.stringify(`${contractId}@2`)}, [{ contractId: ${JSON.stringify(contractId)}, version: 1.5 }]]])`,
198
+ `new Map([[${JSON.stringify(`${contractId}@2`)}, [{ contractId: ${JSON.stringify(contractId)}, version: { from: 2, to: 1 } }]]])`,
199
+ ])("rejects malformed host alsoImplements without changing the snapshot: %s", async (aliases) => {
200
+ await (0, freeze_contracts_1.freezePackageContracts)({ root });
201
+ const file = path.join(root, "frozen-contracts.json");
202
+ const before = await (0, promises_1.readFile)(file, "utf8");
203
+ const { frozenBy: _frozenBy, ...original } = (await frozen()).contracts[0];
204
+ const bundle = path.join(root, "invalid-alias.bundle.js");
205
+ await (0, promises_1.writeFile)(bundle, `exports.packageDefinition = {
206
+ version: "2.0.0", contracts: [${JSON.stringify({ ...original, version: 2 })}],
207
+ alsoImplements: ${aliases},
208
+ };`);
209
+ for (const check of [true, false]) {
210
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check })).rejects.toThrow("Invalid alsoImplements");
211
+ expect(await (0, promises_1.readFile)(file, "utf8")).toBe(before);
212
+ }
213
+ });
214
+ test.each([
215
+ "",
216
+ "{",
217
+ "null",
218
+ "{}",
219
+ '{"contracts":[{}]}',
220
+ '{"contracts":null}',
221
+ ])("rejects a malformed snapshot without overwriting it: %s", async (content) => {
222
+ const file = path.join(root, "frozen-contracts.json");
223
+ await (0, promises_1.writeFile)(file, content);
224
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root })).rejects.toThrow();
225
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, check: true })).rejects.toThrow();
226
+ await expect((0, build_package_1.buildPackage)({ root })).rejects.toThrow();
227
+ expect(await (0, promises_1.readFile)(file, "utf8")).toBe(content);
228
+ });
229
+ test("rejects incomplete shapes and duplicate frozen identities", async () => {
230
+ await (0, freeze_contracts_1.freezePackageContracts)({ root });
231
+ const snapshot = await frozen();
232
+ const invalid = [
233
+ { contracts: snapshot.contracts.map(({ version: _version, ...entry }) => entry) },
234
+ { contracts: snapshot.contracts.map(({ tools: _tools, ...entry }) => entry) },
235
+ { contracts: snapshot.contracts.map((entry) => ({ ...entry, tools: [{ name: "broken" }] })) },
236
+ { contracts: [...snapshot.contracts, ...snapshot.contracts] },
237
+ ];
238
+ for (const content of invalid) {
239
+ await (0, promises_1.writeFile)(path.join(root, "frozen-contracts.json"), JSON.stringify(content));
240
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, check: true })).rejects.toThrow("Invalid");
241
+ }
242
+ });
243
+ test("accepts an empty contracts list and refuses to freeze a missing package version", async () => {
244
+ await (0, promises_1.writeFile)(path.join(root, "frozen-contracts.json"), '{"contracts":[]}');
245
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root })).resolves.toMatchObject({ written: true });
246
+ const snapshot = await (0, promises_1.readFile)(path.join(root, "frozen-contracts.json"), "utf8");
247
+ const bundle = path.join(root, "missing-version.js");
248
+ await (0, promises_1.writeFile)(bundle, "exports.packageDefinition = { contracts: [] };");
249
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle })).rejects.toThrow("no version");
250
+ expect(await (0, promises_1.readFile)(path.join(root, "frozen-contracts.json"), "utf8")).toBe(snapshot);
251
+ });
252
+ test("reports a newly written empty snapshot", async () => {
253
+ const bundle = path.join(root, "empty.js");
254
+ await (0, promises_1.writeFile)(bundle, 'exports.packageDefinition = { version: "1.0.0", contracts: [] };');
255
+ const log = jest.spyOn(console, "log").mockImplementation(() => undefined);
256
+ try {
257
+ await expect((0, cli_1.main)(["contract", "freeze", "--root", root, "--bundle", bundle])).resolves.toBe(0);
258
+ expect(log).toHaveBeenCalledWith(expect.stringContaining("Wrote "));
259
+ expect((await frozen()).contracts).toEqual([]);
260
+ }
261
+ finally {
262
+ log.mockRestore();
263
+ }
264
+ });
265
+ test("rejects malformed host definitions without creating or replacing a snapshot", async () => {
266
+ const bundle = path.join(root, "malformed.bundle.js");
267
+ await (0, promises_1.writeFile)(bundle, 'exports.packageDefinition = { version: "1.0.0", contracts: [{}] };');
268
+ for (const check of [true, false]) {
269
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check })).rejects.toThrow("Invalid contracts");
270
+ }
271
+ await expect((0, promises_1.readFile)(path.join(root, "frozen-contracts.json"))).rejects.toMatchObject({
272
+ code: "ENOENT",
273
+ });
274
+ await (0, freeze_contracts_1.freezePackageContracts)({ root });
275
+ const before = await (0, promises_1.readFile)(path.join(root, "frozen-contracts.json"), "utf8");
276
+ for (const check of [true, false]) {
277
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check })).rejects.toThrow("Invalid contracts");
278
+ }
279
+ expect(await (0, promises_1.readFile)(path.join(root, "frozen-contracts.json"), "utf8")).toBe(before);
280
+ });
281
+ test("rejects duplicate host contracts without writing and preserves unknown shape keys", async () => {
282
+ await (0, freeze_contracts_1.freezePackageContracts)({ root });
283
+ const file = path.join(root, "frozen-contracts.json");
284
+ const before = await (0, promises_1.readFile)(file, "utf8");
285
+ const { frozenBy: _frozenBy, ...original } = (await frozen()).contracts[0];
286
+ const changed = { ...original, tools: [] };
287
+ const bundle = path.join(root, "duplicate.bundle.js");
288
+ const writeBundle = (contracts) => (0, promises_1.writeFile)(bundle, `exports.packageDefinition = ${JSON.stringify({ version: "1.1.0", contracts })};`);
289
+ await writeBundle([changed, original]);
290
+ for (const check of [true, false]) {
291
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check })).rejects.toThrow("Duplicate provided contract");
292
+ }
293
+ expect(await (0, promises_1.readFile)(file, "utf8")).toBe(before);
294
+ await (0, promises_1.rm)(file);
295
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle })).rejects.toThrow("Duplicate provided contract");
296
+ await expect((0, promises_1.readFile)(file)).rejects.toMatchObject({ code: "ENOENT" });
297
+ const extended = {
298
+ ...original,
299
+ tools: [{ ...original.tools[0], futureShape: { enabled: true } }],
300
+ };
301
+ await writeBundle([extended]);
302
+ await (0, freeze_contracts_1.freezePackageContracts)({ root, bundle });
303
+ expect((await frozen()).contracts[0].tools[0]).toMatchObject({ futureShape: { enabled: true } });
304
+ await writeBundle([
305
+ { ...extended, tools: [{ ...extended.tools[0], futureShape: { enabled: false } }] },
306
+ ]);
307
+ expect((await (0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check: true })).check.changed).toHaveLength(1);
308
+ });
309
+ test("reads a host-backed package's CommonJS bundle, with the SDK and zod as externals", async () => {
310
+ const bundle = path.join(root, "legacy.bundle.js");
311
+ const tool = (fields) => `{
312
+ tool: {
313
+ toolId: ${JSON.stringify((0, peer_id_1.newid)())}, name: "act", code: "", usageDescription: "Act.",
314
+ inputSchema: { type: "complex", fields: ${fields} },
315
+ outputSchema: { type: "complex", fields: [] },
316
+ },
317
+ }`;
318
+ const legacy = (version, fields) => `
319
+ const { definePackage } = require("PeersSDK");
320
+ require("zod");
321
+ exports.packageDefinition = definePackage((pkg) => {
322
+ pkg.packageId = ${JSON.stringify(packageId)};
323
+ pkg.version = ${JSON.stringify(version)};
324
+ const contract = pkg.contract(${JSON.stringify(contractId)}, 1, "Legacy", "A legacy contract.");
325
+ contract.tools = [${tool(fields)}];
326
+ });
327
+ `;
328
+ await (0, promises_1.writeFile)(bundle, legacy("0.5.0", '[{ name: "value", type: "string" }]'));
329
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle })).resolves.toMatchObject({
330
+ packageVersion: "0.5.0",
331
+ added: [expect.objectContaining({ name: "Legacy", version: 1, frozenBy: "0.5.0" })],
332
+ });
333
+ expect((await frozen()).contracts[0]).not.toHaveProperty("devTag");
334
+ await (0, promises_1.writeFile)(bundle, legacy("0.6.0", '[{ name: "value", type: "number" }]'));
335
+ const checked = await (0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check: true });
336
+ expect(checked.check.changed.map(({ changes }) => changes)).toEqual([["tool act changed"]]);
337
+ // v2 also implementing v1 still serves it; without that, v1 is no longer provided.
338
+ const upgraded = (alsoImplements) => `
339
+ const { definePackage } = require("PeersSDK");
340
+ exports.packageDefinition = definePackage((pkg) => {
341
+ pkg.packageId = ${JSON.stringify(packageId)};
342
+ pkg.version = "1.0.0";
343
+ const contract = pkg.contract(${JSON.stringify(contractId)}, 2, "Legacy", "A legacy contract.");
344
+ contract.tools = [${tool('[{ name: "value", type: "string" }]')}];
345
+ ${alsoImplements}
346
+ });
347
+ `;
348
+ await (0, promises_1.writeFile)(bundle, upgraded(`contract.alsoImplements(${JSON.stringify(contractId)}, 1);`));
349
+ await expect((0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check: true })).resolves.toMatchObject({
350
+ check: { changed: [], notProvided: [] },
351
+ });
352
+ await (0, promises_1.writeFile)(bundle, upgraded(""));
353
+ const dropped = await (0, freeze_contracts_1.freezePackageContracts)({ root, bundle, check: true });
354
+ expect(dropped.check.notProvided.map(({ version }) => version)).toEqual([1]);
355
+ });
@@ -2,6 +2,10 @@ import type { DataContext } from "../context/data-context";
2
2
  import type { ICreatePackageOpts, ICreatePackageResult, IPackageInstallerDeps, ITemplateCacheResult, TemplateCacheRefreshFailure } from "./types";
3
3
  /**
4
4
  * Convert a human-readable package name to a filesystem-safe folder slug.
5
+ * Keeps existing ASCII slugs so creation still imports folders made by older creators.
6
+ * Uses Unicode letters/numbers when the ASCII slug is empty, or a fresh package folder for
7
+ * names with no letters or numbers, rather than resolving to the packages root.
8
+ * Shortens names exceeding a filesystem component's byte limit with a stable hash suffix.
5
9
  * "My Widget" → "my-widget", "camelCase Thing" → "camel-case-thing"
6
10
  */
7
11
  export declare function nameToFolderSlug(name: string): string;