@peers-app/peers-sdk 0.18.4 → 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.
@@ -1,3 +1,4 @@
1
+ import { GroupMemberRole } from "./group-permissions";
1
2
  import type { IPackageVersion } from "./package-versions";
2
3
  /**
3
4
  * Verifies that a package version update signature is valid.
@@ -6,6 +7,8 @@ import type { IPackageVersion } from "./package-versions";
6
7
  * Beta and stable versions require Admin role.
7
8
  *
8
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)
9
12
  * @throws Error if signature is invalid or unauthorized
10
13
  */
11
- export declare function verifyPackageVersionSignature(packageVersion: IPackageVersion, groupId: string): Promise<void>;
14
+ export declare function verifyPackageVersionSignature(packageVersion: IPackageVersion, groupId: string, signerRole?: GroupMemberRole): Promise<void>;
@@ -10,12 +10,16 @@ const group_permissions_1 = require("./group-permissions");
10
10
  * Beta and stable versions require Admin role.
11
11
  *
12
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)
13
15
  * @throws Error if signature is invalid or unauthorized
14
16
  */
15
- async function verifyPackageVersionSignature(packageVersion, groupId) {
17
+ async function verifyPackageVersionSignature(packageVersion, groupId, signerRole) {
16
18
  (0, keys_1.verifyObjectSignature)(packageVersion);
17
- const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageVersion) ?? "";
18
- const signerRole = await (0, group_permissions_1.getUserRoleFromPublicKey)(groupId, signerPublicKey);
19
+ if (signerRole === undefined) {
20
+ const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(packageVersion) ?? "";
21
+ signerRole = await (0, group_permissions_1.getUserRoleFromPublicKey)(groupId, signerPublicKey);
22
+ }
19
23
  const isDevVersion = packageVersion.versionTag === "dev";
20
24
  const requiredRole = isDevVersion ? group_permissions_1.GroupMemberRole.Writer : group_permissions_1.GroupMemberRole.Admin;
21
25
  if (signerRole < requiredRole) {
@@ -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
+ });
@@ -113,14 +113,7 @@ let PackageVersionsTable = (() => {
113
113
  // users can do whatever they want to package versions in their personal space
114
114
  return super.save(packageVersion, opts);
115
115
  }
116
- try {
117
- await (0, package_version_permissions_1.verifyPackageVersionSignature)(packageVersion, this.groupId);
118
- }
119
- catch (err) {
120
- throw new Error("Package version verification failed. Did you mean to call `signAndSave`?", {
121
- cause: err,
122
- });
123
- }
116
+ await (0, package_version_permissions_1.verifyPackageVersionSignature)(packageVersion, this.groupId);
124
117
  return super.save(packageVersion, opts);
125
118
  }
126
119
  async signAndSave(packageVersion, opts) {
@@ -128,7 +121,7 @@ let PackageVersionsTable = (() => {
128
121
  throw new Error("Package version signing is not enabled. Call PackageVersionsTable.enablePackageVersionSigning(fn) to enable it.");
129
122
  }
130
123
  packageVersion = await PackageVersionsTable.addSignatureToPackageVersion(packageVersion);
131
- return super.save(packageVersion, opts);
124
+ return this.save(packageVersion, opts);
132
125
  }
133
126
  static addSignatureToPackageVersion = undefined;
134
127
  static enablePackageVersionSigning(fn) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SQLiteDB = require("better-sqlite3");
4
+ const user_context_1 = require("../context/user-context");
5
+ const user_context_singleton_1 = require("../context/user-context-singleton");
6
+ const keys_1 = require("../keys");
7
+ const utils_1 = require("../utils");
8
+ const group_member_roles_1 = require("./group-member-roles");
9
+ const group_members_1 = require("./group-members");
10
+ const groups_1 = require("./groups");
11
+ const sql_data_source_1 = require("./orm/sql.data-source");
12
+ const package_versions_1 = require("./package-versions");
13
+ const users_1 = require("./users");
14
+ // ---------------------------------------------------------------------------
15
+ // In-memory SQLite harness (same pattern as sql.data-source.test.ts)
16
+ // ---------------------------------------------------------------------------
17
+ class DBHarness {
18
+ _db = null;
19
+ get db() {
20
+ if (!this._db) {
21
+ this._db = new SQLiteDB(":memory:");
22
+ this._db.pragma("journal_mode = WAL");
23
+ }
24
+ return this._db;
25
+ }
26
+ async get(sql, params = []) {
27
+ return this.db.prepare(sql).get(params);
28
+ }
29
+ async all(sql, params = []) {
30
+ return this.db.prepare(sql).all(params);
31
+ }
32
+ async exec(sql, params = []) {
33
+ const result = this.db.prepare(sql).run(params);
34
+ return { changes: result.changes };
35
+ }
36
+ async close() {
37
+ this._db?.close();
38
+ this._db = null;
39
+ }
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Test fixtures
43
+ // ---------------------------------------------------------------------------
44
+ const writerKeys = (0, keys_1.newKeys)();
45
+ const adminKeys = (0, keys_1.newKeys)();
46
+ const testGroupId = (0, utils_1.newid)();
47
+ const writerUserId = (0, utils_1.newid)();
48
+ const adminUserId = (0, utils_1.newid)();
49
+ function makePV(overrides = {}) {
50
+ return {
51
+ packageVersionId: (0, utils_1.newid)(),
52
+ packageId: (0, utils_1.newid)(),
53
+ version: "1.0.0",
54
+ versionTag: "dev",
55
+ packageVersionHash: "testhash",
56
+ packageBundleFileId: (0, utils_1.newid)(),
57
+ packageBundleFileHash: "bundlehash",
58
+ signature: "",
59
+ createdBy: writerUserId,
60
+ createdAt: new Date().toISOString(),
61
+ ...overrides,
62
+ };
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Setup: create a real ephemeral UserContext with Users + GroupMembers seeded
66
+ // so that getUserRoleFromPublicKey resolves Writer vs Admin from real data.
67
+ // ---------------------------------------------------------------------------
68
+ let userContext;
69
+ beforeAll(async () => {
70
+ const db = new DBHarness();
71
+ const dataSourceFactory = (metaData, schema) => {
72
+ return new sql_data_source_1.SQLDataSource(db, metaData, schema);
73
+ };
74
+ userContext = new user_context_1.UserContext(writerUserId, dataSourceFactory, true);
75
+ await userContext.loadingPromise;
76
+ userContext.deviceId((0, utils_1.newid)());
77
+ (0, user_context_singleton_1.setUserContext)(userContext);
78
+ const dc = userContext.userDataContext;
79
+ // Enable passthrough so seeding skips signature checks on Groups/GroupMembers/Users
80
+ groups_1.GroupsTable.isPassthrough = true;
81
+ group_members_1.GroupMembersTable.isPassthrough = true;
82
+ users_1.UsersTable.isPassthrough = true;
83
+ // Seed the writer user
84
+ const { Users } = await Promise.resolve().then(() => require("./users"));
85
+ const writerUser = {
86
+ userId: writerUserId,
87
+ name: "Writer User",
88
+ publicKey: writerKeys.publicKey,
89
+ publicBoxKey: "",
90
+ signature: "",
91
+ };
92
+ await Users(dc).save(writerUser);
93
+ // Seed the admin user
94
+ const adminUser = {
95
+ userId: adminUserId,
96
+ name: "Admin User",
97
+ publicKey: adminKeys.publicKey,
98
+ publicBoxKey: "",
99
+ signature: "",
100
+ };
101
+ await Users(dc).save(adminUser);
102
+ // Seed a group so the group lookup finds real data (not "group not found → Founder")
103
+ const { Groups } = await Promise.resolve().then(() => require("./groups"));
104
+ await Groups(dc).save({
105
+ groupId: testGroupId,
106
+ name: "Test Group",
107
+ description: "",
108
+ founderUserId: (0, utils_1.newid)(), // different from writer/admin so nobody is auto-Founder
109
+ signature: "",
110
+ publicKey: "",
111
+ publicBoxKey: "",
112
+ });
113
+ // Seed group memberships with real roles
114
+ const { GroupMembers } = await Promise.resolve().then(() => require("./group-members"));
115
+ const writerMembership = {
116
+ groupMemberId: (0, utils_1.newid)(),
117
+ groupId: testGroupId,
118
+ userId: writerUserId,
119
+ role: group_member_roles_1.GroupMemberRole.Writer,
120
+ signature: "",
121
+ };
122
+ await GroupMembers(dc).save(writerMembership);
123
+ const adminMembership = {
124
+ groupMemberId: (0, utils_1.newid)(),
125
+ groupId: testGroupId,
126
+ userId: adminUserId,
127
+ role: group_member_roles_1.GroupMemberRole.Admin,
128
+ signature: "",
129
+ };
130
+ await GroupMembers(dc).save(adminMembership);
131
+ // Disable passthrough so the real permission checks apply during tests
132
+ groups_1.GroupsTable.isPassthrough = false;
133
+ group_members_1.GroupMembersTable.isPassthrough = false;
134
+ users_1.UsersTable.isPassthrough = false;
135
+ // Enable real signing
136
+ package_versions_1.PackageVersionsTable.enablePackageVersionSigning((pv) => (0, keys_1.addSignatureToObject)(pv, activeSecretKey));
137
+ });
138
+ let activeSecretKey = writerKeys.secretKey;
139
+ // ---------------------------------------------------------------------------
140
+ // Tests
141
+ // ---------------------------------------------------------------------------
142
+ describe("PackageVersionsTable.signAndSave — promotion permission enforcement", () => {
143
+ // These tests exercise the real signAndSave → save → verifyPackageVersionSignature
144
+ // chain against a real in-memory database with real group membership data.
145
+ //
146
+ // THE BUG: signAndSave previously called super.save() which skipped the
147
+ // overridden save() and its verifyPackageVersionSignature check. A Writer
148
+ // could promote dev → beta/stable unchallenged.
149
+ //
150
+ // THE FIX: signAndSave now calls this.save(), so the permission check fires.
151
+ // Helper: get the PackageVersions table in the test group context
152
+ function pvTable() {
153
+ const dc = userContext.getDataContext(testGroupId);
154
+ return (0, package_versions_1.PackageVersions)(dc);
155
+ }
156
+ describe("Writer role", () => {
157
+ beforeEach(() => {
158
+ activeSecretKey = writerKeys.secretKey;
159
+ });
160
+ it("can signAndSave a dev version", async () => {
161
+ const pv = makePV({ versionTag: "dev" });
162
+ const saved = await pvTable().signAndSave(pv);
163
+ expect(saved.versionTag).toBe("dev");
164
+ expect(saved.signature).toBeTruthy();
165
+ });
166
+ it("is blocked from signAndSave with versionTag=beta", async () => {
167
+ const pv = makePV({ versionTag: "beta" });
168
+ // Before the fix: this would PASS (super.save skipped the check)
169
+ // After the fix: this correctly REJECTS
170
+ await expect(pvTable().signAndSave(pv)).rejects.toThrow("Only group admins can create or update beta/stable package versions");
171
+ });
172
+ it("is blocked from signAndSave with versionTag=stable", async () => {
173
+ const pv = makePV({ versionTag: "stable" });
174
+ await expect(pvTable().signAndSave(pv)).rejects.toThrow("Only group admins can create or update beta/stable package versions");
175
+ });
176
+ it("is blocked from promoting an existing dev version to beta", async () => {
177
+ const pv = makePV({ versionTag: "dev" });
178
+ const saved = await pvTable().signAndSave(pv);
179
+ const promoted = { ...saved, versionTag: "beta" };
180
+ await expect(pvTable().signAndSave(promoted)).rejects.toThrow("Only group admins can create or update beta/stable package versions");
181
+ });
182
+ });
183
+ describe("Admin role", () => {
184
+ beforeEach(() => {
185
+ activeSecretKey = adminKeys.secretKey;
186
+ });
187
+ it("can signAndSave a dev version", async () => {
188
+ const pv = makePV({ versionTag: "dev" });
189
+ const saved = await pvTable().signAndSave(pv);
190
+ expect(saved.versionTag).toBe("dev");
191
+ });
192
+ it("can signAndSave a beta version", async () => {
193
+ const pv = makePV({ versionTag: "beta" });
194
+ const saved = await pvTable().signAndSave(pv);
195
+ expect(saved.versionTag).toBe("beta");
196
+ });
197
+ it("can signAndSave a stable version", async () => {
198
+ const pv = makePV({ versionTag: "stable" });
199
+ const saved = await pvTable().signAndSave(pv);
200
+ expect(saved.versionTag).toBe("stable");
201
+ });
202
+ it("can promote an existing dev version to beta", async () => {
203
+ const pv = makePV({ versionTag: "dev" });
204
+ const saved = await pvTable().signAndSave(pv);
205
+ const promoted = { ...saved, versionTag: "beta" };
206
+ const result = await pvTable().signAndSave(promoted);
207
+ expect(result.versionTag).toBe("beta");
208
+ });
209
+ it("can promote an existing beta version to stable", async () => {
210
+ const pv = makePV({ versionTag: "beta" });
211
+ const saved = await pvTable().signAndSave(pv);
212
+ const promoted = { ...saved, versionTag: "stable" };
213
+ const result = await pvTable().signAndSave(promoted);
214
+ expect(result.versionTag).toBe("stable");
215
+ });
216
+ });
217
+ describe("personal space (no group)", () => {
218
+ it("allows Writer to signAndSave any tag without restriction", async () => {
219
+ activeSecretKey = writerKeys.secretKey;
220
+ const dc = userContext.userDataContext;
221
+ const table = (0, package_versions_1.PackageVersions)(dc);
222
+ const pv = makePV({ versionTag: "stable" });
223
+ const saved = await table.signAndSave(pv);
224
+ expect(saved.versionTag).toBe("stable");
225
+ });
226
+ });
227
+ });
@@ -106,14 +106,7 @@ let PackagesTable = (() => {
106
106
  // users can do whatever they want to packages in their personal space
107
107
  return super.save(packageObj, opts);
108
108
  }
109
- try {
110
- await (0, package_permissions_1.verifyPackageSignature)(packageObj, this.groupId);
111
- }
112
- catch (err) {
113
- throw new Error("Package verification failed. Did you mean to call `signAndSave`?", {
114
- cause: err,
115
- });
116
- }
109
+ await (0, package_permissions_1.verifyPackageSignature)(packageObj, this.groupId);
117
110
  return super.save(packageObj, opts);
118
111
  }
119
112
  async signAndSave(packageObj, opts) {
@@ -121,7 +114,7 @@ let PackagesTable = (() => {
121
114
  throw new Error("Package signing is not enabled. Call PackagesTable.enablePackageSigning(fn) to enable it.");
122
115
  }
123
116
  packageObj = await PackagesTable.addSignatureToPackage(packageObj);
124
- return super.save(packageObj, opts);
117
+ return this.save(packageObj, opts);
125
118
  }
126
119
  static addSignatureToPackage = undefined;
127
120
  static enablePackageSigning(fn) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"