@peers-app/peers-sdk 0.18.8 → 0.19.6
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/README.md +74 -1
- package/dist/data/files/file-read-stream.js +7 -0
- package/dist/data/files/file.types.d.ts +6 -0
- package/dist/data/files/file.types.js +18 -0
- package/dist/data/files/files.test.js +50 -7
- package/dist/data/package-version-resolver.d.ts +13 -5
- package/dist/data/package-version-resolver.js +64 -6
- package/dist/data/package-version-resolver.test.d.ts +0 -4
- package/dist/data/package-version-resolver.test.js +127 -5
- package/dist/data/package-versions.d.ts +3 -0
- package/dist/data/package-versions.js +5 -0
- package/dist/data/packages.d.ts +6 -29
- package/dist/data/packages.js +8 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/package-installer/index.d.ts +10 -0
- package/dist/package-installer/index.js +26 -0
- package/dist/package-installer/package-author-signing.d.ts +54 -0
- package/dist/package-installer/package-author-signing.js +82 -0
- package/dist/package-installer/package-author-signing.test.d.ts +1 -0
- package/dist/package-installer/package-author-signing.test.js +189 -0
- package/dist/package-installer/package-cloner.d.ts +16 -0
- package/dist/package-installer/package-cloner.js +115 -0
- package/dist/package-installer/package-cloner.test.d.ts +1 -0
- package/dist/package-installer/package-cloner.test.js +276 -0
- package/dist/package-installer/package-creator.d.ts +22 -0
- package/dist/package-installer/package-creator.js +154 -0
- package/dist/package-installer/package-creator.test.d.ts +1 -0
- package/dist/package-installer/package-creator.test.js +354 -0
- package/dist/package-installer/package-installer.d.ts +32 -0
- package/dist/package-installer/package-installer.js +247 -0
- package/dist/package-installer/package-installer.test.d.ts +1 -0
- package/dist/package-installer/package-installer.test.js +666 -0
- package/dist/package-installer/package-propagation.d.ts +29 -0
- package/dist/package-installer/package-propagation.js +364 -0
- package/dist/package-installer/package-propagation.test.d.ts +1 -0
- package/dist/package-installer/package-propagation.test.js +1145 -0
- package/dist/package-installer/package-publisher.d.ts +55 -0
- package/dist/package-installer/package-publisher.js +71 -0
- package/dist/package-installer/package-publisher.test.d.ts +1 -0
- package/dist/package-installer/package-publisher.test.js +142 -0
- package/dist/package-installer/package-remote-checker.d.ts +54 -0
- package/dist/package-installer/package-remote-checker.js +194 -0
- package/dist/package-installer/package-remote-checker.test.d.ts +1 -0
- package/dist/package-installer/package-remote-checker.test.js +269 -0
- package/dist/package-installer/package-seed-installer.d.ts +45 -0
- package/dist/package-installer/package-seed-installer.js +108 -0
- package/dist/package-installer/package-seed-installer.test.d.ts +1 -0
- package/dist/package-installer/package-seed-installer.test.js +123 -0
- package/dist/package-installer/package-tarball.d.ts +35 -0
- package/dist/package-installer/package-tarball.js +57 -0
- package/dist/package-installer/package-tarball.test.d.ts +1 -0
- package/dist/package-installer/package-tarball.test.js +75 -0
- package/dist/package-installer/types.d.ts +110 -0
- package/dist/package-installer/types.js +2 -0
- package/dist/rpc-types.d.ts +14 -0
- package/dist/rpc-types.js +6 -0
- package/dist/system-ids.d.ts +1 -0
- package/dist/system-ids.js +2 -1
- package/package.json +3 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure artifact builder for publishing Peers packages.
|
|
3
|
+
*
|
|
4
|
+
* Accepts bundle contents and a signing callback, produces a signed tarball
|
|
5
|
+
* and an {@link ILatestPointer}-compatible JSON object. Does not perform any
|
|
6
|
+
* I/O — callers (the system tool, tests) provide bundle strings and receive
|
|
7
|
+
* artifacts in memory.
|
|
8
|
+
*/
|
|
9
|
+
import type { IAppNav } from "../types/app-nav";
|
|
10
|
+
import { type IAuthorSignedPayload } from "./package-author-signing";
|
|
11
|
+
import type { ILatestPointer } from "./package-remote-checker";
|
|
12
|
+
import { type ITarballPayload } from "./package-tarball";
|
|
13
|
+
/** Bundle contents to publish. Only `packageBundle` is required. */
|
|
14
|
+
export interface IPublishBundleInput {
|
|
15
|
+
packageBundle: string;
|
|
16
|
+
routesBundle?: string;
|
|
17
|
+
uiBundle?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Options for {@link buildPublishArtifacts}. */
|
|
20
|
+
export interface IBuildPublishArtifactsOpts {
|
|
21
|
+
packageId: string;
|
|
22
|
+
version: string;
|
|
23
|
+
versionTag?: string;
|
|
24
|
+
bundles: IPublishBundleInput;
|
|
25
|
+
/**
|
|
26
|
+
* Callback that signs the author payload and returns a base64url signature.
|
|
27
|
+
* The private key never leaves the caller's scope.
|
|
28
|
+
*/
|
|
29
|
+
sign: (payload: IAuthorSignedPayload) => Promise<string> | string;
|
|
30
|
+
/** Publisher's Ed25519 public key (base64url). */
|
|
31
|
+
publicKey: string;
|
|
32
|
+
/** Pre-generated packageVersionId. If omitted, a new one is created via {@link newid}. */
|
|
33
|
+
packageVersionId?: string;
|
|
34
|
+
/** App navigation items to include in the tarball metadata. */
|
|
35
|
+
appNavs?: IAppNav[];
|
|
36
|
+
/** Peer ID of the publisher. Recorded as `createdBy` on the PV. */
|
|
37
|
+
createdBy?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Artifacts produced by {@link buildPublishArtifacts}. */
|
|
40
|
+
export interface IPublishArtifacts {
|
|
41
|
+
tarball: Uint8Array;
|
|
42
|
+
tarballHash: string;
|
|
43
|
+
pointer: ILatestPointer;
|
|
44
|
+
payload: ITarballPayload;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build publish artifacts from bundle contents.
|
|
48
|
+
*
|
|
49
|
+
* 1. Computes SHA-256 hashes for each bundle.
|
|
50
|
+
* 2. Builds the author-signed payload.
|
|
51
|
+
* 3. Calls the `sign` callback to get the detached Ed25519 signature.
|
|
52
|
+
* 4. Packs everything into a `.peers-pkg.tar.gz`.
|
|
53
|
+
* 5. Returns the tarball bytes, their SHA-256, and an {@link ILatestPointer}.
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildPublishArtifacts(opts: IBuildPublishArtifactsOpts): Promise<IPublishArtifacts>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pure artifact builder for publishing Peers packages.
|
|
4
|
+
*
|
|
5
|
+
* Accepts bundle contents and a signing callback, produces a signed tarball
|
|
6
|
+
* and an {@link ILatestPointer}-compatible JSON object. Does not perform any
|
|
7
|
+
* I/O — callers (the system tool, tests) provide bundle strings and receive
|
|
8
|
+
* artifacts in memory.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.buildPublishArtifacts = buildPublishArtifacts;
|
|
12
|
+
const file_types_1 = require("../data/files/file.types");
|
|
13
|
+
const keys_1 = require("../keys");
|
|
14
|
+
const utils_1 = require("../utils");
|
|
15
|
+
const package_author_signing_1 = require("./package-author-signing");
|
|
16
|
+
const package_tarball_1 = require("./package-tarball");
|
|
17
|
+
/**
|
|
18
|
+
* Build publish artifacts from bundle contents.
|
|
19
|
+
*
|
|
20
|
+
* 1. Computes SHA-256 hashes for each bundle.
|
|
21
|
+
* 2. Builds the author-signed payload.
|
|
22
|
+
* 3. Calls the `sign` callback to get the detached Ed25519 signature.
|
|
23
|
+
* 4. Packs everything into a `.peers-pkg.tar.gz`.
|
|
24
|
+
* 5. Returns the tarball bytes, their SHA-256, and an {@link ILatestPointer}.
|
|
25
|
+
*/
|
|
26
|
+
async function buildPublishArtifacts(opts) {
|
|
27
|
+
const { packageId, version, bundles, sign, publicKey } = opts;
|
|
28
|
+
const versionTag = opts.versionTag ?? "stable";
|
|
29
|
+
const packageVersionId = opts.packageVersionId ?? (0, utils_1.newid)();
|
|
30
|
+
const packageBundleFileHash = (0, file_types_1.computeFileHash)(bundles.packageBundle);
|
|
31
|
+
const routesBundleFileHash = bundles.routesBundle
|
|
32
|
+
? (0, file_types_1.computeFileHash)(bundles.routesBundle)
|
|
33
|
+
: undefined;
|
|
34
|
+
const uiBundleFileHash = bundles.uiBundle ? (0, file_types_1.computeFileHash)(bundles.uiBundle) : undefined;
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
const pvFields = {
|
|
37
|
+
packageId,
|
|
38
|
+
packageVersionId,
|
|
39
|
+
version,
|
|
40
|
+
versionTag,
|
|
41
|
+
packageBundleFileHash,
|
|
42
|
+
routesBundleFileHash,
|
|
43
|
+
uiBundleFileHash,
|
|
44
|
+
appNavs: opts.appNavs,
|
|
45
|
+
createdBy: opts.createdBy,
|
|
46
|
+
createdAt: now,
|
|
47
|
+
};
|
|
48
|
+
const payload = (0, package_author_signing_1.buildAuthorSignedPayload)(pvFields, publicKey);
|
|
49
|
+
const packageAuthorSignature = await sign(payload);
|
|
50
|
+
const tarballPayload = {
|
|
51
|
+
...payload,
|
|
52
|
+
packageAuthorSignature,
|
|
53
|
+
};
|
|
54
|
+
const tarball = await (0, package_tarball_1.packPeersTarball)({
|
|
55
|
+
payload: tarballPayload,
|
|
56
|
+
packageBundle: bundles.packageBundle,
|
|
57
|
+
routesBundle: bundles.routesBundle,
|
|
58
|
+
uiBundle: bundles.uiBundle,
|
|
59
|
+
});
|
|
60
|
+
const tarballHash = (0, keys_1.hashBytes)(tarball);
|
|
61
|
+
const tarballFileName = `v${version}-${versionTag}.peers-pkg.tar.gz`;
|
|
62
|
+
const pointer = {
|
|
63
|
+
packageVersionId,
|
|
64
|
+
version,
|
|
65
|
+
versionTag,
|
|
66
|
+
path: tarballFileName,
|
|
67
|
+
sha256: tarballHash,
|
|
68
|
+
publishedAt: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
return { tarball, tarballHash, pointer, payload: tarballPayload };
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const file_types_1 = require("../data/files/file.types");
|
|
4
|
+
const keys_1 = require("../keys");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const package_author_signing_1 = require("./package-author-signing");
|
|
7
|
+
const package_publisher_1 = require("./package-publisher");
|
|
8
|
+
const package_tarball_1 = require("./package-tarball");
|
|
9
|
+
function makeOpts(overrides) {
|
|
10
|
+
const keys = (0, keys_1.newKeys)();
|
|
11
|
+
return {
|
|
12
|
+
packageId: (0, utils_1.newid)(),
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
bundles: {
|
|
15
|
+
packageBundle: 'console.log("main");',
|
|
16
|
+
},
|
|
17
|
+
publicKey: keys.publicKey,
|
|
18
|
+
sign: (payload) => (0, package_author_signing_1.signPackageAuthor)({ ...payload, packageBundleFileHash: payload.packageBundleFileHash }, keys.secretKey),
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
describe("package-publisher", () => {
|
|
23
|
+
describe("buildPublishArtifacts", () => {
|
|
24
|
+
it("round-trips through unpackPeersTarball and verifies signature", async () => {
|
|
25
|
+
const keys = (0, keys_1.newKeys)();
|
|
26
|
+
const opts = makeOpts({
|
|
27
|
+
publicKey: keys.publicKey,
|
|
28
|
+
sign: (payload) => (0, package_author_signing_1.signPackageAuthor)(payload, keys.secretKey),
|
|
29
|
+
});
|
|
30
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(opts);
|
|
31
|
+
expect(result.tarball).toBeInstanceOf(Uint8Array);
|
|
32
|
+
expect(result.tarball.length).toBeGreaterThan(0);
|
|
33
|
+
const unpacked = await (0, package_tarball_1.unpackPeersTarball)(result.tarball);
|
|
34
|
+
expect(unpacked.packageBundle).toBe(opts.bundles.packageBundle);
|
|
35
|
+
expect(unpacked.payload.packageId).toBe(opts.packageId);
|
|
36
|
+
expect(unpacked.payload.version).toBe("1.0.0");
|
|
37
|
+
expect(unpacked.payload.versionTag).toBe("stable");
|
|
38
|
+
expect(unpacked.payload.publicKey).toBe(keys.publicKey);
|
|
39
|
+
const sigResult = (0, package_author_signing_1.verifyPackageAuthorSignature)({
|
|
40
|
+
...unpacked.payload,
|
|
41
|
+
packageAuthorSignature: unpacked.payload.packageAuthorSignature,
|
|
42
|
+
}, keys.publicKey);
|
|
43
|
+
expect(sigResult.valid).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it("pointer SHA-256 matches tarball bytes", async () => {
|
|
46
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts());
|
|
47
|
+
const actualHash = (0, keys_1.hashBytes)(result.tarball);
|
|
48
|
+
expect(result.pointer.sha256).toBe(actualHash);
|
|
49
|
+
expect(result.tarballHash).toBe(actualHash);
|
|
50
|
+
});
|
|
51
|
+
it("includes optional routes and UI bundles", async () => {
|
|
52
|
+
const opts = makeOpts({
|
|
53
|
+
bundles: {
|
|
54
|
+
packageBundle: 'console.log("main");',
|
|
55
|
+
routesBundle: 'console.log("routes");',
|
|
56
|
+
uiBundle: 'console.log("ui");',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(opts);
|
|
60
|
+
const unpacked = await (0, package_tarball_1.unpackPeersTarball)(result.tarball);
|
|
61
|
+
expect(unpacked.routesBundle).toBe('console.log("routes");');
|
|
62
|
+
expect(unpacked.uiBundle).toBe('console.log("ui");');
|
|
63
|
+
expect(unpacked.payload.routesBundleFileHash).toBe((0, file_types_1.computeFileHash)('console.log("routes");'));
|
|
64
|
+
expect(unpacked.payload.uiBundleFileHash).toBe((0, file_types_1.computeFileHash)('console.log("ui");'));
|
|
65
|
+
});
|
|
66
|
+
it("omits optional bundles when not provided", async () => {
|
|
67
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts());
|
|
68
|
+
const unpacked = await (0, package_tarball_1.unpackPeersTarball)(result.tarball);
|
|
69
|
+
expect(unpacked.routesBundle).toBeUndefined();
|
|
70
|
+
expect(unpacked.uiBundle).toBeUndefined();
|
|
71
|
+
expect(unpacked.payload.routesBundleFileHash).toBeUndefined();
|
|
72
|
+
expect(unpacked.payload.uiBundleFileHash).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
it("rejects tampered bundle hash via signature verification", async () => {
|
|
75
|
+
const keys = (0, keys_1.newKeys)();
|
|
76
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts({
|
|
77
|
+
publicKey: keys.publicKey,
|
|
78
|
+
sign: (payload) => (0, package_author_signing_1.signPackageAuthor)(payload, keys.secretKey),
|
|
79
|
+
}));
|
|
80
|
+
const sigResult = (0, package_author_signing_1.verifyPackageAuthorSignature)({
|
|
81
|
+
...result.payload,
|
|
82
|
+
packageBundleFileHash: "tampered-hash",
|
|
83
|
+
}, keys.publicKey);
|
|
84
|
+
expect(sigResult.valid).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
it("rejects wrong key via signature verification", async () => {
|
|
87
|
+
const authorKeys = (0, keys_1.newKeys)();
|
|
88
|
+
const wrongKeys = (0, keys_1.newKeys)();
|
|
89
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts({
|
|
90
|
+
publicKey: authorKeys.publicKey,
|
|
91
|
+
sign: (payload) => (0, package_author_signing_1.signPackageAuthor)(payload, authorKeys.secretKey),
|
|
92
|
+
}));
|
|
93
|
+
const sigResult = (0, package_author_signing_1.verifyPackageAuthorSignature)(result.payload, wrongKeys.publicKey);
|
|
94
|
+
expect(sigResult.valid).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
it("defaults versionTag to stable", async () => {
|
|
97
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts());
|
|
98
|
+
expect(result.pointer.versionTag).toBe("stable");
|
|
99
|
+
expect(result.payload.versionTag).toBe("stable");
|
|
100
|
+
});
|
|
101
|
+
it("respects custom versionTag", async () => {
|
|
102
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts({ versionTag: "beta" }));
|
|
103
|
+
expect(result.pointer.versionTag).toBe("beta");
|
|
104
|
+
expect(result.payload.versionTag).toBe("beta");
|
|
105
|
+
expect(result.pointer.path).toBe("v1.0.0-beta.peers-pkg.tar.gz");
|
|
106
|
+
});
|
|
107
|
+
it("uses provided packageVersionId", async () => {
|
|
108
|
+
const pvId = (0, utils_1.newid)();
|
|
109
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts({ packageVersionId: pvId }));
|
|
110
|
+
expect(result.pointer.packageVersionId).toBe(pvId);
|
|
111
|
+
expect(result.payload.packageVersionId).toBe(pvId);
|
|
112
|
+
});
|
|
113
|
+
it("generates packageVersionId when not provided", async () => {
|
|
114
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts());
|
|
115
|
+
expect(result.pointer.packageVersionId).toHaveLength(25);
|
|
116
|
+
expect(result.payload.packageVersionId).toBe(result.pointer.packageVersionId);
|
|
117
|
+
});
|
|
118
|
+
it("pointer path follows naming convention", async () => {
|
|
119
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts({ version: "2.5.1", versionTag: "stable" }));
|
|
120
|
+
expect(result.pointer.path).toBe("v2.5.1-stable.peers-pkg.tar.gz");
|
|
121
|
+
});
|
|
122
|
+
it("pointer contains publishedAt ISO timestamp", async () => {
|
|
123
|
+
const before = new Date().toISOString();
|
|
124
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts());
|
|
125
|
+
const after = new Date().toISOString();
|
|
126
|
+
expect(result.pointer.publishedAt).toBeDefined();
|
|
127
|
+
expect(result.pointer.publishedAt >= before).toBe(true);
|
|
128
|
+
expect(result.pointer.publishedAt <= after).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it("bundle hashes in payload match actual content hashes", async () => {
|
|
131
|
+
const bundles = {
|
|
132
|
+
packageBundle: "const x = 1;",
|
|
133
|
+
routesBundle: "const r = 2;",
|
|
134
|
+
uiBundle: "const u = 3;",
|
|
135
|
+
};
|
|
136
|
+
const result = await (0, package_publisher_1.buildPublishArtifacts)(makeOpts({ bundles }));
|
|
137
|
+
expect(result.payload.packageBundleFileHash).toBe((0, file_types_1.computeFileHash)(bundles.packageBundle));
|
|
138
|
+
expect(result.payload.routesBundleFileHash).toBe((0, file_types_1.computeFileHash)(bundles.routesBundle));
|
|
139
|
+
expect(result.payload.uiBundleFileHash).toBe((0, file_types_1.computeFileHash)(bundles.uiBundle));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote package update checker.
|
|
3
|
+
*
|
|
4
|
+
* Provides a memoized mechanism for checking a package's `updateUrl` for
|
|
5
|
+
* new versions and installing them into local data contexts.
|
|
6
|
+
*/
|
|
7
|
+
import type { DataContext } from "../context/data-context";
|
|
8
|
+
import { type IPackageVersion } from "../data/package-versions";
|
|
9
|
+
import { type IPackage } from "../data/packages";
|
|
10
|
+
import { type IUnpackedTarball } from "./package-tarball";
|
|
11
|
+
import type { IHttpDeps } from "./types";
|
|
12
|
+
/**
|
|
13
|
+
* Schema for the `latest-<tag>.json` pointer file hosted at `<updateUrl>/`.
|
|
14
|
+
*/
|
|
15
|
+
export interface ILatestPointer {
|
|
16
|
+
packageVersionId: string;
|
|
17
|
+
version: string;
|
|
18
|
+
versionTag: string;
|
|
19
|
+
path: string;
|
|
20
|
+
sha256: string;
|
|
21
|
+
publishedAt: string;
|
|
22
|
+
}
|
|
23
|
+
/** Verified result from a remote check — ready to be installed into group contexts. */
|
|
24
|
+
export interface IRemoteCheckResult {
|
|
25
|
+
packageId: string;
|
|
26
|
+
pointer: ILatestPointer;
|
|
27
|
+
unpacked: IUnpackedTarball;
|
|
28
|
+
}
|
|
29
|
+
/** Result from checking and installing a remote version into one group context. */
|
|
30
|
+
export interface IRemoteInstallResult {
|
|
31
|
+
activated: boolean;
|
|
32
|
+
packageVersion: IPackageVersion;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check a package's remote `updateUrl` for a new version. Memoized per
|
|
36
|
+
* `(updateUrl, tag)` for the lifetime of the process — re-check requires restart.
|
|
37
|
+
*
|
|
38
|
+
* @returns Verified payload + bundle data if a new version is available, null otherwise.
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkPackageRemoteForNewVersion(packageId: string, updateUrl: string, tag: string, http: IHttpDeps): Promise<IRemoteCheckResult | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Check a remote once, then install it into this specific group only when this
|
|
43
|
+
* group does not already have the remote PackageVersion.
|
|
44
|
+
*/
|
|
45
|
+
export declare function checkAndInstallPackageRemoteVersion(dataContext: DataContext, pkg: IPackage, tag: string, http: IHttpDeps): Promise<IRemoteInstallResult | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Install a verified remote package version into a data context.
|
|
48
|
+
* Saves bundle files, creates the PV record, and runs activation policy.
|
|
49
|
+
*/
|
|
50
|
+
export declare function installRemotePackageVersion(dataContext: DataContext, result: IRemoteCheckResult): Promise<IRemoteInstallResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Clear the memoization cache. Primarily for testing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function clearRemoteCheckCache(): void;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Remote package update checker.
|
|
4
|
+
*
|
|
5
|
+
* Provides a memoized mechanism for checking a package's `updateUrl` for
|
|
6
|
+
* new versions and installing them into local data contexts.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.checkPackageRemoteForNewVersion = checkPackageRemoteForNewVersion;
|
|
10
|
+
exports.checkAndInstallPackageRemoteVersion = checkAndInstallPackageRemoteVersion;
|
|
11
|
+
exports.installRemotePackageVersion = installRemotePackageVersion;
|
|
12
|
+
exports.clearRemoteCheckCache = clearRemoteCheckCache;
|
|
13
|
+
const file_types_1 = require("../data/files/file.types");
|
|
14
|
+
const package_versions_1 = require("../data/package-versions");
|
|
15
|
+
const packages_1 = require("../data/packages");
|
|
16
|
+
const keys_1 = require("../keys");
|
|
17
|
+
const package_version_resolver_1 = require("../data/package-version-resolver");
|
|
18
|
+
const package_author_signing_1 = require("./package-author-signing");
|
|
19
|
+
const package_installer_1 = require("./package-installer");
|
|
20
|
+
const package_propagation_1 = require("./package-propagation");
|
|
21
|
+
const package_tarball_1 = require("./package-tarball");
|
|
22
|
+
// Module-level memoization: unique by `updateUrl + ":" + tag`
|
|
23
|
+
const remoteCheckCache = new Map();
|
|
24
|
+
/**
|
|
25
|
+
* Check a package's remote `updateUrl` for a new version. Memoized per
|
|
26
|
+
* `(updateUrl, tag)` for the lifetime of the process — re-check requires restart.
|
|
27
|
+
*
|
|
28
|
+
* @returns Verified payload + bundle data if a new version is available, null otherwise.
|
|
29
|
+
*/
|
|
30
|
+
function checkPackageRemoteForNewVersion(packageId, updateUrl, tag, http) {
|
|
31
|
+
const cacheKey = `${updateUrl}:${tag}`;
|
|
32
|
+
const existing = remoteCheckCache.get(cacheKey);
|
|
33
|
+
if (existing)
|
|
34
|
+
return existing;
|
|
35
|
+
const promise = doRemoteCheck(packageId, updateUrl, tag, http).catch((err) => {
|
|
36
|
+
console.error(`[remote-checker] Error checking ${updateUrl} for ${tag}:`, err);
|
|
37
|
+
return null;
|
|
38
|
+
});
|
|
39
|
+
remoteCheckCache.set(cacheKey, promise);
|
|
40
|
+
return promise;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check a remote once, then install it into this specific group only when this
|
|
44
|
+
* group does not already have the remote PackageVersion.
|
|
45
|
+
*/
|
|
46
|
+
async function checkAndInstallPackageRemoteVersion(dataContext, pkg, tag, http) {
|
|
47
|
+
if (!pkg.updateUrl || pkg.disabled)
|
|
48
|
+
return null;
|
|
49
|
+
const result = await checkPackageRemoteForNewVersion(pkg.packageId, pkg.updateUrl, tag, http);
|
|
50
|
+
if (!result)
|
|
51
|
+
return null;
|
|
52
|
+
const existing = await (0, package_versions_1.PackageVersions)(dataContext).get(result.pointer.packageVersionId);
|
|
53
|
+
if (existing)
|
|
54
|
+
return null;
|
|
55
|
+
return installRemotePackageVersion(dataContext, result);
|
|
56
|
+
}
|
|
57
|
+
async function doRemoteCheck(packageId, updateUrl, tag, http) {
|
|
58
|
+
const pointerUrl = `${updateUrl}/latest-${tag}.json`;
|
|
59
|
+
const pointer = await http.fetchJson(pointerUrl);
|
|
60
|
+
if (!pointer?.packageVersionId || !pointer?.path) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const tarballUrl = `${updateUrl}/${pointer.path}`;
|
|
64
|
+
const tarballData = await http.fetchBinary(tarballUrl);
|
|
65
|
+
// Verify tarball integrity
|
|
66
|
+
const actualHash = (0, keys_1.hashBytes)(tarballData);
|
|
67
|
+
if (pointer.sha256 && actualHash !== pointer.sha256) {
|
|
68
|
+
throw new Error(`Tarball integrity check failed: expected ${pointer.sha256}, got ${actualHash}`);
|
|
69
|
+
}
|
|
70
|
+
const unpacked = await (0, package_tarball_1.unpackPeersTarball)(tarballData);
|
|
71
|
+
verifyPayloadMatchesPointer(packageId, pointer, unpacked);
|
|
72
|
+
// Verify bundle hashes match what the payload claims
|
|
73
|
+
const actualPkgHash = (0, file_types_1.computeFileHash)(unpacked.packageBundle);
|
|
74
|
+
if (actualPkgHash !== unpacked.payload.packageBundleFileHash) {
|
|
75
|
+
throw new Error("Package bundle hash does not match payload");
|
|
76
|
+
}
|
|
77
|
+
verifyOptionalBundleHash("Routes", unpacked.routesBundle, unpacked.payload.routesBundleFileHash);
|
|
78
|
+
verifyOptionalBundleHash("UI", unpacked.uiBundle, unpacked.payload.uiBundleFileHash);
|
|
79
|
+
// Verify author signature (the payload embeds the public key for verification)
|
|
80
|
+
const sigResult = (0, package_author_signing_1.verifyPackageAuthorSignature)({
|
|
81
|
+
packageId: unpacked.payload.packageId,
|
|
82
|
+
packageVersionId: unpacked.payload.packageVersionId,
|
|
83
|
+
version: unpacked.payload.version,
|
|
84
|
+
versionTag: unpacked.payload.versionTag,
|
|
85
|
+
packageBundleFileHash: unpacked.payload.packageBundleFileHash,
|
|
86
|
+
routesBundleFileHash: unpacked.payload.routesBundleFileHash,
|
|
87
|
+
uiBundleFileHash: unpacked.payload.uiBundleFileHash,
|
|
88
|
+
appNavs: unpacked.payload.appNavs,
|
|
89
|
+
createdBy: unpacked.payload.createdBy,
|
|
90
|
+
createdAt: unpacked.payload.createdAt,
|
|
91
|
+
packageAuthorSignature: unpacked.payload.packageAuthorSignature,
|
|
92
|
+
}, unpacked.payload.publicKey);
|
|
93
|
+
if (!sigResult.valid) {
|
|
94
|
+
throw new Error("Package author signature verification failed");
|
|
95
|
+
}
|
|
96
|
+
return { packageId, pointer, unpacked };
|
|
97
|
+
}
|
|
98
|
+
function verifyPayloadMatchesPointer(packageId, pointer, unpacked) {
|
|
99
|
+
const { payload } = unpacked;
|
|
100
|
+
const mismatches = [
|
|
101
|
+
["packageId", packageId, payload.packageId],
|
|
102
|
+
["packageVersionId", pointer.packageVersionId, payload.packageVersionId],
|
|
103
|
+
["version", pointer.version, payload.version],
|
|
104
|
+
["versionTag", pointer.versionTag, payload.versionTag],
|
|
105
|
+
].filter(([, expected, actual]) => expected !== actual);
|
|
106
|
+
if (mismatches.length > 0) {
|
|
107
|
+
const details = mismatches
|
|
108
|
+
.map(([field, expected, actual]) => `${field}: expected ${expected}, got ${actual}`)
|
|
109
|
+
.join("; ");
|
|
110
|
+
throw new Error(`Payload does not match remote pointer: ${details}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function verifyOptionalBundleHash(label, bundle, expectedHash) {
|
|
114
|
+
if (bundle && !expectedHash) {
|
|
115
|
+
throw new Error(`${label} bundle is present but not covered by payload hash`);
|
|
116
|
+
}
|
|
117
|
+
if (!bundle && expectedHash) {
|
|
118
|
+
throw new Error(`${label} bundle hash is present but bundle is missing`);
|
|
119
|
+
}
|
|
120
|
+
if (!bundle || !expectedHash)
|
|
121
|
+
return;
|
|
122
|
+
const actualHash = (0, file_types_1.computeFileHash)(bundle);
|
|
123
|
+
if (actualHash !== expectedHash) {
|
|
124
|
+
throw new Error(`${label} bundle hash does not match payload`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Install a verified remote package version into a data context.
|
|
129
|
+
* Saves bundle files, creates the PV record, and runs activation policy.
|
|
130
|
+
*/
|
|
131
|
+
async function installRemotePackageVersion(dataContext, result) {
|
|
132
|
+
const { packageId, unpacked } = result;
|
|
133
|
+
const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
|
|
134
|
+
if (!pkg) {
|
|
135
|
+
throw new Error(`Cannot install remote package version: package ${packageId} is not installed`);
|
|
136
|
+
}
|
|
137
|
+
if (!unpacked.payload.publicKey) {
|
|
138
|
+
throw new Error("Cannot install remote package version: payload publicKey is missing");
|
|
139
|
+
}
|
|
140
|
+
if (pkg.publishPublicKey && pkg.publishPublicKey !== unpacked.payload.publicKey) {
|
|
141
|
+
throw new Error(`Package publisher key mismatch for ${packageId}: expected ${pkg.publishPublicKey}, got ${unpacked.payload.publicKey}`);
|
|
142
|
+
}
|
|
143
|
+
// Save bundle files
|
|
144
|
+
const pkgFile = await (0, package_installer_1.saveBundleFile)(dataContext, unpacked.packageBundle, `${packageId}-package.bundle.js`, packageId);
|
|
145
|
+
let routesFile;
|
|
146
|
+
if (unpacked.routesBundle) {
|
|
147
|
+
routesFile = await (0, package_installer_1.saveBundleFile)(dataContext, unpacked.routesBundle, `${packageId}-routes.bundle.js`, packageId);
|
|
148
|
+
}
|
|
149
|
+
let uiFile;
|
|
150
|
+
if (unpacked.uiBundle) {
|
|
151
|
+
uiFile = await (0, package_installer_1.saveBundleFile)(dataContext, unpacked.uiBundle, `${packageId}-uis.bundle.js`, packageId);
|
|
152
|
+
}
|
|
153
|
+
// Create PV record
|
|
154
|
+
const pvHash = (0, package_versions_1.computePackageVersionHash)(unpacked.payload.version, unpacked.payload.versionTag, pkgFile.fileHash, routesFile?.fileHash, uiFile?.fileHash);
|
|
155
|
+
const pv = {
|
|
156
|
+
packageVersionId: unpacked.payload.packageVersionId,
|
|
157
|
+
packageId,
|
|
158
|
+
version: unpacked.payload.version,
|
|
159
|
+
versionTag: unpacked.payload.versionTag,
|
|
160
|
+
packageVersionHash: pvHash,
|
|
161
|
+
packageBundleFileId: pkgFile.fileId,
|
|
162
|
+
packageBundleFileHash: pkgFile.fileHash,
|
|
163
|
+
routesBundleFileId: routesFile?.fileId,
|
|
164
|
+
routesBundleFileHash: routesFile?.fileHash,
|
|
165
|
+
uiBundleFileId: uiFile?.fileId,
|
|
166
|
+
uiBundleFileHash: uiFile?.fileHash,
|
|
167
|
+
appNavs: unpacked.payload.appNavs,
|
|
168
|
+
packageAuthorSignature: unpacked.payload.packageAuthorSignature,
|
|
169
|
+
signature: "",
|
|
170
|
+
createdBy: unpacked.payload.createdBy ?? packageId,
|
|
171
|
+
createdAt: unpacked.payload.createdAt ?? new Date().toISOString(),
|
|
172
|
+
};
|
|
173
|
+
const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
|
|
174
|
+
const savedPv = await pvTable.signAndSave(pv);
|
|
175
|
+
// TOFU: if package has no publishPublicKey, establish it now
|
|
176
|
+
if (!pkg.publishPublicKey) {
|
|
177
|
+
pkg.publishPublicKey = unpacked.payload.publicKey;
|
|
178
|
+
await (0, packages_1.Packages)(dataContext).signAndSave(pkg);
|
|
179
|
+
}
|
|
180
|
+
const activationResult = await (0, package_propagation_1.activateBestEligibleVersion)(dataContext, pkg);
|
|
181
|
+
// Also run the device-level resolver so this device picks up the new version
|
|
182
|
+
// (activateBestEligibleVersion only updates the group-level default).
|
|
183
|
+
const resolveResult = await (0, package_version_resolver_1.resolveDevicePackageVersion)(pkg, dataContext, { force: true });
|
|
184
|
+
return {
|
|
185
|
+
activated: activationResult?.activated || resolveResult.upgraded,
|
|
186
|
+
packageVersion: savedPv,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Clear the memoization cache. Primarily for testing.
|
|
191
|
+
*/
|
|
192
|
+
function clearRemoteCheckCache() {
|
|
193
|
+
remoteCheckCache.clear();
|
|
194
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|