@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.
- package/dist/contracts/__tests__/builder.test.js +5 -8
- package/dist/contracts/__tests__/integration.test.js +1 -1
- package/dist/contracts/builder.d.ts +6 -8
- package/dist/contracts/builder.js +8 -16
- package/dist/contracts/contract-providers.table.d.ts +2 -2
- package/dist/contracts/registry.d.ts +4 -0
- package/dist/contracts/registry.js +16 -0
- package/dist/contracts/types.d.ts +0 -2
- package/dist/data/package-permissions.d.ts +9 -2
- package/dist/data/package-permissions.js +12 -4
- package/dist/data/package-version-permissions.d.ts +9 -2
- package/dist/data/package-version-permissions.js +18 -6
- package/dist/data/package-version-permissions.test.d.ts +1 -0
- package/dist/data/package-version-permissions.test.js +107 -0
- package/dist/data/package-versions.d.ts +42 -2
- package/dist/data/package-versions.js +30 -18
- package/dist/data/package-versions.test.d.ts +1 -0
- package/dist/data/package-versions.test.js +227 -0
- package/dist/data/packages.js +2 -9
- package/dist/package-loader/contract-package-loader.d.ts +12 -0
- package/dist/package-loader/contract-package-loader.js +36 -2
- package/dist/package-loader/package-loader.d.ts +1 -1
- package/dist/package-loader/package-loader.js +24 -26
- package/package.json +1 -1
|
@@ -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).
|
|
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("
|
|
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"
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
+
});
|
package/dist/data/packages.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
this.dataContext.tableContainer.registerTableDefinition(tableDefinition, {
|
|
218
|
-
overwrite: true,
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
219
|
+
});
|
|
222
220
|
return packageInstance;
|
|
223
221
|
}
|
|
224
222
|
return;
|