@peers-app/peers-sdk 0.18.3 → 0.18.4

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,6 +1,10 @@
1
1
  import type { IPackageVersion } from "./package-versions";
2
2
  /**
3
- * Verifies that a package version update signature is valid, throws on invalid signature
3
+ * Verifies that a package version update signature is valid.
4
+ *
5
+ * Dev versions (`versionTag === "dev"`) require Writer role.
6
+ * Beta and stable versions require Admin role.
7
+ *
4
8
  * @param packageVersion The package version record to verify
5
9
  * @throws Error if signature is invalid or unauthorized
6
10
  */
@@ -4,7 +4,11 @@ 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
9
13
  * @throws Error if signature is invalid or unauthorized
10
14
  */
@@ -12,7 +16,11 @@ async function verifyPackageVersionSignature(packageVersion, groupId) {
12
16
  (0, keys_1.verifyObjectSignature)(packageVersion);
13
17
  const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageVersion) ?? "";
14
18
  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
+ const isDevVersion = packageVersion.versionTag === "dev";
20
+ const requiredRole = isDevVersion ? group_permissions_1.GroupMemberRole.Writer : group_permissions_1.GroupMemberRole.Admin;
21
+ if (signerRole < requiredRole) {
22
+ throw new Error(isDevVersion
23
+ ? "Only group writers or above can create dev package versions"
24
+ : "Only group admins can create or update beta/stable package versions");
17
25
  }
18
26
  }
@@ -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"),
@@ -137,16 +146,15 @@ function PackageVersions(dataContext) {
137
146
  return (0, context_1.getTableContainer)(dataContext).getTable(metaData, schema, PackageVersionsTable);
138
147
  }
139
148
  /**
140
- * Compute a hash combining the bundle file hashes to uniquely identify this version's content.
149
+ * Compute a content hash from bundle file hashes. The hash represents code
150
+ * content only — `versionTag` is intentionally excluded so the same code
151
+ * produces the same hash regardless of its promotion level (dev/beta/stable).
152
+ *
153
+ * @deprecated The `versionTag` parameter is accepted for backward compatibility
154
+ * but ignored. It will be removed in a future release.
141
155
  */
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(":"));
156
+ function computePackageVersionHash(version, _versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) {
157
+ return (0, keys_1.hashValue)([version, packageBundleFileHash, routesBundleFileHash ?? "", uiBundleFileHash ?? ""].join(":"));
150
158
  }
151
159
  function isVersionInRange(activeVersion, incomingVersion, range) {
152
160
  if (range === "pinned")
@@ -173,10 +181,21 @@ function isNewerVersion(activeVersion, incomingVersion) {
173
181
  return iMin > aMin;
174
182
  return iPat > aPat;
175
183
  }
184
+ /**
185
+ * Determines whether an incoming version tag matches the device's follow policy.
186
+ *
187
+ * `"dev"` tags are **never** auto-matched unless the device explicitly opts in
188
+ * via `deviceVersionTag: "dev"`. This prevents local development builds from
189
+ * auto-activating on other devices in the group.
190
+ */
176
191
  function doesTagMatch(activeVersionTag, incomingVersionTag, followVersionTags, deviceVersionTag) {
177
192
  const inTag = incomingVersionTag || "stable";
193
+ // Device-level override: only match the exact tag the device requested
178
194
  if (deviceVersionTag)
179
195
  return inTag === deviceVersionTag;
196
+ // Dev versions never auto-activate unless explicitly followed
197
+ if (inTag === "dev")
198
+ return false;
180
199
  if (!followVersionTags) {
181
200
  return inTag === (activeVersionTag || "stable");
182
201
  }
@@ -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.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"