@peers-app/peers-sdk 0.18.8 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +74 -1
  2. package/dist/data/files/file-read-stream.js +7 -0
  3. package/dist/data/files/file.types.d.ts +6 -0
  4. package/dist/data/files/file.types.js +18 -0
  5. package/dist/data/files/files.test.js +50 -7
  6. package/dist/data/package-version-resolver.test.js +1 -0
  7. package/dist/data/package-versions.d.ts +3 -0
  8. package/dist/data/package-versions.js +5 -0
  9. package/dist/data/packages.d.ts +6 -0
  10. package/dist/data/packages.js +8 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/package-installer/index.d.ts +10 -0
  14. package/dist/package-installer/index.js +26 -0
  15. package/dist/package-installer/package-author-signing.d.ts +48 -0
  16. package/dist/package-installer/package-author-signing.js +73 -0
  17. package/dist/package-installer/package-author-signing.test.d.ts +1 -0
  18. package/dist/package-installer/package-author-signing.test.js +189 -0
  19. package/dist/package-installer/package-cloner.d.ts +16 -0
  20. package/dist/package-installer/package-cloner.js +115 -0
  21. package/dist/package-installer/package-cloner.test.d.ts +1 -0
  22. package/dist/package-installer/package-cloner.test.js +276 -0
  23. package/dist/package-installer/package-creator.d.ts +22 -0
  24. package/dist/package-installer/package-creator.js +154 -0
  25. package/dist/package-installer/package-creator.test.d.ts +1 -0
  26. package/dist/package-installer/package-creator.test.js +354 -0
  27. package/dist/package-installer/package-installer.d.ts +32 -0
  28. package/dist/package-installer/package-installer.js +247 -0
  29. package/dist/package-installer/package-installer.test.d.ts +1 -0
  30. package/dist/package-installer/package-installer.test.js +666 -0
  31. package/dist/package-installer/package-propagation.d.ts +29 -0
  32. package/dist/package-installer/package-propagation.js +363 -0
  33. package/dist/package-installer/package-propagation.test.d.ts +1 -0
  34. package/dist/package-installer/package-propagation.test.js +1145 -0
  35. package/dist/package-installer/package-publisher.d.ts +50 -0
  36. package/dist/package-installer/package-publisher.js +67 -0
  37. package/dist/package-installer/package-publisher.test.d.ts +1 -0
  38. package/dist/package-installer/package-publisher.test.js +142 -0
  39. package/dist/package-installer/package-remote-checker.d.ts +54 -0
  40. package/dist/package-installer/package-remote-checker.js +186 -0
  41. package/dist/package-installer/package-remote-checker.test.d.ts +1 -0
  42. package/dist/package-installer/package-remote-checker.test.js +263 -0
  43. package/dist/package-installer/package-seed-installer.d.ts +45 -0
  44. package/dist/package-installer/package-seed-installer.js +108 -0
  45. package/dist/package-installer/package-seed-installer.test.d.ts +1 -0
  46. package/dist/package-installer/package-seed-installer.test.js +123 -0
  47. package/dist/package-installer/package-tarball.d.ts +35 -0
  48. package/dist/package-installer/package-tarball.js +57 -0
  49. package/dist/package-installer/package-tarball.test.d.ts +1 -0
  50. package/dist/package-installer/package-tarball.test.js +75 -0
  51. package/dist/package-installer/types.d.ts +110 -0
  52. package/dist/package-installer/types.js +2 -0
  53. package/dist/rpc-types.d.ts +14 -0
  54. package/dist/rpc-types.js +6 -0
  55. package/dist/system-ids.d.ts +1 -0
  56. package/dist/system-ids.js +2 -1
  57. package/package.json +3 -2
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mockPackageVersionsGet = jest.fn();
4
+ const mockPackageVersionsSignAndSave = jest.fn();
5
+ jest.mock("../data/package-versions", () => {
6
+ const actual = jest.requireActual("../data/package-versions");
7
+ return {
8
+ ...actual,
9
+ PackageVersions: (ctx) => ({
10
+ get: (packageVersionId) => mockPackageVersionsGet(ctx, packageVersionId),
11
+ signAndSave: (pv) => mockPackageVersionsSignAndSave(ctx, pv),
12
+ }),
13
+ };
14
+ });
15
+ const mockPackagesGet = jest.fn();
16
+ const mockPackagesSignAndSave = jest.fn();
17
+ jest.mock("../data/packages", () => ({
18
+ Packages: (ctx) => ({
19
+ get: (packageId) => mockPackagesGet(ctx, packageId),
20
+ signAndSave: (pkg) => mockPackagesSignAndSave(ctx, pkg),
21
+ }),
22
+ }));
23
+ const mockSaveBundleFile = jest.fn();
24
+ jest.mock("./package-installer", () => ({
25
+ saveBundleFile: (...args) => mockSaveBundleFile(...args),
26
+ }));
27
+ const mockActivateBestEligibleVersion = jest.fn();
28
+ jest.mock("./package-propagation", () => ({
29
+ activateBestEligibleVersion: (...args) => mockActivateBestEligibleVersion(...args),
30
+ }));
31
+ const file_types_1 = require("../data/files/file.types");
32
+ const keys_1 = require("../keys");
33
+ const package_author_signing_1 = require("./package-author-signing");
34
+ const package_remote_checker_1 = require("./package-remote-checker");
35
+ const package_tarball_1 = require("./package-tarball");
36
+ const PKG_ID = "00mh0wlipjixk2gqmurbwee0o";
37
+ const PV_ID = "00mpdg7xstwt3oe51x9brk15i";
38
+ function makeContext(dataContextId) {
39
+ return { dataContextId };
40
+ }
41
+ async function makeSignedRemoteResult(secretKey, publicKey, overrides = {}) {
42
+ const bundleCode = overrides.packageBundleFileHash
43
+ ? "console.log('different');"
44
+ : "console.log('hello');";
45
+ const bundleHash = (0, file_types_1.computeFileHash)(bundleCode);
46
+ const payloadBase = {
47
+ packageId: PKG_ID,
48
+ packageVersionId: PV_ID,
49
+ version: "1.0.0",
50
+ versionTag: "stable",
51
+ packageBundleFileHash: bundleHash,
52
+ routesBundleFileHash: undefined,
53
+ uiBundleFileHash: undefined,
54
+ ...overrides,
55
+ };
56
+ const signature = (0, package_author_signing_1.signPackageAuthor)({
57
+ packageId: payloadBase.packageId,
58
+ packageVersionId: payloadBase.packageVersionId,
59
+ version: payloadBase.version,
60
+ versionTag: payloadBase.versionTag,
61
+ packageBundleFileHash: payloadBase.packageBundleFileHash,
62
+ routesBundleFileHash: payloadBase.routesBundleFileHash,
63
+ uiBundleFileHash: payloadBase.uiBundleFileHash,
64
+ }, secretKey);
65
+ const payload = {
66
+ ...payloadBase,
67
+ publicKey,
68
+ packageAuthorSignature: signature,
69
+ };
70
+ const tarball = await (0, package_tarball_1.packPeersTarball)({ payload, packageBundle: bundleCode });
71
+ const pointer = {
72
+ packageVersionId: payload.packageVersionId,
73
+ version: payload.version,
74
+ versionTag: payload.versionTag,
75
+ path: "v1.0.0-stable.peers-pkg.tar.gz",
76
+ sha256: (0, keys_1.hashBytes)(tarball),
77
+ publishedAt: new Date().toISOString(),
78
+ };
79
+ return { payload, bundleCode, bundleHash, tarball, pointer };
80
+ }
81
+ describe("package-remote-checker", () => {
82
+ let keys;
83
+ beforeAll(() => {
84
+ keys = (0, keys_1.newKeys)();
85
+ });
86
+ beforeEach(() => {
87
+ (0, package_remote_checker_1.clearRemoteCheckCache)();
88
+ jest.clearAllMocks();
89
+ mockSaveBundleFile.mockImplementation(async (_ctx, content, name) => ({
90
+ fileId: `file-${name}`,
91
+ fileHash: (0, file_types_1.computeFileHash)(content),
92
+ }));
93
+ mockPackageVersionsSignAndSave.mockImplementation(async (_ctx, pv) => pv);
94
+ mockPackagesSignAndSave.mockImplementation(async (_ctx, pkg) => pkg);
95
+ mockActivateBestEligibleVersion.mockResolvedValue({
96
+ activated: true,
97
+ activePackageVersionId: PV_ID,
98
+ });
99
+ });
100
+ describe("checkPackageRemoteForNewVersion", () => {
101
+ it("downloads and verifies tarball when new version available", async () => {
102
+ const { pointer, tarball, bundleCode } = await makeSignedRemoteResult(keys.secretKey, keys.publicKey);
103
+ const http = {
104
+ fetchJson: jest.fn().mockResolvedValue(pointer),
105
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
106
+ };
107
+ const result = await (0, package_remote_checker_1.checkPackageRemoteForNewVersion)(PKG_ID, "https://example.com/packages/test", "stable", http);
108
+ expect(result).not.toBeNull();
109
+ expect(result?.packageId).toBe(PKG_ID);
110
+ expect(result?.unpacked.payload.version).toBe("1.0.0");
111
+ expect(result?.unpacked.packageBundle).toBe(bundleCode);
112
+ });
113
+ it("verifies tarball integrity with byte-level hash", async () => {
114
+ const { pointer, tarball } = await makeSignedRemoteResult(keys.secretKey, keys.publicKey);
115
+ const badPointer = { ...pointer, sha256: (0, keys_1.hashValue)(new TextDecoder().decode(tarball)) };
116
+ const http = {
117
+ fetchJson: jest.fn().mockResolvedValue(badPointer),
118
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
119
+ };
120
+ const result = await (0, package_remote_checker_1.checkPackageRemoteForNewVersion)(PKG_ID, "https://example.com/packages/test", "stable", http);
121
+ expect(result).toBeNull();
122
+ });
123
+ it("rejects tarball with invalid author signature", async () => {
124
+ const bundleCode = "console.log('bad');";
125
+ const bundleHash = (0, file_types_1.computeFileHash)(bundleCode);
126
+ const payload = {
127
+ packageId: PKG_ID,
128
+ packageVersionId: PV_ID,
129
+ version: "1.0.0",
130
+ versionTag: "stable",
131
+ packageBundleFileHash: bundleHash,
132
+ publicKey: keys.publicKey,
133
+ packageAuthorSignature: "invalidSignatureHere", // invalid
134
+ };
135
+ const tarball = await (0, package_tarball_1.packPeersTarball)({ payload, packageBundle: bundleCode });
136
+ const pointer = {
137
+ packageVersionId: PV_ID,
138
+ version: "1.0.0",
139
+ versionTag: "stable",
140
+ path: "v1.0.0-stable.peers-pkg.tar.gz",
141
+ sha256: "",
142
+ publishedAt: new Date().toISOString(),
143
+ };
144
+ const http = {
145
+ fetchJson: jest.fn().mockResolvedValue(pointer),
146
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
147
+ };
148
+ // Should return null (error caught internally and logged)
149
+ const result = await (0, package_remote_checker_1.checkPackageRemoteForNewVersion)(PKG_ID, "https://example.com/packages/test", "stable", http);
150
+ expect(result).toBeNull();
151
+ });
152
+ it("memoizes results per (updateUrl, tag)", async () => {
153
+ const { pointer, tarball } = await makeSignedRemoteResult(keys.secretKey, keys.publicKey);
154
+ const http = {
155
+ fetchJson: jest.fn().mockResolvedValue(pointer),
156
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
157
+ };
158
+ await (0, package_remote_checker_1.checkPackageRemoteForNewVersion)(PKG_ID, "https://example.com/pkg", "stable", http);
159
+ await (0, package_remote_checker_1.checkPackageRemoteForNewVersion)(PKG_ID, "https://example.com/pkg", "stable", http);
160
+ expect(http.fetchJson).toHaveBeenCalledTimes(1);
161
+ expect(http.fetchBinary).toHaveBeenCalledTimes(1);
162
+ });
163
+ it("rejects tarball with mismatched bundle hash", async () => {
164
+ const bundleCode = "console.log('real');";
165
+ const fakeHash = "notTheRealHash";
166
+ const signature = (0, package_author_signing_1.signPackageAuthor)({
167
+ packageId: PKG_ID,
168
+ packageVersionId: PV_ID,
169
+ version: "1.0.0",
170
+ versionTag: "stable",
171
+ packageBundleFileHash: fakeHash, // hash in payload doesn't match actual bundle
172
+ }, keys.secretKey);
173
+ const payload = {
174
+ packageId: PKG_ID,
175
+ packageVersionId: PV_ID,
176
+ version: "1.0.0",
177
+ versionTag: "stable",
178
+ packageBundleFileHash: fakeHash,
179
+ publicKey: keys.publicKey,
180
+ packageAuthorSignature: signature,
181
+ };
182
+ const tarball = await (0, package_tarball_1.packPeersTarball)({ payload, packageBundle: bundleCode });
183
+ const http = {
184
+ fetchJson: jest.fn().mockResolvedValue({
185
+ packageVersionId: PV_ID,
186
+ version: "1.0.0",
187
+ versionTag: "stable",
188
+ path: "test.tar.gz",
189
+ sha256: "",
190
+ publishedAt: new Date().toISOString(),
191
+ }),
192
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
193
+ };
194
+ const result = await (0, package_remote_checker_1.checkPackageRemoteForNewVersion)(PKG_ID, "https://example.com/pkg2", "stable", http);
195
+ // Error logged, returns null
196
+ expect(result).toBeNull();
197
+ });
198
+ it("rejects pointer and payload mismatches", async () => {
199
+ const { pointer, tarball } = await makeSignedRemoteResult(keys.secretKey, keys.publicKey);
200
+ const http = {
201
+ fetchJson: jest.fn().mockResolvedValue({ ...pointer, version: "2.0.0" }),
202
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
203
+ };
204
+ const result = await (0, package_remote_checker_1.checkPackageRemoteForNewVersion)(PKG_ID, "https://example.com/pkg3", "stable", http);
205
+ expect(result).toBeNull();
206
+ });
207
+ });
208
+ describe("checkAndInstallPackageRemoteVersion", () => {
209
+ it("checks local version existence per context outside the remote cache", async () => {
210
+ const groupA = makeContext("group-a");
211
+ const groupB = makeContext("group-b");
212
+ const { pointer, tarball } = await makeSignedRemoteResult(keys.secretKey, keys.publicKey);
213
+ const pkg = {
214
+ packageId: PKG_ID,
215
+ name: "test",
216
+ publishPublicKey: keys.publicKey,
217
+ updateUrl: "https://example.com/pkg",
218
+ };
219
+ const http = {
220
+ fetchJson: jest.fn().mockResolvedValue(pointer),
221
+ fetchBinary: jest.fn().mockResolvedValue(tarball),
222
+ };
223
+ mockPackageVersionsGet.mockImplementation(async (ctx, pvId) => ctx.dataContextId === "group-a" ? { packageVersionId: pvId } : undefined);
224
+ mockPackagesGet.mockResolvedValue(pkg);
225
+ const first = await (0, package_remote_checker_1.checkAndInstallPackageRemoteVersion)(groupA, pkg, "stable", http);
226
+ const second = await (0, package_remote_checker_1.checkAndInstallPackageRemoteVersion)(groupB, pkg, "stable", http);
227
+ expect(first).toBeNull();
228
+ expect(second?.packageVersion.packageVersionId).toBe(PV_ID);
229
+ expect(http.fetchJson).toHaveBeenCalledTimes(1);
230
+ expect(mockSaveBundleFile).toHaveBeenCalled();
231
+ });
232
+ });
233
+ describe("installRemotePackageVersion", () => {
234
+ it("rejects publisher key mismatch before saving files", async () => {
235
+ const { pointer, payload } = await makeSignedRemoteResult(keys.secretKey, keys.publicKey);
236
+ const result = {
237
+ packageId: PKG_ID,
238
+ pointer,
239
+ unpacked: { payload, packageBundle: "console.log('hello');" },
240
+ };
241
+ mockPackagesGet.mockResolvedValue({
242
+ packageId: PKG_ID,
243
+ name: "test",
244
+ publishPublicKey: (0, keys_1.newKeys)().publicKey,
245
+ });
246
+ await expect((0, package_remote_checker_1.installRemotePackageVersion)(makeContext("group-a"), result)).rejects.toThrow("Package publisher key mismatch");
247
+ expect(mockSaveBundleFile).not.toHaveBeenCalled();
248
+ });
249
+ it("establishes TOFU key and activates after installing", async () => {
250
+ const { pointer, payload, bundleCode } = await makeSignedRemoteResult(keys.secretKey, keys.publicKey);
251
+ const pkg = { packageId: PKG_ID, name: "test", publishPublicKey: "" };
252
+ mockPackagesGet.mockResolvedValue(pkg);
253
+ const installed = await (0, package_remote_checker_1.installRemotePackageVersion)(makeContext("group-a"), {
254
+ packageId: PKG_ID,
255
+ pointer,
256
+ unpacked: { payload, packageBundle: bundleCode },
257
+ });
258
+ expect(installed.activated).toBe(true);
259
+ expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ publishPublicKey: keys.publicKey }));
260
+ expect(mockPackageVersionsSignAndSave).toHaveBeenCalled();
261
+ });
262
+ });
263
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Generic package seed installer.
3
+ *
4
+ * Seeds a package into a data context if it doesn't already exist.
5
+ * Called at group creation time (not on every startup).
6
+ */
7
+ import type { DataContext } from "../context/data-context";
8
+ /** Options for seeding a package into a data context. */
9
+ export interface ISeedPackageOpts {
10
+ packageId: string;
11
+ name: string;
12
+ description?: string;
13
+ createdBy: string;
14
+ publishPublicKey?: string;
15
+ versionFollowRange?: "pinned" | "patch" | "minor" | "latest";
16
+ followVersionTags?: string;
17
+ updateUrl?: string;
18
+ remoteRepo?: string;
19
+ /** Bundle data for the initial PackageVersion. */
20
+ version: string;
21
+ versionTag: string;
22
+ packageBundleCode: string;
23
+ routesBundleCode?: string;
24
+ uiBundleCode?: string;
25
+ /**
26
+ * If provided, set as the packageAuthorSignature on the initial PV.
27
+ * Used for bundled seeds where the signature was pre-computed at build time.
28
+ */
29
+ packageAuthorSignature?: string;
30
+ /**
31
+ * When true, immediately load the seeded package and persist appNavs on the
32
+ * seeded PackageVersion. Useful for first-run/core package bootstrapping.
33
+ */
34
+ loadAfterSeed?: boolean;
35
+ }
36
+ /**
37
+ * Seed a package into a data context. Creates the Package record and an
38
+ * initial PackageVersion if the package doesn't already exist.
39
+ *
40
+ * This is a no-op if the package is already installed (has any PV).
41
+ * Intended to be called at group creation time, not on every startup.
42
+ *
43
+ * @returns true if seeding occurred, false if package already existed.
44
+ */
45
+ export declare function seedPackageInContext(dataContext: DataContext, opts: ISeedPackageOpts): Promise<boolean>;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ /**
3
+ * Generic package seed installer.
4
+ *
5
+ * Seeds a package into a data context if it doesn't already exist.
6
+ * Called at group creation time (not on every startup).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.seedPackageInContext = seedPackageInContext;
10
+ const package_versions_1 = require("../data/package-versions");
11
+ const packages_1 = require("../data/packages");
12
+ const utils_1 = require("../utils");
13
+ const package_installer_1 = require("./package-installer");
14
+ /**
15
+ * Seed a package into a data context. Creates the Package record and an
16
+ * initial PackageVersion if the package doesn't already exist.
17
+ *
18
+ * This is a no-op if the package is already installed (has any PV).
19
+ * Intended to be called at group creation time, not on every startup.
20
+ *
21
+ * @returns true if seeding occurred, false if package already existed.
22
+ */
23
+ async function seedPackageInContext(dataContext, opts) {
24
+ const pkgTable = (0, packages_1.Packages)(dataContext);
25
+ const existingPkg = await pkgTable.get(opts.packageId);
26
+ if (existingPkg) {
27
+ // Package already exists — check if it has any version installed
28
+ const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
29
+ const existingPvs = await pvTable.list({ packageId: opts.packageId });
30
+ if (existingPvs.length > 0) {
31
+ return false;
32
+ }
33
+ // Package record exists but no versions — install the initial PV
34
+ applyMissingSeedDefaults(existingPkg, opts);
35
+ return await installSeedVersion(dataContext, existingPkg, opts);
36
+ }
37
+ // Create Package record with provided defaults
38
+ const pkg = {
39
+ packageId: opts.packageId,
40
+ name: opts.name,
41
+ description: opts.description ?? "",
42
+ createdBy: opts.createdBy,
43
+ disabled: false,
44
+ publishPublicKey: opts.publishPublicKey ?? "",
45
+ versionFollowRange: opts.versionFollowRange,
46
+ followVersionTags: opts.followVersionTags,
47
+ updateUrl: opts.updateUrl,
48
+ remoteRepo: opts.remoteRepo,
49
+ signature: "",
50
+ };
51
+ await pkgTable.signAndSave(pkg, { saveAsSnapshot: true });
52
+ await installSeedVersion(dataContext, pkg, opts);
53
+ return true;
54
+ }
55
+ function applyMissingSeedDefaults(pkg, opts) {
56
+ pkg.description ||= opts.description ?? "";
57
+ pkg.publishPublicKey ||= opts.publishPublicKey ?? "";
58
+ pkg.versionFollowRange ||= opts.versionFollowRange;
59
+ pkg.followVersionTags ||= opts.followVersionTags;
60
+ pkg.updateUrl ||= opts.updateUrl;
61
+ pkg.remoteRepo ||= opts.remoteRepo;
62
+ }
63
+ async function installSeedVersion(dataContext, pkg, opts) {
64
+ // Save bundle files (deduped by hash)
65
+ const pkgFile = await (0, package_installer_1.saveBundleFile)(dataContext, opts.packageBundleCode, `${opts.packageId}-package.bundle.js`, opts.packageId);
66
+ let routesFile;
67
+ if (opts.routesBundleCode) {
68
+ routesFile = await (0, package_installer_1.saveBundleFile)(dataContext, opts.routesBundleCode, `${opts.packageId}-routes.bundle.js`, opts.packageId);
69
+ }
70
+ let uiFile;
71
+ if (opts.uiBundleCode) {
72
+ uiFile = await (0, package_installer_1.saveBundleFile)(dataContext, opts.uiBundleCode, `${opts.packageId}-uis.bundle.js`, opts.packageId);
73
+ }
74
+ const pvHash = (0, package_versions_1.computePackageVersionHash)(opts.version, opts.versionTag, pkgFile.fileHash, routesFile?.fileHash, uiFile?.fileHash);
75
+ const pv = {
76
+ packageVersionId: (0, utils_1.newid)(),
77
+ packageId: opts.packageId,
78
+ version: opts.version,
79
+ versionTag: opts.versionTag,
80
+ packageVersionHash: pvHash,
81
+ packageBundleFileId: pkgFile.fileId,
82
+ packageBundleFileHash: pkgFile.fileHash,
83
+ routesBundleFileId: routesFile?.fileId,
84
+ routesBundleFileHash: routesFile?.fileHash,
85
+ uiBundleFileId: uiFile?.fileId,
86
+ uiBundleFileHash: uiFile?.fileHash,
87
+ packageAuthorSignature: opts.packageAuthorSignature,
88
+ signature: "",
89
+ createdBy: opts.createdBy,
90
+ createdAt: new Date().toISOString(),
91
+ };
92
+ const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
93
+ const savedPv = await pvTable.signAndSave(pv, { saveAsSnapshot: true });
94
+ // Activate this version
95
+ pkg.activePackageVersionId = savedPv.packageVersionId;
96
+ await (0, packages_1.Packages)(dataContext).signAndSave(pkg, { saveAsSnapshot: true });
97
+ if (opts.loadAfterSeed) {
98
+ const loaded = await dataContext.packageLoader.loadPackage(pkg, {
99
+ force: true,
100
+ packageVersionId: savedPv.packageVersionId,
101
+ });
102
+ if (loaded?.appNavs && JSON.stringify(loaded.appNavs) !== JSON.stringify(savedPv.appNavs)) {
103
+ savedPv.appNavs = loaded.appNavs;
104
+ await pvTable.signAndSave(savedPv, { saveAsSnapshot: true });
105
+ }
106
+ }
107
+ return true;
108
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mockPackageVersionsList = jest.fn();
4
+ const mockPackageVersionsSignAndSave = jest.fn();
5
+ jest.mock("../data/package-versions", () => {
6
+ const actual = jest.requireActual("../data/package-versions");
7
+ return {
8
+ ...actual,
9
+ PackageVersions: (_ctx) => ({
10
+ list: (filter) => mockPackageVersionsList(filter),
11
+ signAndSave: (pv, opts) => mockPackageVersionsSignAndSave(pv, opts),
12
+ }),
13
+ };
14
+ });
15
+ const mockPackagesGet = jest.fn();
16
+ const mockPackagesSignAndSave = jest.fn();
17
+ jest.mock("../data/packages", () => ({
18
+ Packages: (_ctx) => ({
19
+ get: (packageId) => mockPackagesGet(packageId),
20
+ signAndSave: (pkg, opts) => mockPackagesSignAndSave(pkg, opts),
21
+ }),
22
+ }));
23
+ const mockSaveBundleFile = jest.fn();
24
+ jest.mock("./package-installer", () => ({
25
+ saveBundleFile: (...args) => mockSaveBundleFile(...args),
26
+ }));
27
+ const keys_1 = require("../keys");
28
+ const package_seed_installer_1 = require("./package-seed-installer");
29
+ const PKG_ID = "00mh0wlipjixk2gqmurbwee0o";
30
+ function makeContext() {
31
+ return {
32
+ dataContextId: "ctx",
33
+ packageLoader: {
34
+ loadPackage: jest.fn(),
35
+ },
36
+ };
37
+ }
38
+ function makeOpts(overrides = {}) {
39
+ return {
40
+ packageId: PKG_ID,
41
+ name: "peers-core",
42
+ description: "Core package",
43
+ createdBy: "00m87fbwu96jzf8qnwllly0fd",
44
+ publishPublicKey: "publish-key",
45
+ versionFollowRange: "latest",
46
+ followVersionTags: "stable",
47
+ updateUrl: "https://example.com/packages/peers-core",
48
+ remoteRepo: "https://github.com/peers-app/peers-core",
49
+ version: "1.2.3",
50
+ versionTag: "stable",
51
+ packageBundleCode: "console.log('package');",
52
+ routesBundleCode: "console.log('routes');",
53
+ uiBundleCode: "console.log('ui');",
54
+ ...overrides,
55
+ };
56
+ }
57
+ describe("package-seed-installer", () => {
58
+ beforeEach(() => {
59
+ jest.clearAllMocks();
60
+ mockSaveBundleFile.mockImplementation(async (_ctx, content, name) => ({
61
+ fileId: `file-${name}`,
62
+ fileHash: (0, keys_1.hashValue)(content),
63
+ }));
64
+ mockPackagesSignAndSave.mockImplementation(async (pkg) => pkg);
65
+ mockPackageVersionsSignAndSave.mockImplementation(async (pv) => pv);
66
+ mockPackageVersionsList.mockResolvedValue([]);
67
+ mockPackagesGet.mockResolvedValue(undefined);
68
+ });
69
+ it("creates package and initial active package version", async () => {
70
+ const seeded = await (0, package_seed_installer_1.seedPackageInContext)(makeContext(), makeOpts());
71
+ expect(seeded).toBe(true);
72
+ expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
73
+ packageId: PKG_ID,
74
+ publishPublicKey: "publish-key",
75
+ versionFollowRange: "latest",
76
+ followVersionTags: "stable",
77
+ }), { saveAsSnapshot: true });
78
+ expect(mockPackageVersionsSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
79
+ packageId: PKG_ID,
80
+ version: "1.2.3",
81
+ versionTag: "stable",
82
+ packageBundleFileHash: (0, keys_1.hashValue)("console.log('package');"),
83
+ }), { saveAsSnapshot: true });
84
+ expect(mockPackagesSignAndSave).toHaveBeenLastCalledWith(expect.objectContaining({ activePackageVersionId: expect.any(String) }), { saveAsSnapshot: true });
85
+ });
86
+ it("is a no-op when package already has any version installed", async () => {
87
+ mockPackagesGet.mockResolvedValue({ packageId: PKG_ID, name: "peers-core" });
88
+ mockPackageVersionsList.mockResolvedValue([{ packageVersionId: "existing" }]);
89
+ const seeded = await (0, package_seed_installer_1.seedPackageInContext)(makeContext(), makeOpts());
90
+ expect(seeded).toBe(false);
91
+ expect(mockSaveBundleFile).not.toHaveBeenCalled();
92
+ expect(mockPackageVersionsSignAndSave).not.toHaveBeenCalled();
93
+ });
94
+ it("installs initial version when package exists without versions", async () => {
95
+ mockPackagesGet.mockResolvedValue({
96
+ packageId: PKG_ID,
97
+ name: "peers-core",
98
+ publishPublicKey: "",
99
+ signature: "",
100
+ });
101
+ mockPackageVersionsList.mockResolvedValue([]);
102
+ const seeded = await (0, package_seed_installer_1.seedPackageInContext)(makeContext(), makeOpts());
103
+ expect(seeded).toBe(true);
104
+ expect(mockSaveBundleFile).toHaveBeenCalled();
105
+ expect(mockPackageVersionsSignAndSave).toHaveBeenCalled();
106
+ expect(mockPackagesSignAndSave).toHaveBeenLastCalledWith(expect.objectContaining({
107
+ activePackageVersionId: expect.any(String),
108
+ publishPublicKey: "publish-key",
109
+ updateUrl: "https://example.com/packages/peers-core",
110
+ }), { saveAsSnapshot: true });
111
+ });
112
+ it("loads seeded package and persists appNavs when requested", async () => {
113
+ const ctx = makeContext();
114
+ ctx.packageLoader.loadPackage.mockResolvedValue({
115
+ appNavs: [{ appId: "tasks", label: "Tasks", path: "tasks" }],
116
+ });
117
+ await (0, package_seed_installer_1.seedPackageInContext)(ctx, makeOpts({ loadAfterSeed: true }));
118
+ expect(ctx.packageLoader.loadPackage).toHaveBeenCalledWith(expect.objectContaining({ packageId: PKG_ID }), expect.objectContaining({ force: true, packageVersionId: expect.any(String) }));
119
+ expect(mockPackageVersionsSignAndSave).toHaveBeenLastCalledWith(expect.objectContaining({
120
+ appNavs: [{ appId: "tasks", label: "Tasks", path: "tasks" }],
121
+ }), { saveAsSnapshot: true });
122
+ });
123
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Isomorphic tarball pack/unpack for `.peers-pkg.tar.gz` archives.
3
+ *
4
+ * Uses `nanotar` (~1KB, zero deps) with native CompressionStream/DecompressionStream
5
+ * for gzip. Works in Node 18+, modern browsers, and edge runtimes.
6
+ */
7
+ import type { IAuthorSignedPayload } from "./package-author-signing";
8
+ /** Contents of `pv-payload.json` inside a peers package tarball. */
9
+ export interface ITarballPayload extends IAuthorSignedPayload {
10
+ packageAuthorSignature: string;
11
+ }
12
+ /** Input for creating a `.peers-pkg.tar.gz`. */
13
+ export interface IPackTarballOpts {
14
+ payload: ITarballPayload;
15
+ packageBundle: string;
16
+ routesBundle?: string;
17
+ uiBundle?: string;
18
+ }
19
+ /** Result of unpacking a `.peers-pkg.tar.gz`. */
20
+ export interface IUnpackedTarball {
21
+ payload: ITarballPayload;
22
+ packageBundle: string;
23
+ routesBundle?: string;
24
+ uiBundle?: string;
25
+ }
26
+ /**
27
+ * Create a `.peers-pkg.tar.gz` from a signed payload and bundle files.
28
+ * Returns the compressed tarball as a Uint8Array.
29
+ */
30
+ export declare function packPeersTarball(opts: IPackTarballOpts): Promise<Uint8Array>;
31
+ /**
32
+ * Extract a `.peers-pkg.tar.gz` and return the payload + bundle contents.
33
+ * Throws if the required `pv-payload.json` or `package.bundle.js` are missing.
34
+ */
35
+ export declare function unpackPeersTarball(data: Uint8Array | ArrayBuffer): Promise<IUnpackedTarball>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Isomorphic tarball pack/unpack for `.peers-pkg.tar.gz` archives.
4
+ *
5
+ * Uses `nanotar` (~1KB, zero deps) with native CompressionStream/DecompressionStream
6
+ * for gzip. Works in Node 18+, modern browsers, and edge runtimes.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.packPeersTarball = packPeersTarball;
10
+ exports.unpackPeersTarball = unpackPeersTarball;
11
+ const nanotar_1 = require("nanotar");
12
+ /**
13
+ * Create a `.peers-pkg.tar.gz` from a signed payload and bundle files.
14
+ * Returns the compressed tarball as a Uint8Array.
15
+ */
16
+ async function packPeersTarball(opts) {
17
+ const files = [
18
+ { name: "pv-payload.json", data: JSON.stringify(opts.payload) },
19
+ { name: "package.bundle.js", data: opts.packageBundle },
20
+ ];
21
+ if (opts.routesBundle) {
22
+ files.push({ name: "routes.bundle.js", data: opts.routesBundle });
23
+ }
24
+ if (opts.uiBundle) {
25
+ files.push({ name: "uis.bundle.js", data: opts.uiBundle });
26
+ }
27
+ return (0, nanotar_1.createTarGzip)(files);
28
+ }
29
+ /**
30
+ * Extract a `.peers-pkg.tar.gz` and return the payload + bundle contents.
31
+ * Throws if the required `pv-payload.json` or `package.bundle.js` are missing.
32
+ */
33
+ async function unpackPeersTarball(data) {
34
+ const input = data instanceof Uint8Array ? data : new Uint8Array(data);
35
+ const files = await (0, nanotar_1.parseTarGzip)(input);
36
+ const fileMap = new Map();
37
+ for (const f of files) {
38
+ if (f.type === "file" && f.data) {
39
+ fileMap.set(f.name, f.text ?? new TextDecoder().decode(f.data));
40
+ }
41
+ }
42
+ const payloadJson = fileMap.get("pv-payload.json");
43
+ if (!payloadJson) {
44
+ throw new Error("Invalid peers tarball: missing pv-payload.json");
45
+ }
46
+ const packageBundle = fileMap.get("package.bundle.js");
47
+ if (!packageBundle) {
48
+ throw new Error("Invalid peers tarball: missing package.bundle.js");
49
+ }
50
+ const payload = JSON.parse(payloadJson);
51
+ return {
52
+ payload,
53
+ packageBundle,
54
+ routesBundle: fileMap.get("routes.bundle.js"),
55
+ uiBundle: fileMap.get("uis.bundle.js"),
56
+ };
57
+ }
@@ -0,0 +1 @@
1
+ export {};