@peers-app/peers-sdk 0.18.3 → 0.18.5

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.
@@ -57,7 +57,7 @@ describe("definePackage", () => {
57
57
  expect(contract.version).toBe(1);
58
58
  expect(contract.name).toBe("Tasks");
59
59
  expect(contract.description).toBe("");
60
- expect(contract.devTag).toBeUndefined();
60
+ expect(contract.devTag).toBe("dev");
61
61
  expect(contract.tables).toHaveLength(1);
62
62
  expect(contract.tables[0].name).toBe("Tasks");
63
63
  expect(contract.tables[0].fields).toHaveLength(2);
@@ -163,11 +163,11 @@ describe("definePackage", () => {
163
163
  expect(result.assistants).toEqual([assistant]);
164
164
  expect(result.appNavs).toEqual([nav]);
165
165
  });
166
- it("supports devTag on contracts", () => {
166
+ it("always assigns devTag 'dev' to contracts from definePackage", () => {
167
167
  const devContractId = (0, utils_1.newid)();
168
168
  const result = (0, builder_1.definePackage)((pkg) => {
169
169
  pkg.packageId = TEST_PKG_ID;
170
- const c = pkg.contract(devContractId, 1, "Dev Contract", "dev");
170
+ const c = pkg.contract(devContractId, 1, "Dev Contract");
171
171
  c.tables = [
172
172
  {
173
173
  metaData: {
@@ -227,12 +227,11 @@ describe("definePackage", () => {
227
227
  expect(result.contracts[0].name).toBe("Human-Readable Name");
228
228
  expect(result.contracts[0].description).toBe("");
229
229
  });
230
- it("includes packageId, version, and versionTag in the result", () => {
230
+ it("includes packageId and version in the result", () => {
231
231
  const cid = (0, utils_1.newid)();
232
232
  const result = (0, builder_1.definePackage)((pkg) => {
233
233
  pkg.packageId = TEST_PKG_ID;
234
234
  pkg.version = "1.2.3";
235
- pkg.versionTag = "dev";
236
235
  const c = pkg.contract(cid, 1, "C");
237
236
  c.tables = [
238
237
  {
@@ -247,9 +246,8 @@ describe("definePackage", () => {
247
246
  });
248
247
  expect(result.packageId).toBe(TEST_PKG_ID);
249
248
  expect(result.version).toBe("1.2.3");
250
- expect(result.versionTag).toBe("dev");
251
249
  });
252
- it("leaves version and versionTag undefined when not set", () => {
250
+ it("leaves version undefined when not set", () => {
253
251
  const cid = (0, utils_1.newid)();
254
252
  const result = (0, builder_1.definePackage)((pkg) => {
255
253
  pkg.packageId = TEST_PKG_ID;
@@ -267,7 +265,6 @@ describe("definePackage", () => {
267
265
  });
268
266
  expect(result.packageId).toBe(TEST_PKG_ID);
269
267
  expect(result.version).toBeUndefined();
270
- expect(result.versionTag).toBeUndefined();
271
268
  });
272
269
  });
273
270
  describe("definePackage — error cases", () => {
@@ -266,7 +266,7 @@ describe("Package Contracts — integration", () => {
266
266
  // Define a broken fancy tasks that is missing the taskCount observable
267
267
  const brokenFancy = (0, builder_1.definePackage)((pkg) => {
268
268
  pkg.packageId = (0, utils_1.newid)();
269
- const fancy = pkg.contract(FANCY_TASKS_CONTRACT_ID, 1, "Broken Fancy Tasks", "dev");
269
+ const fancy = pkg.contract(FANCY_TASKS_CONTRACT_ID, 1, "Broken Fancy Tasks");
270
270
  fancy.alsoImplements(TASKS_CONTRACT_ID, 1);
271
271
  fancy.tables = [
272
272
  {
@@ -11,7 +11,6 @@ export declare class ContractBuilder {
11
11
  readonly version: number;
12
12
  readonly name: string;
13
13
  readonly description?: string | undefined;
14
- readonly devTag?: "dev" | undefined;
15
14
  private _tables;
16
15
  private _tools;
17
16
  private _observables;
@@ -20,7 +19,7 @@ export declare class ContractBuilder {
20
19
  private _toolInstances;
21
20
  /** Full table definitions for this contract (used by the install flow). */
22
21
  private _tableDefinitions;
23
- constructor(contractId: string, version: number, name: string, description?: string | undefined, devTag?: "dev" | undefined);
22
+ constructor(contractId: string, version: number, name: string, description?: string | undefined);
24
23
  set tables(defs: IExtractableTableDef[]);
25
24
  get tables(): IExtractableTableDef[];
26
25
  set tools(instances: IExtractableToolInstance[]);
@@ -61,7 +60,6 @@ export declare class ContractBuilder {
61
60
  export declare class PackageBuilder {
62
61
  private _packageId?;
63
62
  private _version?;
64
- private _versionTag?;
65
63
  private _contracts;
66
64
  private _consumes;
67
65
  /** Package-level assistants (not part of any contract). */
@@ -74,19 +72,19 @@ export declare class PackageBuilder {
74
72
  /** Semantic version string (e.g. "1.0.0"). Typically imported from package.json at build time. */
75
73
  set version(v: string);
76
74
  get version(): string | undefined;
77
- /** Version channel tag (e.g. "dev", "beta", "stable"). */
78
- set versionTag(tag: string);
79
- get versionTag(): string | undefined;
80
75
  /**
81
76
  * Define (and implement) a contract at the given version.
82
77
  * Returns a `ContractBuilder` for assigning tables, tools, and observables.
83
78
  *
79
+ * Contracts are always registered as `devTag: "dev"` from code. The platform
80
+ * finalizes contracts (removes devTag) when the package version is promoted
81
+ * to stable. Previously-frozen contracts are preserved by the installer.
82
+ *
84
83
  * @param contractId - Must be a valid peer ID (25-char alphanumeric from `newid()`).
85
84
  * @param version - Positive integer version number.
86
85
  * @param name - Human-readable contract name. This is the meaningful identifier that creators can change over time.
87
- * @param devTag - Set to `"dev"` while the contract shape is still mutable.
88
86
  */
89
- contract(contractId: string, version: number, name: string, devTag?: "dev"): ContractBuilder;
87
+ contract(contractId: string, version: number, name: string): ContractBuilder;
90
88
  /**
91
89
  * Declare a dependency on another package's contract.
92
90
  */
@@ -14,7 +14,6 @@ class ContractBuilder {
14
14
  version;
15
15
  name;
16
16
  description;
17
- devTag;
18
17
  _tables = [];
19
18
  _tools = [];
20
19
  _observables = [];
@@ -23,12 +22,11 @@ class ContractBuilder {
23
22
  _toolInstances = [];
24
23
  /** Full table definitions for this contract (used by the install flow). */
25
24
  _tableDefinitions = [];
26
- constructor(contractId, version, name, description, devTag) {
25
+ constructor(contractId, version, name, description) {
27
26
  this.contractId = contractId;
28
27
  this.version = version;
29
28
  this.name = name;
30
29
  this.description = description;
31
- this.devTag = devTag;
32
30
  }
33
31
  set tables(defs) {
34
32
  this._tables = defs;
@@ -78,7 +76,7 @@ class ContractBuilder {
78
76
  definition: {
79
77
  contractId: this.contractId,
80
78
  version: this.version,
81
- devTag: this.devTag,
79
+ devTag: "dev",
82
80
  name: this.name,
83
81
  description: this.description ?? "",
84
82
  tables: this._tables.map(extract_1.extractTableShape),
@@ -104,7 +102,6 @@ exports.ContractBuilder = ContractBuilder;
104
102
  class PackageBuilder {
105
103
  _packageId;
106
104
  _version;
107
- _versionTag;
108
105
  _contracts = [];
109
106
  _consumes = [];
110
107
  /** Package-level assistants (not part of any contract). */
@@ -128,23 +125,19 @@ class PackageBuilder {
128
125
  get version() {
129
126
  return this._version;
130
127
  }
131
- /** Version channel tag (e.g. "dev", "beta", "stable"). */
132
- set versionTag(tag) {
133
- this._versionTag = tag;
134
- }
135
- get versionTag() {
136
- return this._versionTag;
137
- }
138
128
  /**
139
129
  * Define (and implement) a contract at the given version.
140
130
  * Returns a `ContractBuilder` for assigning tables, tools, and observables.
141
131
  *
132
+ * Contracts are always registered as `devTag: "dev"` from code. The platform
133
+ * finalizes contracts (removes devTag) when the package version is promoted
134
+ * to stable. Previously-frozen contracts are preserved by the installer.
135
+ *
142
136
  * @param contractId - Must be a valid peer ID (25-char alphanumeric from `newid()`).
143
137
  * @param version - Positive integer version number.
144
138
  * @param name - Human-readable contract name. This is the meaningful identifier that creators can change over time.
145
- * @param devTag - Set to `"dev"` while the contract shape is still mutable.
146
139
  */
147
- contract(contractId, version, name, devTag) {
140
+ contract(contractId, version, name) {
148
141
  if (!(0, utils_1.isid)(contractId)) {
149
142
  throw new Error(`contractId must be a valid peer ID (25-char alphanumeric), got "${contractId}"`);
150
143
  }
@@ -155,7 +148,7 @@ class PackageBuilder {
155
148
  if (existing) {
156
149
  throw new Error(`Duplicate contract: ${contractId} v${version} is already defined in this package`);
157
150
  }
158
- const builder = new ContractBuilder(contractId, version, name, undefined, devTag);
151
+ const builder = new ContractBuilder(contractId, version, name);
159
152
  this._contracts.push(builder);
160
153
  return builder;
161
154
  }
@@ -192,7 +185,6 @@ class PackageBuilder {
192
185
  return {
193
186
  packageId: this._packageId,
194
187
  version: this._version,
195
- versionTag: this._versionTag,
196
188
  contracts,
197
189
  consumes: [...this._consumes],
198
190
  alsoImplements: alsoImplementsMap,
@@ -16,15 +16,15 @@ declare const schema: z.ZodObject<{
16
16
  registeredAt: z.ZodString;
17
17
  }, "strip", z.ZodTypeAny, {
18
18
  version: number;
19
- contractProviderId: string;
20
19
  contractId: string;
20
+ contractProviderId: string;
21
21
  providerPackageId: string;
22
22
  isActive: boolean;
23
23
  registeredAt: string;
24
24
  }, {
25
25
  version: number;
26
- contractProviderId: string;
27
26
  contractId: string;
27
+ contractProviderId: string;
28
28
  providerPackageId: string;
29
29
  isActive: boolean;
30
30
  registeredAt: string;
@@ -41,6 +41,10 @@ export declare class ContractRegistry {
41
41
  getProviderPackageId(contractId: string, version: number): string | undefined;
42
42
  /** Get the stored contract definition (regardless of active provider). */
43
43
  getDefinition(contractId: string, version: number): IContractDefinition | undefined;
44
+ /** Replace the stored definition for a contract (e.g. to freeze a dev contract). */
45
+ updateDefinition(contractId: string, version: number, definition: IContractDefinition): void;
46
+ /** Get all contract keys provided by a package. */
47
+ getContractKeysForPackage(packageId: string): string[];
44
48
  /**
45
49
  * Remove all contracts provided by a package. If the removed package was
46
50
  * the active provider for a contract, the next registered provider (if any)
@@ -102,6 +102,22 @@ class ContractRegistry {
102
102
  getDefinition(contractId, version) {
103
103
  return this.definitions.get((0, types_1.contractKey)(contractId, version));
104
104
  }
105
+ /** Replace the stored definition for a contract (e.g. to freeze a dev contract). */
106
+ updateDefinition(contractId, version, definition) {
107
+ const key = (0, types_1.contractKey)(contractId, version);
108
+ this.definitions.set(key, definition);
109
+ // Also update any active/all providers that reference this definition
110
+ const providers = this.allProviders.get(key);
111
+ if (providers) {
112
+ for (const p of providers) {
113
+ p.definition = definition;
114
+ }
115
+ }
116
+ }
117
+ /** Get all contract keys provided by a package. */
118
+ getContractKeysForPackage(packageId) {
119
+ return this.packageContracts.get(packageId) ?? [];
120
+ }
105
121
  /**
106
122
  * Remove all contracts provided by a package. If the removed package was
107
123
  * the active provider for a contract, the next registered provider (if any)
@@ -64,8 +64,6 @@ export interface IPackageDefinitionResult {
64
64
  packageId: string;
65
65
  /** Semantic version string baked in from package.json at build time. */
66
66
  version?: string;
67
- /** Version channel tag (e.g. "dev", "beta", "stable"). */
68
- versionTag?: string;
69
67
  /** Contracts this package defines and provides. */
70
68
  contracts: IContractDefinition[];
71
69
  /** Contracts this package depends on. */
@@ -1,7 +1,14 @@
1
1
  import type { IPackage } from "./packages";
2
2
  /**
3
- * Verifies that a package update signature is valid, throws on invalid signature
3
+ * Verifies that a package update signature is valid.
4
+ *
5
+ * Writers can create or update packages that have a dev active version.
6
+ * Admins are required for packages with beta or stable active versions.
7
+ *
4
8
  * @param packageObj The package record to verify
9
+ * @param opts.isDevVersion When true, allows Writer role (for dev version operations)
5
10
  * @throws Error if signature is invalid or unauthorized
6
11
  */
7
- export declare function verifyPackageSignature(packageObj: IPackage, groupId: string): Promise<void>;
12
+ export declare function verifyPackageSignature(packageObj: IPackage, groupId: string, opts?: {
13
+ isDevVersion?: boolean;
14
+ }): Promise<void>;
@@ -4,15 +4,23 @@ exports.verifyPackageSignature = verifyPackageSignature;
4
4
  const keys_1 = require("../keys");
5
5
  const group_permissions_1 = require("./group-permissions");
6
6
  /**
7
- * Verifies that a package update signature is valid, throws on invalid signature
7
+ * Verifies that a package update signature is valid.
8
+ *
9
+ * Writers can create or update packages that have a dev active version.
10
+ * Admins are required for packages with beta or stable active versions.
11
+ *
8
12
  * @param packageObj The package record to verify
13
+ * @param opts.isDevVersion When true, allows Writer role (for dev version operations)
9
14
  * @throws Error if signature is invalid or unauthorized
10
15
  */
11
- async function verifyPackageSignature(packageObj, groupId) {
16
+ async function verifyPackageSignature(packageObj, groupId, opts) {
12
17
  (0, keys_1.verifyObjectSignature)(packageObj);
13
18
  const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageObj) ?? "";
14
19
  const signerRole = await (0, group_permissions_1.getUserRoleFromPublicKey)(groupId, signerPublicKey);
15
- if (signerRole < group_permissions_1.GroupMemberRole.Admin) {
16
- throw new Error("Only group admins can create or update packages");
20
+ const requiredRole = opts?.isDevVersion ? group_permissions_1.GroupMemberRole.Writer : group_permissions_1.GroupMemberRole.Admin;
21
+ if (signerRole < requiredRole) {
22
+ throw new Error(opts?.isDevVersion
23
+ ? "Only group writers or above can create or update dev packages"
24
+ : "Only group admins can create or update packages");
17
25
  }
18
26
  }
@@ -1,7 +1,14 @@
1
+ import { GroupMemberRole } from "./group-permissions";
1
2
  import type { IPackageVersion } from "./package-versions";
2
3
  /**
3
- * Verifies that a package version update signature is valid, throws on invalid signature
4
+ * Verifies that a package version update signature is valid.
5
+ *
6
+ * Dev versions (`versionTag === "dev"`) require Writer role.
7
+ * Beta and stable versions require Admin role.
8
+ *
4
9
  * @param packageVersion The package version record to verify
10
+ * @param groupId The group context to check roles against
11
+ * @param signerRole Optional pre-resolved role (skips `getUserRoleFromPublicKey` lookup; useful in tests)
5
12
  * @throws Error if signature is invalid or unauthorized
6
13
  */
7
- export declare function verifyPackageVersionSignature(packageVersion: IPackageVersion, groupId: string): Promise<void>;
14
+ export declare function verifyPackageVersionSignature(packageVersion: IPackageVersion, groupId: string, signerRole?: GroupMemberRole): Promise<void>;
@@ -4,15 +4,27 @@ exports.verifyPackageVersionSignature = verifyPackageVersionSignature;
4
4
  const keys_1 = require("../keys");
5
5
  const group_permissions_1 = require("./group-permissions");
6
6
  /**
7
- * Verifies that a package version update signature is valid, throws on invalid signature
7
+ * Verifies that a package version update signature is valid.
8
+ *
9
+ * Dev versions (`versionTag === "dev"`) require Writer role.
10
+ * Beta and stable versions require Admin role.
11
+ *
8
12
  * @param packageVersion The package version record to verify
13
+ * @param groupId The group context to check roles against
14
+ * @param signerRole Optional pre-resolved role (skips `getUserRoleFromPublicKey` lookup; useful in tests)
9
15
  * @throws Error if signature is invalid or unauthorized
10
16
  */
11
- async function verifyPackageVersionSignature(packageVersion, groupId) {
17
+ async function verifyPackageVersionSignature(packageVersion, groupId, signerRole) {
12
18
  (0, keys_1.verifyObjectSignature)(packageVersion);
13
- const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageVersion) ?? "";
14
- const signerRole = await (0, group_permissions_1.getUserRoleFromPublicKey)(groupId, signerPublicKey);
15
- if (signerRole < group_permissions_1.GroupMemberRole.Admin) {
16
- throw new Error("Only group admins can create or update package versions");
19
+ if (signerRole === undefined) {
20
+ const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageVersion) ?? "";
21
+ signerRole = await (0, group_permissions_1.getUserRoleFromPublicKey)(groupId, signerPublicKey);
22
+ }
23
+ const isDevVersion = packageVersion.versionTag === "dev";
24
+ const requiredRole = isDevVersion ? group_permissions_1.GroupMemberRole.Writer : group_permissions_1.GroupMemberRole.Admin;
25
+ if (signerRole < requiredRole) {
26
+ throw new Error(isDevVersion
27
+ ? "Only group writers or above can create dev package versions"
28
+ : "Only group admins can create or update beta/stable package versions");
17
29
  }
18
30
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const keys_1 = require("../keys");
4
+ const utils_1 = require("../utils");
5
+ const group_member_roles_1 = require("./group-member-roles");
6
+ const package_version_permissions_1 = require("./package-version-permissions");
7
+ describe("Package Version Permissions", () => {
8
+ const writerKeys = (0, keys_1.newKeys)();
9
+ const adminKeys = (0, keys_1.newKeys)();
10
+ function makePV(overrides = {}) {
11
+ return {
12
+ packageVersionId: (0, utils_1.newid)(),
13
+ packageId: (0, utils_1.newid)(),
14
+ version: "1.0.0",
15
+ versionTag: "dev",
16
+ packageVersionHash: "testhash",
17
+ packageBundleFileId: (0, utils_1.newid)(),
18
+ packageBundleFileHash: "bundlehash",
19
+ signature: "",
20
+ createdBy: (0, utils_1.newid)(),
21
+ createdAt: new Date().toISOString(),
22
+ ...overrides,
23
+ };
24
+ }
25
+ describe("dev versions", () => {
26
+ it("allows Writer to sign a dev version", async () => {
27
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
28
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Writer)).resolves.toBeUndefined();
29
+ });
30
+ it("allows Admin to sign a dev version", async () => {
31
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), adminKeys.secretKey);
32
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
33
+ });
34
+ it("rejects Reader signing a dev version", async () => {
35
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
36
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Reader)).rejects.toThrow("Only group writers or above");
37
+ });
38
+ });
39
+ describe("beta versions", () => {
40
+ it("allows Admin to sign a beta version", async () => {
41
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "beta" }), adminKeys.secretKey);
42
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
43
+ });
44
+ it("allows Owner to sign a beta version", async () => {
45
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "beta" }), adminKeys.secretKey);
46
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Owner)).resolves.toBeUndefined();
47
+ });
48
+ it("rejects Writer signing a beta version", async () => {
49
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "beta" }), writerKeys.secretKey);
50
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
51
+ });
52
+ });
53
+ describe("stable versions", () => {
54
+ it("allows Admin to sign a stable version", async () => {
55
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "stable" }), adminKeys.secretKey);
56
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
57
+ });
58
+ it("rejects Writer signing a stable version", async () => {
59
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "stable" }), writerKeys.secretKey);
60
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
61
+ });
62
+ });
63
+ describe("promotion scenarios (dev → beta → stable)", () => {
64
+ it("rejects Writer promoting dev to beta", async () => {
65
+ const devPV = makePV({ versionTag: "dev" });
66
+ const promoted = { ...devPV, versionTag: "beta" };
67
+ const signed = (0, keys_1.addSignatureToObject)(promoted, writerKeys.secretKey);
68
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
69
+ });
70
+ it("rejects Writer promoting dev to stable", async () => {
71
+ const devPV = makePV({ versionTag: "dev" });
72
+ const promoted = { ...devPV, versionTag: "stable" };
73
+ const signed = (0, keys_1.addSignatureToObject)(promoted, writerKeys.secretKey);
74
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
75
+ });
76
+ it("rejects Writer promoting beta to stable", async () => {
77
+ const betaPV = makePV({ versionTag: "beta" });
78
+ const promoted = { ...betaPV, versionTag: "stable" };
79
+ const signed = (0, keys_1.addSignatureToObject)(promoted, writerKeys.secretKey);
80
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Writer)).rejects.toThrow("Only group admins can create or update beta/stable");
81
+ });
82
+ it("allows Admin promoting dev to beta", async () => {
83
+ const devPV = makePV({ versionTag: "dev" });
84
+ const promoted = { ...devPV, versionTag: "beta" };
85
+ const signed = (0, keys_1.addSignatureToObject)(promoted, adminKeys.secretKey);
86
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
87
+ });
88
+ it("allows Admin promoting beta to stable", async () => {
89
+ const betaPV = makePV({ versionTag: "beta" });
90
+ const promoted = { ...betaPV, versionTag: "stable" };
91
+ const signed = (0, keys_1.addSignatureToObject)(promoted, adminKeys.secretKey);
92
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(signed, "test-group", group_member_roles_1.GroupMemberRole.Admin)).resolves.toBeUndefined();
93
+ });
94
+ });
95
+ describe("signature tampering", () => {
96
+ it("rejects a tampered package version", async () => {
97
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
98
+ pv.version = "9.9.9";
99
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).rejects.toThrow();
100
+ });
101
+ it("rejects a version with tag changed after signing", async () => {
102
+ const pv = (0, keys_1.addSignatureToObject)(makePV({ versionTag: "dev" }), writerKeys.secretKey);
103
+ pv.versionTag = "stable";
104
+ await expect((0, package_version_permissions_1.verifyPackageVersionSignature)(pv, "test-group", group_member_roles_1.GroupMemberRole.Admin)).rejects.toThrow();
105
+ });
106
+ });
107
+ });
@@ -30,6 +30,22 @@ declare const schema: z.ZodObject<{
30
30
  navigationPath: string;
31
31
  displayName?: string | undefined;
32
32
  }>, "many">>;
33
+ history: z.ZodOptional<z.ZodArray<z.ZodObject<{
34
+ action: z.ZodString;
35
+ by: z.ZodString;
36
+ at: z.ZodString;
37
+ signature: z.ZodString;
38
+ }, "strip", z.ZodTypeAny, {
39
+ at: string;
40
+ action: string;
41
+ signature: string;
42
+ by: string;
43
+ }, {
44
+ at: string;
45
+ action: string;
46
+ signature: string;
47
+ by: string;
48
+ }>, "many">>;
33
49
  signature: z.ZodString;
34
50
  createdBy: z.ZodEffects<z.ZodString, string, string>;
35
51
  createdAt: z.ZodString;
@@ -43,6 +59,12 @@ declare const schema: z.ZodObject<{
43
59
  packageBundleFileHash: string;
44
60
  createdBy: string;
45
61
  createdAt: string;
62
+ history?: {
63
+ at: string;
64
+ action: string;
65
+ signature: string;
66
+ by: string;
67
+ }[] | undefined;
46
68
  versionTag?: string | undefined;
47
69
  routesBundleFileId?: string | undefined;
48
70
  routesBundleFileHash?: string | undefined;
@@ -64,6 +86,12 @@ declare const schema: z.ZodObject<{
64
86
  packageBundleFileHash: string;
65
87
  createdBy: string;
66
88
  createdAt: string;
89
+ history?: {
90
+ at: string;
91
+ action: string;
92
+ signature: string;
93
+ by: string;
94
+ }[] | undefined;
67
95
  versionTag?: string | undefined;
68
96
  routesBundleFileId?: string | undefined;
69
97
  routesBundleFileHash?: string | undefined;
@@ -88,13 +116,25 @@ export declare class PackageVersionsTable extends Table<IPackageVersion> {
88
116
  }
89
117
  export declare function PackageVersions(dataContext?: DataContext): PackageVersionsTable;
90
118
  /**
91
- * Compute a hash combining the bundle file hashes to uniquely identify this version's content.
119
+ * Compute a content hash from bundle file hashes. The hash represents code
120
+ * content only — `versionTag` is intentionally excluded so the same code
121
+ * produces the same hash regardless of its promotion level (dev/beta/stable).
122
+ *
123
+ * @deprecated The `versionTag` parameter is accepted for backward compatibility
124
+ * but ignored. It will be removed in a future release.
92
125
  */
93
- export declare function computePackageVersionHash(version: string, versionTag: string, packageBundleFileHash: string, routesBundleFileHash?: string, uiBundleFileHash?: string): string;
126
+ export declare function computePackageVersionHash(version: string, _versionTag: string, packageBundleFileHash: string, routesBundleFileHash?: string, uiBundleFileHash?: string): string;
94
127
  export declare function isVersionInRange(activeVersion: string, incomingVersion: string, range: "pinned" | "patch" | "minor" | "latest"): boolean;
95
128
  /**
96
129
  * Returns true if incomingVersion is strictly newer than activeVersion (semver comparison).
97
130
  */
98
131
  export declare function isNewerVersion(activeVersion: string, incomingVersion: string): boolean;
132
+ /**
133
+ * Determines whether an incoming version tag matches the device's follow policy.
134
+ *
135
+ * `"dev"` tags are **never** auto-matched unless the device explicitly opts in
136
+ * via `deviceVersionTag: "dev"`. This prevents local development builds from
137
+ * auto-activating on other devices in the group.
138
+ */
99
139
  export declare function doesTagMatch(activeVersionTag: string | undefined, incomingVersionTag: string | undefined, followVersionTags: string | undefined, deviceVersionTag: string | undefined): boolean;
100
140
  export {};
@@ -66,6 +66,15 @@ const schema = zod_1.z.object({
66
66
  .array()
67
67
  .optional()
68
68
  .describe("The app navigation items that this version provides"),
69
+ history: zod_1.z
70
+ .array(zod_1.z.object({
71
+ action: zod_1.z.string().describe("created | promoted:beta | promoted:stable | activated"),
72
+ by: zod_1.z.string().describe("Peer ID of the actor"),
73
+ at: zod_1.z.string().describe("ISO 8601 timestamp"),
74
+ signature: zod_1.z.string().describe("Actor's signature over the action"),
75
+ }))
76
+ .optional()
77
+ .describe("Audit trail of lifecycle events for this version"),
69
78
  signature: zod_1.z.string().describe("The signed hash of this data excluding the signature itself"),
70
79
  createdBy: zod_types_1.zodPeerId.describe("The user who created this version"),
71
80
  createdAt: zod_1.z.string().describe("ISO timestamp of when this version was created"),
@@ -104,14 +113,7 @@ let PackageVersionsTable = (() => {
104
113
  // users can do whatever they want to package versions in their personal space
105
114
  return super.save(packageVersion, opts);
106
115
  }
107
- try {
108
- await (0, package_version_permissions_1.verifyPackageVersionSignature)(packageVersion, this.groupId);
109
- }
110
- catch (err) {
111
- throw new Error("Package version verification failed. Did you mean to call `signAndSave`?", {
112
- cause: err,
113
- });
114
- }
116
+ await (0, package_version_permissions_1.verifyPackageVersionSignature)(packageVersion, this.groupId);
115
117
  return super.save(packageVersion, opts);
116
118
  }
117
119
  async signAndSave(packageVersion, opts) {
@@ -119,7 +121,7 @@ let PackageVersionsTable = (() => {
119
121
  throw new Error("Package version signing is not enabled. Call PackageVersionsTable.enablePackageVersionSigning(fn) to enable it.");
120
122
  }
121
123
  packageVersion = await PackageVersionsTable.addSignatureToPackageVersion(packageVersion);
122
- return super.save(packageVersion, opts);
124
+ return this.save(packageVersion, opts);
123
125
  }
124
126
  static addSignatureToPackageVersion = undefined;
125
127
  static enablePackageVersionSigning(fn) {
@@ -137,16 +139,15 @@ function PackageVersions(dataContext) {
137
139
  return (0, context_1.getTableContainer)(dataContext).getTable(metaData, schema, PackageVersionsTable);
138
140
  }
139
141
  /**
140
- * Compute a hash combining the bundle file hashes to uniquely identify this version's content.
142
+ * Compute a content hash from bundle file hashes. The hash represents code
143
+ * content only — `versionTag` is intentionally excluded so the same code
144
+ * produces the same hash regardless of its promotion level (dev/beta/stable).
145
+ *
146
+ * @deprecated The `versionTag` parameter is accepted for backward compatibility
147
+ * but ignored. It will be removed in a future release.
141
148
  */
142
- function computePackageVersionHash(version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) {
143
- return (0, keys_1.hashValue)([
144
- version,
145
- versionTag,
146
- packageBundleFileHash,
147
- routesBundleFileHash ?? "",
148
- uiBundleFileHash ?? "",
149
- ].join(":"));
149
+ function computePackageVersionHash(version, _versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) {
150
+ return (0, keys_1.hashValue)([version, packageBundleFileHash, routesBundleFileHash ?? "", uiBundleFileHash ?? ""].join(":"));
150
151
  }
151
152
  function isVersionInRange(activeVersion, incomingVersion, range) {
152
153
  if (range === "pinned")
@@ -173,10 +174,21 @@ function isNewerVersion(activeVersion, incomingVersion) {
173
174
  return iMin > aMin;
174
175
  return iPat > aPat;
175
176
  }
177
+ /**
178
+ * Determines whether an incoming version tag matches the device's follow policy.
179
+ *
180
+ * `"dev"` tags are **never** auto-matched unless the device explicitly opts in
181
+ * via `deviceVersionTag: "dev"`. This prevents local development builds from
182
+ * auto-activating on other devices in the group.
183
+ */
176
184
  function doesTagMatch(activeVersionTag, incomingVersionTag, followVersionTags, deviceVersionTag) {
177
185
  const inTag = incomingVersionTag || "stable";
186
+ // Device-level override: only match the exact tag the device requested
178
187
  if (deviceVersionTag)
179
188
  return inTag === deviceVersionTag;
189
+ // Dev versions never auto-activate unless explicitly followed
190
+ if (inTag === "dev")
191
+ return false;
180
192
  if (!followVersionTags) {
181
193
  return inTag === (activeVersionTag || "stable");
182
194
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SQLiteDB = require("better-sqlite3");
4
+ const user_context_1 = require("../context/user-context");
5
+ const user_context_singleton_1 = require("../context/user-context-singleton");
6
+ const keys_1 = require("../keys");
7
+ const utils_1 = require("../utils");
8
+ const group_member_roles_1 = require("./group-member-roles");
9
+ const group_members_1 = require("./group-members");
10
+ const groups_1 = require("./groups");
11
+ const sql_data_source_1 = require("./orm/sql.data-source");
12
+ const package_versions_1 = require("./package-versions");
13
+ const users_1 = require("./users");
14
+ // ---------------------------------------------------------------------------
15
+ // In-memory SQLite harness (same pattern as sql.data-source.test.ts)
16
+ // ---------------------------------------------------------------------------
17
+ class DBHarness {
18
+ _db = null;
19
+ get db() {
20
+ if (!this._db) {
21
+ this._db = new SQLiteDB(":memory:");
22
+ this._db.pragma("journal_mode = WAL");
23
+ }
24
+ return this._db;
25
+ }
26
+ async get(sql, params = []) {
27
+ return this.db.prepare(sql).get(params);
28
+ }
29
+ async all(sql, params = []) {
30
+ return this.db.prepare(sql).all(params);
31
+ }
32
+ async exec(sql, params = []) {
33
+ const result = this.db.prepare(sql).run(params);
34
+ return { changes: result.changes };
35
+ }
36
+ async close() {
37
+ this._db?.close();
38
+ this._db = null;
39
+ }
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Test fixtures
43
+ // ---------------------------------------------------------------------------
44
+ const writerKeys = (0, keys_1.newKeys)();
45
+ const adminKeys = (0, keys_1.newKeys)();
46
+ const testGroupId = (0, utils_1.newid)();
47
+ const writerUserId = (0, utils_1.newid)();
48
+ const adminUserId = (0, utils_1.newid)();
49
+ function makePV(overrides = {}) {
50
+ return {
51
+ packageVersionId: (0, utils_1.newid)(),
52
+ packageId: (0, utils_1.newid)(),
53
+ version: "1.0.0",
54
+ versionTag: "dev",
55
+ packageVersionHash: "testhash",
56
+ packageBundleFileId: (0, utils_1.newid)(),
57
+ packageBundleFileHash: "bundlehash",
58
+ signature: "",
59
+ createdBy: writerUserId,
60
+ createdAt: new Date().toISOString(),
61
+ ...overrides,
62
+ };
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Setup: create a real ephemeral UserContext with Users + GroupMembers seeded
66
+ // so that getUserRoleFromPublicKey resolves Writer vs Admin from real data.
67
+ // ---------------------------------------------------------------------------
68
+ let userContext;
69
+ beforeAll(async () => {
70
+ const db = new DBHarness();
71
+ const dataSourceFactory = (metaData, schema) => {
72
+ return new sql_data_source_1.SQLDataSource(db, metaData, schema);
73
+ };
74
+ userContext = new user_context_1.UserContext(writerUserId, dataSourceFactory, true);
75
+ await userContext.loadingPromise;
76
+ userContext.deviceId((0, utils_1.newid)());
77
+ (0, user_context_singleton_1.setUserContext)(userContext);
78
+ const dc = userContext.userDataContext;
79
+ // Enable passthrough so seeding skips signature checks on Groups/GroupMembers/Users
80
+ groups_1.GroupsTable.isPassthrough = true;
81
+ group_members_1.GroupMembersTable.isPassthrough = true;
82
+ users_1.UsersTable.isPassthrough = true;
83
+ // Seed the writer user
84
+ const { Users } = await Promise.resolve().then(() => require("./users"));
85
+ const writerUser = {
86
+ userId: writerUserId,
87
+ name: "Writer User",
88
+ publicKey: writerKeys.publicKey,
89
+ publicBoxKey: "",
90
+ signature: "",
91
+ };
92
+ await Users(dc).save(writerUser);
93
+ // Seed the admin user
94
+ const adminUser = {
95
+ userId: adminUserId,
96
+ name: "Admin User",
97
+ publicKey: adminKeys.publicKey,
98
+ publicBoxKey: "",
99
+ signature: "",
100
+ };
101
+ await Users(dc).save(adminUser);
102
+ // Seed a group so the group lookup finds real data (not "group not found → Founder")
103
+ const { Groups } = await Promise.resolve().then(() => require("./groups"));
104
+ await Groups(dc).save({
105
+ groupId: testGroupId,
106
+ name: "Test Group",
107
+ description: "",
108
+ founderUserId: (0, utils_1.newid)(), // different from writer/admin so nobody is auto-Founder
109
+ signature: "",
110
+ publicKey: "",
111
+ publicBoxKey: "",
112
+ });
113
+ // Seed group memberships with real roles
114
+ const { GroupMembers } = await Promise.resolve().then(() => require("./group-members"));
115
+ const writerMembership = {
116
+ groupMemberId: (0, utils_1.newid)(),
117
+ groupId: testGroupId,
118
+ userId: writerUserId,
119
+ role: group_member_roles_1.GroupMemberRole.Writer,
120
+ signature: "",
121
+ };
122
+ await GroupMembers(dc).save(writerMembership);
123
+ const adminMembership = {
124
+ groupMemberId: (0, utils_1.newid)(),
125
+ groupId: testGroupId,
126
+ userId: adminUserId,
127
+ role: group_member_roles_1.GroupMemberRole.Admin,
128
+ signature: "",
129
+ };
130
+ await GroupMembers(dc).save(adminMembership);
131
+ // Disable passthrough so the real permission checks apply during tests
132
+ groups_1.GroupsTable.isPassthrough = false;
133
+ group_members_1.GroupMembersTable.isPassthrough = false;
134
+ users_1.UsersTable.isPassthrough = false;
135
+ // Enable real signing
136
+ package_versions_1.PackageVersionsTable.enablePackageVersionSigning((pv) => (0, keys_1.addSignatureToObject)(pv, activeSecretKey));
137
+ });
138
+ let activeSecretKey = writerKeys.secretKey;
139
+ // ---------------------------------------------------------------------------
140
+ // Tests
141
+ // ---------------------------------------------------------------------------
142
+ describe("PackageVersionsTable.signAndSave — promotion permission enforcement", () => {
143
+ // These tests exercise the real signAndSave → save → verifyPackageVersionSignature
144
+ // chain against a real in-memory database with real group membership data.
145
+ //
146
+ // THE BUG: signAndSave previously called super.save() which skipped the
147
+ // overridden save() and its verifyPackageVersionSignature check. A Writer
148
+ // could promote dev → beta/stable unchallenged.
149
+ //
150
+ // THE FIX: signAndSave now calls this.save(), so the permission check fires.
151
+ // Helper: get the PackageVersions table in the test group context
152
+ function pvTable() {
153
+ const dc = userContext.getDataContext(testGroupId);
154
+ return (0, package_versions_1.PackageVersions)(dc);
155
+ }
156
+ describe("Writer role", () => {
157
+ beforeEach(() => {
158
+ activeSecretKey = writerKeys.secretKey;
159
+ });
160
+ it("can signAndSave a dev version", async () => {
161
+ const pv = makePV({ versionTag: "dev" });
162
+ const saved = await pvTable().signAndSave(pv);
163
+ expect(saved.versionTag).toBe("dev");
164
+ expect(saved.signature).toBeTruthy();
165
+ });
166
+ it("is blocked from signAndSave with versionTag=beta", async () => {
167
+ const pv = makePV({ versionTag: "beta" });
168
+ // Before the fix: this would PASS (super.save skipped the check)
169
+ // After the fix: this correctly REJECTS
170
+ await expect(pvTable().signAndSave(pv)).rejects.toThrow("Only group admins can create or update beta/stable package versions");
171
+ });
172
+ it("is blocked from signAndSave with versionTag=stable", async () => {
173
+ const pv = makePV({ versionTag: "stable" });
174
+ await expect(pvTable().signAndSave(pv)).rejects.toThrow("Only group admins can create or update beta/stable package versions");
175
+ });
176
+ it("is blocked from promoting an existing dev version to beta", async () => {
177
+ const pv = makePV({ versionTag: "dev" });
178
+ const saved = await pvTable().signAndSave(pv);
179
+ const promoted = { ...saved, versionTag: "beta" };
180
+ await expect(pvTable().signAndSave(promoted)).rejects.toThrow("Only group admins can create or update beta/stable package versions");
181
+ });
182
+ });
183
+ describe("Admin role", () => {
184
+ beforeEach(() => {
185
+ activeSecretKey = adminKeys.secretKey;
186
+ });
187
+ it("can signAndSave a dev version", async () => {
188
+ const pv = makePV({ versionTag: "dev" });
189
+ const saved = await pvTable().signAndSave(pv);
190
+ expect(saved.versionTag).toBe("dev");
191
+ });
192
+ it("can signAndSave a beta version", async () => {
193
+ const pv = makePV({ versionTag: "beta" });
194
+ const saved = await pvTable().signAndSave(pv);
195
+ expect(saved.versionTag).toBe("beta");
196
+ });
197
+ it("can signAndSave a stable version", async () => {
198
+ const pv = makePV({ versionTag: "stable" });
199
+ const saved = await pvTable().signAndSave(pv);
200
+ expect(saved.versionTag).toBe("stable");
201
+ });
202
+ it("can promote an existing dev version to beta", async () => {
203
+ const pv = makePV({ versionTag: "dev" });
204
+ const saved = await pvTable().signAndSave(pv);
205
+ const promoted = { ...saved, versionTag: "beta" };
206
+ const result = await pvTable().signAndSave(promoted);
207
+ expect(result.versionTag).toBe("beta");
208
+ });
209
+ it("can promote an existing beta version to stable", async () => {
210
+ const pv = makePV({ versionTag: "beta" });
211
+ const saved = await pvTable().signAndSave(pv);
212
+ const promoted = { ...saved, versionTag: "stable" };
213
+ const result = await pvTable().signAndSave(promoted);
214
+ expect(result.versionTag).toBe("stable");
215
+ });
216
+ });
217
+ describe("personal space (no group)", () => {
218
+ it("allows Writer to signAndSave any tag without restriction", async () => {
219
+ activeSecretKey = writerKeys.secretKey;
220
+ const dc = userContext.userDataContext;
221
+ const table = (0, package_versions_1.PackageVersions)(dc);
222
+ const pv = makePV({ versionTag: "stable" });
223
+ const saved = await table.signAndSave(pv);
224
+ expect(saved.versionTag).toBe("stable");
225
+ });
226
+ });
227
+ });
@@ -106,14 +106,7 @@ let PackagesTable = (() => {
106
106
  // users can do whatever they want to packages in their personal space
107
107
  return super.save(packageObj, opts);
108
108
  }
109
- try {
110
- await (0, package_permissions_1.verifyPackageSignature)(packageObj, this.groupId);
111
- }
112
- catch (err) {
113
- throw new Error("Package verification failed. Did you mean to call `signAndSave`?", {
114
- cause: err,
115
- });
116
- }
109
+ await (0, package_permissions_1.verifyPackageSignature)(packageObj, this.groupId);
117
110
  return super.save(packageObj, opts);
118
111
  }
119
112
  async signAndSave(packageObj, opts) {
@@ -121,7 +114,7 @@ let PackagesTable = (() => {
121
114
  throw new Error("Package signing is not enabled. Call PackagesTable.enablePackageSigning(fn) to enable it.");
122
115
  }
123
116
  packageObj = await PackagesTable.addSignatureToPackage(packageObj);
124
- return super.save(packageObj, opts);
117
+ return this.save(packageObj, opts);
125
118
  }
126
119
  static addSignatureToPackage = undefined;
127
120
  static enablePackageSigning(fn) {
@@ -7,6 +7,9 @@ import type { IPackageDefinitionResult } from "../contracts/types";
7
7
  * Handles packages that export an `IPackageDefinitionResult` from `definePackage()`.
8
8
  * Registers contracts in the provided ContractRegistry, saves tools/assistants/workflows
9
9
  * with the `packageId` from the definition set, and registers table definitions.
10
+ *
11
+ * When a contract version was previously frozen (promoted to stable), the
12
+ * installer preserves its stable status rather than re-registering it as dev.
10
13
  */
11
14
  export declare function installContractPackage(dataContext: DataContext, packageDefinition: IPackageDefinitionResult, opts?: {
12
15
  registry?: ContractRegistry;
@@ -14,6 +17,9 @@ export declare function installContractPackage(dataContext: DataContext, package
14
17
  /**
15
18
  * Attempts to extract an `IPackageDefinitionResult` from a bundle's module exports.
16
19
  * Returns undefined if the bundle doesn't export a contract-based package definition.
20
+ *
21
+ * Matches any bundle that exports a `packageDefinition` with a valid `packageId`,
22
+ * including UI-only packages with zero contracts.
17
23
  */
18
24
  export declare function extractPackageDefinition(bundleExports: Record<string, unknown>): IPackageDefinitionResult | undefined;
19
25
  /**
@@ -21,3 +27,9 @@ export declare function extractPackageDefinition(bundleExports: Record<string, u
21
27
  * indicating it should be installed via the new contract-aware path.
22
28
  */
23
29
  export declare function hasPackageDefinition(bundleExports: Record<string, unknown>): boolean;
30
+ /**
31
+ * Finalize all dev contracts for a package when promoting to stable.
32
+ * Removes `devTag` from each contract in the registry, freezing their shapes.
33
+ * Returns the list of contract keys that were finalized.
34
+ */
35
+ export declare function finalizeContractsForPromotion(registry: ContractRegistry, packageId: string): string[];
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.installContractPackage = installContractPackage;
4
4
  exports.extractPackageDefinition = extractPackageDefinition;
5
5
  exports.hasPackageDefinition = hasPackageDefinition;
6
+ exports.finalizeContractsForPromotion = finalizeContractsForPromotion;
6
7
  const tools_1 = require("../data/tools");
7
8
  const tools_2 = require("../tools");
8
9
  /**
@@ -11,6 +12,9 @@ const tools_2 = require("../tools");
11
12
  * Handles packages that export an `IPackageDefinitionResult` from `definePackage()`.
12
13
  * Registers contracts in the provided ContractRegistry, saves tools/assistants/workflows
13
14
  * with the `packageId` from the definition set, and registers table definitions.
15
+ *
16
+ * When a contract version was previously frozen (promoted to stable), the
17
+ * installer preserves its stable status rather than re-registering it as dev.
14
18
  */
15
19
  async function installContractPackage(dataContext, packageDefinition, opts) {
16
20
  const { packageId } = packageDefinition;
@@ -20,7 +24,14 @@ async function installContractPackage(dataContext, packageDefinition, opts) {
20
24
  for (const contract of packageDefinition.contracts) {
21
25
  const key = `${contract.contractId}@${contract.version}`;
22
26
  const alsoImpl = packageDefinition.alsoImplements.get(key) ?? [];
23
- const result = registry.register(packageId, contract, alsoImpl);
27
+ // If the contract is already frozen in the registry, re-register as
28
+ // stable instead of dev to preserve immutability.
29
+ const existing = registry.getDefinition(contract.contractId, contract.version);
30
+ let effectiveContract = contract;
31
+ if (existing && !existing.devTag && contract.devTag === "dev") {
32
+ effectiveContract = { ...contract, devTag: undefined };
33
+ }
34
+ const result = registry.register(packageId, effectiveContract, alsoImpl);
24
35
  if (!result.valid) {
25
36
  console.error(`[ContractPackageLoader] Contract registration failed for ${contract.name}:`, result.errors);
26
37
  }
@@ -48,10 +59,13 @@ async function installContractPackage(dataContext, packageDefinition, opts) {
48
59
  /**
49
60
  * Attempts to extract an `IPackageDefinitionResult` from a bundle's module exports.
50
61
  * Returns undefined if the bundle doesn't export a contract-based package definition.
62
+ *
63
+ * Matches any bundle that exports a `packageDefinition` with a valid `packageId`,
64
+ * including UI-only packages with zero contracts.
51
65
  */
52
66
  function extractPackageDefinition(bundleExports) {
53
67
  const def = bundleExports?.packageDefinition;
54
- if (def && Array.isArray(def.contracts) && def.contracts.length > 0) {
68
+ if (def && typeof def.packageId === "string" && Array.isArray(def.contracts)) {
55
69
  return def;
56
70
  }
57
71
  return undefined;
@@ -63,3 +77,23 @@ function extractPackageDefinition(bundleExports) {
63
77
  function hasPackageDefinition(bundleExports) {
64
78
  return extractPackageDefinition(bundleExports) !== undefined;
65
79
  }
80
+ /**
81
+ * Finalize all dev contracts for a package when promoting to stable.
82
+ * Removes `devTag` from each contract in the registry, freezing their shapes.
83
+ * Returns the list of contract keys that were finalized.
84
+ */
85
+ function finalizeContractsForPromotion(registry, packageId) {
86
+ const finalized = [];
87
+ const contractKeys = registry.getContractKeysForPackage(packageId);
88
+ for (const key of contractKeys) {
89
+ const [contractId, versionStr] = key.split("@");
90
+ const version = Number(versionStr);
91
+ const def = registry.getDefinition(contractId, version);
92
+ if (def?.devTag === "dev") {
93
+ const frozenDef = { ...def, devTag: undefined };
94
+ registry.updateDefinition(contractId, version, frozenDef);
95
+ finalized.push(key);
96
+ }
97
+ }
98
+ return finalized;
99
+ }
@@ -21,7 +21,7 @@ export declare class PackageLoader {
21
21
  }): Promise<IPeersPackage | undefined>;
22
22
  private _readLocalBundle;
23
23
  /**
24
- * Post-correction: if the package definition carries version/versionTag,
24
+ * Post-correction: if the package definition carries a version string,
25
25
  * update the active IPackageVersion record to match. This fixes cases where
26
26
  * the PV record was created with stale or placeholder values (e.g. "0.0.1")
27
27
  * before the bundle was evaluated.
@@ -99,7 +99,7 @@ class PackageLoader {
99
99
  }
100
100
  }
101
101
  /**
102
- * Post-correction: if the package definition carries version/versionTag,
102
+ * Post-correction: if the package definition carries a version string,
103
103
  * update the active IPackageVersion record to match. This fixes cases where
104
104
  * the PV record was created with stale or placeholder values (e.g. "0.0.1")
105
105
  * before the bundle was evaluated.
@@ -107,28 +107,18 @@ class PackageLoader {
107
107
  correctPackageVersion(pkg, def) {
108
108
  if (!pkg.activePackageVersionId)
109
109
  return;
110
- if (!def.version && !def.versionTag)
110
+ if (!def.version)
111
111
  return;
112
112
  const pvTable = (0, package_versions_1.PackageVersions)(this.dataContext);
113
113
  pvTable.get(pkg.activePackageVersionId).then((pv) => {
114
114
  if (!pv)
115
115
  return;
116
- let needsUpdate = false;
117
- const updated = { ...pv };
118
116
  if (def.version && pv.version !== def.version) {
119
- updated.version = def.version;
120
- needsUpdate = true;
121
- }
122
- if (def.versionTag && pv.versionTag !== def.versionTag) {
123
- updated.versionTag = def.versionTag;
124
- needsUpdate = true;
125
- }
126
- if (needsUpdate) {
127
- pvTable.save(updated);
117
+ pvTable.save({ ...pv, version: def.version });
128
118
  }
129
119
  });
130
120
  }
131
- _evaluateBundle(pkg, bundleCode) {
121
+ async _evaluateBundle(pkg, bundleCode) {
132
122
  // Node.js built-in modules that do not exist in React Native (and would cause
133
123
  // a fatal native error if passed through to RN's require). We block them here
134
124
  // and throw a descriptive JS Error instead, which the surrounding try/catch
@@ -199,26 +189,34 @@ class PackageLoader {
199
189
  if (packageDefinition.packageId !== pkg.packageId) {
200
190
  console.warn(`[PackageLoader] packageId mismatch: definition has "${packageDefinition.packageId}" but DB record has "${pkg.packageId}"`);
201
191
  }
202
- (0, contract_package_loader_1.installContractPackage)(this.dataContext, packageDefinition);
192
+ await (0, contract_package_loader_1.installContractPackage)(this.dataContext, packageDefinition);
203
193
  this.contractInstalledPackages.add(packageDefinition.packageId);
204
194
  this.correctPackageVersion(pkg, packageDefinition);
195
+ // Build a proper IPeersPackage from the contract definition so callers
196
+ // (e.g. package-installer) can read appNavs, assistants, etc.
197
+ const contractPackageInstance = {
198
+ packageId: packageDefinition.packageId,
199
+ appNavs: packageDefinition.appNavs,
200
+ assistants: packageDefinition.assistants,
201
+ toolInstances: packageDefinition.toolInstances,
202
+ tableDefinitions: packageDefinition.tableDefinitions,
203
+ };
204
+ this.packageInstances[pkg.packageId] = contractPackageInstance;
205
+ return contractPackageInstance;
205
206
  }
206
207
  // Legacy path: extract IPeersPackage for backward compat
207
208
  const packageInstance = bundleExports?.exports || bundleExports?.package || bundleExports?.default || bundleExports;
208
209
  this.packageInstances[pkg.packageId] = packageInstance;
209
210
  if (packageInstance && typeof packageInstance === "object") {
210
- // Skip legacy tool/table registration if the contract path already handled it
211
- if (!packageDefinition) {
212
- packageInstance.toolInstances?.forEach((toolInstance) => {
213
- (0, tools_2.registerTool)(toolInstance);
214
- (0, tools_1.Tools)(this.dataContext).save(toolInstance.tool);
211
+ packageInstance.toolInstances?.forEach((toolInstance) => {
212
+ (0, tools_2.registerTool)(toolInstance);
213
+ (0, tools_1.Tools)(this.dataContext).save(toolInstance.tool);
214
+ });
215
+ packageInstance.tableDefinitions?.forEach((tableDefinition) => {
216
+ this.dataContext.tableContainer.registerTableDefinition(tableDefinition, {
217
+ overwrite: true,
215
218
  });
216
- packageInstance.tableDefinitions?.forEach((tableDefinition) => {
217
- this.dataContext.tableContainer.registerTableDefinition(tableDefinition, {
218
- overwrite: true,
219
- });
220
- });
221
- }
219
+ });
222
220
  return packageInstance;
223
221
  }
224
222
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"