@peers-app/peers-sdk 0.25.10 → 0.25.12
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/index.d.ts +1 -1
- package/dist/contracts/index.js +5 -1
- package/dist/contracts/system/local-folders-contract.d.ts +49 -9
- package/dist/contracts/system/local-folders-contract.js +43 -13
- package/dist/package-installer/index.d.ts +1 -0
- package/dist/package-installer/index.js +1 -0
- package/dist/package-installer/package-author-signing.d.ts +16 -1
- package/dist/package-installer/package-author-signing.js +21 -2
- package/dist/package-installer/package-propagation.js +12 -0
- package/dist/package-installer/package-propagation.test.js +124 -0
- package/dist/package-installer/package-remote-checker.js +7 -2
- package/dist/package-installer/package-remote-checker.test.js +65 -0
- package/dist/package-installer/peers-core-installer.d.ts +4 -0
- package/dist/package-installer/peers-core-installer.js +6 -0
- package/dist/package-installer/peers-core-installer.test.d.ts +1 -0
- package/dist/package-installer/peers-core-installer.test.js +49 -0
- package/dist/package-installer/peers-core-trust-anchor.d.ts +47 -0
- package/dist/package-installer/peers-core-trust-anchor.js +62 -0
- package/dist/package-installer/peers-core-trust-anchor.test.d.ts +1 -0
- package/dist/package-installer/peers-core-trust-anchor.test.js +155 -0
- package/dist/system-ids.d.ts +23 -2
- package/dist/system-ids.js +27 -3
- package/package.json +6 -4
|
@@ -15,7 +15,7 @@ export { createRemoteContractEndpoint, getRemoteContractTransportFactory, type I
|
|
|
15
15
|
export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./system/device-operations-contract";
|
|
16
16
|
export { getIdentityContractDefinition, IDENTITY_CONTRACT_VERSION, type IIdentityConsumer, type IIdentitySessionInfo, } from "./system/identity-contract";
|
|
17
17
|
export { getInvitesContractDefinition, type IAcceptInviteResult, type ICreatedInvite, type IInvitePreview, type IInvitesConsumer, INVITES_CONTRACT_VERSION, } from "./system/invites-contract";
|
|
18
|
-
export { getLocalFoldersContractDefinition, type ILocalFolderChange, type ILocalFolderChangedEvent, type ILocalFolderEntry, type ILocalFolderFileBlocks, type ILocalFolderFileBlocksInput, type ILocalFolderGrantInfo, type ILocalFolderIndexInput, type ILocalFolderJobFinishedEvent, type ILocalFolderJobHandle, type ILocalFolderJobInfo, type ILocalFolderJobProgressEvent, type ILocalFolderListInput, type ILocalFolderMoveInput, type ILocalFolderPathInput, type ILocalFolderPurgeTrashInput, type ILocalFolderReadRangeInput, type ILocalFolderReadRangeResult, type ILocalFolderRequestGrantInput, type ILocalFolderStatInput, LOCAL_FOLDERS_CHUNK_AVG, LOCAL_FOLDERS_CHUNK_MAX, LOCAL_FOLDERS_CHUNK_MIN, LOCAL_FOLDERS_CONTRACT_VERSION, LOCAL_FOLDERS_JOB_TTL_MS, LOCAL_FOLDERS_READ_RANGE_MAX, LOCAL_FOLDERS_RESERVED_DIR, type LocalFolderChangeKind, type LocalFolderEntryKind, type LocalFolderMode, localFoldersProviderPackageId, } from "./system/local-folders-contract";
|
|
18
|
+
export { getLocalFoldersContractDefinition, type ILocalFolderAssembleInput, type ILocalFolderAssembleResult, type ILocalFolderChange, type ILocalFolderChangedEvent, type ILocalFolderEntry, type ILocalFolderFileBlocks, type ILocalFolderFileBlocksInput, type ILocalFolderGrantInfo, type ILocalFolderIndexInput, type ILocalFolderIndexResult, type ILocalFolderJobFinishedEvent, type ILocalFolderJobHandle, type ILocalFolderJobInfo, type ILocalFolderJobProgressEvent, type ILocalFolderListInput, type ILocalFolderManifestBlock, type ILocalFolderMoveInput, type ILocalFolderPathInput, type ILocalFolderPurgeTrashInput, type ILocalFolderReadRangeInput, type ILocalFolderReadRangeResult, type ILocalFolderRequestGrantInput, type ILocalFolderStatInput, LOCAL_FOLDERS_ASSEMBLE_STALL_MS, LOCAL_FOLDERS_CHUNK_AVG, LOCAL_FOLDERS_CHUNK_MAX, LOCAL_FOLDERS_CHUNK_MIN, LOCAL_FOLDERS_CONTRACT_VERSION, LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS, LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES, LOCAL_FOLDERS_HAVE_BATCH, LOCAL_FOLDERS_JOB_TTL_MS, LOCAL_FOLDERS_READ_RANGE_MAX, LOCAL_FOLDERS_RESERVED_DIR, type LocalFolderChangeKind, type LocalFolderEntryKind, type LocalFolderJobKind, type LocalFolderMode, localFoldersProviderPackageId, } from "./system/local-folders-contract";
|
|
19
19
|
export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./system/logs-contract";
|
|
20
20
|
export * from "./system/network-contracts";
|
|
21
21
|
export { getToolAccessControlContractDefinition, type IToolAccessControlConsumer, TOOL_ACCESS_CONTROL_CONTRACT_VERSION, } from "./system/tool-access-control-contract";
|
package/dist/contracts/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.DEVICE_OPERATIONS_MAX_ROWS = exports.DEVICE_OPERATIONS_MAX_RESULT_BYTES = exports.DEVICE_OPERATIONS_MAX_QUERY_LENGTH = exports.DEVICE_OPERATIONS_CONTRACT_VERSION = exports.setRemoteContractTransportFactory = exports.resolveRemoteTargetEndpoint = exports.getRemoteContractTransportFactory = exports.createRemoteContractEndpoint = exports.ContractRegistry = exports.PersistentContractRegistry = exports.waitForHostContractTransport = exports.subscribeHostContractTransport = exports.registerHostContractTransport = exports.getHostContractTransport = exports.hostContractConsumer = exports.setSchemaToFieldsFn = exports.extractToolShape = exports.extractTableShape = exports.createLocalContractConsumer = exports.contractsSchema = exports.contractsMetaData = exports.ContractsTable = exports.Contracts = exports.inProcessTransportPair = exports.connectionContractTransport = exports.CONTRACT_REQUEST_CHANNEL = exports.CONTRACT_NOTIFY_CHANNEL = exports.verifyRemoteContractIdentity = exports.registerContractResolver = exports.isSelfTrustedContractCaller = exports.handleContractCall = exports.getContractResolver = exports.createUserContextContractProviderRouter = exports.createLocalUserContextContractProviderRouter = exports.authorizeRemoteContractCall = exports.sameUserContractPermissionCheck = exports.createRemoteContractConsumer = exports.createContractProviderSession = exports.createContractProviderEndpoint = exports.createContractProvider = exports.createContractConsumer = exports.CONTRACT_CALL_RPC = exports.contractProvidersSchema = exports.contractProvidersMetaData = exports.ContractProvidersTable = exports.ContractProviders = exports.createContractProviderRouter = exports.PackageBuilder = exports.definePackage = exports.ContractBuilder = void 0;
|
|
18
|
-
exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.registerSystemContract = exports.listSystemContracts = exports.installSystemContracts = exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = exports.getToolAccessControlContractDefinition = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.localFoldersProviderPackageId = exports.LOCAL_FOLDERS_RESERVED_DIR = exports.LOCAL_FOLDERS_READ_RANGE_MAX = exports.LOCAL_FOLDERS_JOB_TTL_MS = exports.LOCAL_FOLDERS_CONTRACT_VERSION = exports.LOCAL_FOLDERS_CHUNK_MIN = exports.LOCAL_FOLDERS_CHUNK_MAX = exports.LOCAL_FOLDERS_CHUNK_AVG = exports.getLocalFoldersContractDefinition = exports.INVITES_CONTRACT_VERSION = exports.getInvitesContractDefinition = exports.IDENTITY_CONTRACT_VERSION = exports.getIdentityContractDefinition = exports.getDeviceOperationsContractDefinition = exports.deviceOperationsToolIds = exports.deviceOperationsProviderPackageId = exports.DEVICE_OPERATIONS_MAX_TOOLS = void 0;
|
|
18
|
+
exports.validateProviderSatisfiesContract = exports.validateImmutability = exports.validateAlsoImplements = exports.contractKey = exports.registerSystemContract = exports.listSystemContracts = exports.installSystemContracts = exports.TOOL_ACCESS_CONTROL_CONTRACT_VERSION = exports.getToolAccessControlContractDefinition = exports.resolveLogsContract = exports.LOGS_CONTRACT_VERSION = exports.getLogsContractDefinition = exports.localFoldersProviderPackageId = exports.LOCAL_FOLDERS_RESERVED_DIR = exports.LOCAL_FOLDERS_READ_RANGE_MAX = exports.LOCAL_FOLDERS_JOB_TTL_MS = exports.LOCAL_FOLDERS_HAVE_BATCH = exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES = exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS = exports.LOCAL_FOLDERS_CONTRACT_VERSION = exports.LOCAL_FOLDERS_CHUNK_MIN = exports.LOCAL_FOLDERS_CHUNK_MAX = exports.LOCAL_FOLDERS_CHUNK_AVG = exports.LOCAL_FOLDERS_ASSEMBLE_STALL_MS = exports.getLocalFoldersContractDefinition = exports.INVITES_CONTRACT_VERSION = exports.getInvitesContractDefinition = exports.IDENTITY_CONTRACT_VERSION = exports.getIdentityContractDefinition = exports.getDeviceOperationsContractDefinition = exports.deviceOperationsToolIds = exports.deviceOperationsProviderPackageId = exports.DEVICE_OPERATIONS_MAX_TOOLS = void 0;
|
|
19
19
|
var builder_1 = require("./builder");
|
|
20
20
|
Object.defineProperty(exports, "ContractBuilder", { enumerable: true, get: function () { return builder_1.ContractBuilder; } });
|
|
21
21
|
Object.defineProperty(exports, "definePackage", { enumerable: true, get: function () { return builder_1.definePackage; } });
|
|
@@ -93,10 +93,14 @@ Object.defineProperty(exports, "getInvitesContractDefinition", { enumerable: tru
|
|
|
93
93
|
Object.defineProperty(exports, "INVITES_CONTRACT_VERSION", { enumerable: true, get: function () { return invites_contract_1.INVITES_CONTRACT_VERSION; } });
|
|
94
94
|
var local_folders_contract_1 = require("./system/local-folders-contract");
|
|
95
95
|
Object.defineProperty(exports, "getLocalFoldersContractDefinition", { enumerable: true, get: function () { return local_folders_contract_1.getLocalFoldersContractDefinition; } });
|
|
96
|
+
Object.defineProperty(exports, "LOCAL_FOLDERS_ASSEMBLE_STALL_MS", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_ASSEMBLE_STALL_MS; } });
|
|
96
97
|
Object.defineProperty(exports, "LOCAL_FOLDERS_CHUNK_AVG", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_CHUNK_AVG; } });
|
|
97
98
|
Object.defineProperty(exports, "LOCAL_FOLDERS_CHUNK_MAX", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_CHUNK_MAX; } });
|
|
98
99
|
Object.defineProperty(exports, "LOCAL_FOLDERS_CHUNK_MIN", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_CHUNK_MIN; } });
|
|
99
100
|
Object.defineProperty(exports, "LOCAL_FOLDERS_CONTRACT_VERSION", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_CONTRACT_VERSION; } });
|
|
101
|
+
Object.defineProperty(exports, "LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS; } });
|
|
102
|
+
Object.defineProperty(exports, "LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES; } });
|
|
103
|
+
Object.defineProperty(exports, "LOCAL_FOLDERS_HAVE_BATCH", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_HAVE_BATCH; } });
|
|
100
104
|
Object.defineProperty(exports, "LOCAL_FOLDERS_JOB_TTL_MS", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_JOB_TTL_MS; } });
|
|
101
105
|
Object.defineProperty(exports, "LOCAL_FOLDERS_READ_RANGE_MAX", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_READ_RANGE_MAX; } });
|
|
102
106
|
Object.defineProperty(exports, "LOCAL_FOLDERS_RESERVED_DIR", { enumerable: true, get: function () { return local_folders_contract_1.LOCAL_FOLDERS_RESERVED_DIR; } });
|
|
@@ -11,8 +11,20 @@ export declare const LOCAL_FOLDERS_CHUNK_AVG: number;
|
|
|
11
11
|
export declare const LOCAL_FOLDERS_CHUNK_MAX: number;
|
|
12
12
|
/** Directory name reserved inside every grant for trash and in-flight files. */
|
|
13
13
|
export declare const LOCAL_FOLDERS_RESERVED_DIR = ".peers";
|
|
14
|
-
/** How long finished index jobs stay readable via `getJob`. */
|
|
14
|
+
/** How long finished index and assemble jobs stay readable via `getJob`. */
|
|
15
15
|
export declare const LOCAL_FOLDERS_JOB_TTL_MS: number;
|
|
16
|
+
/**
|
|
17
|
+
* How long an assemble job waits without receiving a block before it fails
|
|
18
|
+
* with `stalled`. A later assemble of the same block list resumes from the
|
|
19
|
+
* partial file.
|
|
20
|
+
*/
|
|
21
|
+
export declare const LOCAL_FOLDERS_ASSEMBLE_STALL_MS: number;
|
|
22
|
+
/** Maximum hashes in one `haveBlocks` request on the block-exchange path. */
|
|
23
|
+
export declare const LOCAL_FOLDERS_HAVE_BATCH = 2000;
|
|
24
|
+
/** Maximum blocks in one `getBlocks` request. */
|
|
25
|
+
export declare const LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS = 32;
|
|
26
|
+
/** Maximum total bytes in one `getBlocks` request. */
|
|
27
|
+
export declare const LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES: number;
|
|
16
28
|
/** Package ID used by live System Local Folders tool instances. */
|
|
17
29
|
export declare const localFoldersProviderPackageId = "000peers0system0000000001";
|
|
18
30
|
/** Grant mode accepted by `requestGrant`. */
|
|
@@ -101,20 +113,48 @@ export interface ILocalFolderFileBlocks {
|
|
|
101
113
|
size: number;
|
|
102
114
|
}[];
|
|
103
115
|
}
|
|
104
|
-
/**
|
|
116
|
+
/** One content-defined block in an assemble manifest. */
|
|
117
|
+
export interface ILocalFolderManifestBlock {
|
|
118
|
+
hash: string;
|
|
119
|
+
size: number;
|
|
120
|
+
}
|
|
121
|
+
/** Input for `assemble`. `blocks` is the ordered manifest to write. */
|
|
122
|
+
export interface ILocalFolderAssembleInput {
|
|
123
|
+
grantId: string;
|
|
124
|
+
/** Relative file path to create or replace. */
|
|
125
|
+
relPath: string;
|
|
126
|
+
blocks: ILocalFolderManifestBlock[];
|
|
127
|
+
/** mtime applied to the finished file, in unix epoch milliseconds. */
|
|
128
|
+
mtimeMs?: number;
|
|
129
|
+
/** Unix permission bits. Only the low 9 bits are applied. */
|
|
130
|
+
mode?: number;
|
|
131
|
+
/** Device ids allowed to serve blocks. Omit to ask every eligible peer. */
|
|
132
|
+
sources?: string[];
|
|
133
|
+
}
|
|
134
|
+
/** Counts from a finished index job. */
|
|
135
|
+
export interface ILocalFolderIndexResult {
|
|
136
|
+
files: number;
|
|
137
|
+
rehashed: number;
|
|
138
|
+
skipped: number;
|
|
139
|
+
removed: number;
|
|
140
|
+
}
|
|
141
|
+
/** Counts from a finished assemble job. `sources` is how many peers sent bytes. */
|
|
142
|
+
export interface ILocalFolderAssembleResult {
|
|
143
|
+
bytesPulled: number;
|
|
144
|
+
bytesReused: number;
|
|
145
|
+
sources: number;
|
|
146
|
+
}
|
|
147
|
+
/** Host job kind reported by `getJob`. */
|
|
148
|
+
export type LocalFolderJobKind = "index" | "assemble";
|
|
149
|
+
/** Index or assemble job snapshot returned by `getJob`. */
|
|
105
150
|
export interface ILocalFolderJobInfo {
|
|
106
151
|
jobId: string;
|
|
107
|
-
kind:
|
|
152
|
+
kind: LocalFolderJobKind;
|
|
108
153
|
status: "running" | "done" | "error" | "cancelled";
|
|
109
154
|
done: number;
|
|
110
155
|
total: number;
|
|
111
156
|
error?: string;
|
|
112
|
-
result?:
|
|
113
|
-
files: number;
|
|
114
|
-
rehashed: number;
|
|
115
|
-
skipped: number;
|
|
116
|
-
removed: number;
|
|
117
|
-
};
|
|
157
|
+
result?: ILocalFolderIndexResult | ILocalFolderAssembleResult;
|
|
118
158
|
}
|
|
119
159
|
/** Input for `move`. */
|
|
120
160
|
export interface ILocalFolderMoveInput {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.localFoldersProviderPackageId = exports.LOCAL_FOLDERS_JOB_TTL_MS = exports.LOCAL_FOLDERS_RESERVED_DIR = exports.LOCAL_FOLDERS_CHUNK_MAX = exports.LOCAL_FOLDERS_CHUNK_AVG = exports.LOCAL_FOLDERS_CHUNK_MIN = exports.LOCAL_FOLDERS_READ_RANGE_MAX = exports.LOCAL_FOLDERS_CONTRACT_VERSION = void 0;
|
|
3
|
+
exports.localFoldersProviderPackageId = exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES = exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS = exports.LOCAL_FOLDERS_HAVE_BATCH = exports.LOCAL_FOLDERS_ASSEMBLE_STALL_MS = exports.LOCAL_FOLDERS_JOB_TTL_MS = exports.LOCAL_FOLDERS_RESERVED_DIR = exports.LOCAL_FOLDERS_CHUNK_MAX = exports.LOCAL_FOLDERS_CHUNK_AVG = exports.LOCAL_FOLDERS_CHUNK_MIN = exports.LOCAL_FOLDERS_READ_RANGE_MAX = exports.LOCAL_FOLDERS_CONTRACT_VERSION = void 0;
|
|
4
4
|
exports.getLocalFoldersContractDefinition = getLocalFoldersContractDefinition;
|
|
5
5
|
const system_ids_1 = require("../../system-ids");
|
|
6
6
|
const field_type_1 = require("../../types/field-type");
|
|
@@ -17,8 +17,20 @@ exports.LOCAL_FOLDERS_CHUNK_AVG = 1024 * 1024;
|
|
|
17
17
|
exports.LOCAL_FOLDERS_CHUNK_MAX = 4 * 1024 * 1024;
|
|
18
18
|
/** Directory name reserved inside every grant for trash and in-flight files. */
|
|
19
19
|
exports.LOCAL_FOLDERS_RESERVED_DIR = ".peers";
|
|
20
|
-
/** How long finished index jobs stay readable via `getJob`. */
|
|
20
|
+
/** How long finished index and assemble jobs stay readable via `getJob`. */
|
|
21
21
|
exports.LOCAL_FOLDERS_JOB_TTL_MS = 60 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* How long an assemble job waits without receiving a block before it fails
|
|
24
|
+
* with `stalled`. A later assemble of the same block list resumes from the
|
|
25
|
+
* partial file.
|
|
26
|
+
*/
|
|
27
|
+
exports.LOCAL_FOLDERS_ASSEMBLE_STALL_MS = 10 * 60 * 1000;
|
|
28
|
+
/** Maximum hashes in one `haveBlocks` request on the block-exchange path. */
|
|
29
|
+
exports.LOCAL_FOLDERS_HAVE_BATCH = 2000;
|
|
30
|
+
/** Maximum blocks in one `getBlocks` request. */
|
|
31
|
+
exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS = 32;
|
|
32
|
+
/** Maximum total bytes in one `getBlocks` request. */
|
|
33
|
+
exports.LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES = 64 * 1024 * 1024;
|
|
22
34
|
/** Package ID used by live System Local Folders tool instances. */
|
|
23
35
|
exports.localFoldersProviderPackageId = system_ids_1.peersSystemPackageId;
|
|
24
36
|
const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
|
|
@@ -36,7 +48,7 @@ function getLocalFoldersContractDefinition() {
|
|
|
36
48
|
version: exports.LOCAL_FOLDERS_CONTRACT_VERSION,
|
|
37
49
|
devTag: "dev",
|
|
38
50
|
name: "System Local Folders",
|
|
39
|
-
description: "Scoped access to operator-granted folders on this device: listing, bounded reads, indexing, move, and trash.",
|
|
51
|
+
description: "Scoped access to operator-granted folders on this device: listing, bounded reads, indexing, assemble, move, and trash.",
|
|
40
52
|
tables: [],
|
|
41
53
|
tools: [
|
|
42
54
|
{
|
|
@@ -144,26 +156,44 @@ function getLocalFoldersContractDefinition() {
|
|
|
144
156
|
}),
|
|
145
157
|
],
|
|
146
158
|
},
|
|
159
|
+
{
|
|
160
|
+
name: "assemble",
|
|
161
|
+
usageDescription: "Start a host job that writes a file from an ordered block list. Missing blocks are pulled from opted-in devices. Requires a readwrite grant.",
|
|
162
|
+
inputFields: [
|
|
163
|
+
grantIdField,
|
|
164
|
+
field("relPath", field_type_1.FieldType.string, "Relative file path to create or replace."),
|
|
165
|
+
field("blocks", field_type_1.FieldType.object, "Ordered blocks with hash and size.", {
|
|
166
|
+
isArray: true,
|
|
167
|
+
}),
|
|
168
|
+
field("mtimeMs", field_type_1.FieldType.number, "mtime applied to the finished file.", {
|
|
169
|
+
optional: true,
|
|
170
|
+
}),
|
|
171
|
+
field("mode", field_type_1.FieldType.number, "Unix permission bits (low 9 bits).", { optional: true }),
|
|
172
|
+
field("sources", field_type_1.FieldType.id, "Device ids allowed to serve blocks.", {
|
|
173
|
+
optional: true,
|
|
174
|
+
isArray: true,
|
|
175
|
+
}),
|
|
176
|
+
],
|
|
177
|
+
outputFields: [field("jobId", field_type_1.FieldType.id, "Assemble job id.")],
|
|
178
|
+
},
|
|
147
179
|
{
|
|
148
180
|
name: "getJob",
|
|
149
|
-
usageDescription: "Return the status of an index job, including after it finishes.",
|
|
150
|
-
inputFields: [field("jobId", field_type_1.FieldType.id, "Job id returned by index.")],
|
|
181
|
+
usageDescription: "Return the status of an index or assemble job, including after it finishes.",
|
|
182
|
+
inputFields: [field("jobId", field_type_1.FieldType.id, "Job id returned by index or assemble.")],
|
|
151
183
|
outputFields: [
|
|
152
184
|
field("jobId", field_type_1.FieldType.id, "Job id."),
|
|
153
|
-
field("kind", field_type_1.FieldType.string,
|
|
185
|
+
field("kind", field_type_1.FieldType.string, 'Job kind: "index" or "assemble".'),
|
|
154
186
|
field("status", field_type_1.FieldType.string, "running, done, error, or cancelled."),
|
|
155
187
|
field("done", field_type_1.FieldType.number, "Units completed."),
|
|
156
188
|
field("total", field_type_1.FieldType.number, "Units discovered."),
|
|
157
189
|
field("error", field_type_1.FieldType.string, "Failure message.", { optional: true }),
|
|
158
|
-
field("result", field_type_1.FieldType.object, "Index counts when status is done.", {
|
|
159
|
-
optional: true,
|
|
160
|
-
}),
|
|
190
|
+
field("result", field_type_1.FieldType.object, "Index counts ({ files, rehashed, skipped, removed }) or assemble counts ({ bytesPulled, bytesReused, sources }) when status is done.", { optional: true }),
|
|
161
191
|
],
|
|
162
192
|
},
|
|
163
193
|
{
|
|
164
194
|
name: "cancelJob",
|
|
165
|
-
usageDescription: "Cancel a running index job.",
|
|
166
|
-
inputFields: [field("jobId", field_type_1.FieldType.id, "Job id returned by index.")],
|
|
195
|
+
usageDescription: "Cancel a running index or assemble job. An assemble keeps its partial file.",
|
|
196
|
+
inputFields: [field("jobId", field_type_1.FieldType.id, "Job id returned by index or assemble.")],
|
|
167
197
|
outputFields: [field("cancelled", field_type_1.FieldType.boolean, "Whether the job was cancelled.")],
|
|
168
198
|
},
|
|
169
199
|
{
|
|
@@ -207,7 +237,7 @@ function getLocalFoldersContractDefinition() {
|
|
|
207
237
|
},
|
|
208
238
|
{
|
|
209
239
|
name: "jobProgress",
|
|
210
|
-
description: "Progress for a host index job.",
|
|
240
|
+
description: "Progress for a host index or assemble job.",
|
|
211
241
|
payloadFields: [
|
|
212
242
|
field("jobId", field_type_1.FieldType.id, "Job id."),
|
|
213
243
|
field("done", field_type_1.FieldType.number, "Units completed."),
|
|
@@ -216,7 +246,7 @@ function getLocalFoldersContractDefinition() {
|
|
|
216
246
|
},
|
|
217
247
|
{
|
|
218
248
|
name: "jobFinished",
|
|
219
|
-
description: "An index job finished, failed, or was cancelled.",
|
|
249
|
+
description: "An index or assemble job finished, failed, or was cancelled.",
|
|
220
250
|
payloadFields: [
|
|
221
251
|
field("jobId", field_type_1.FieldType.id, "Job id."),
|
|
222
252
|
field("status", field_type_1.FieldType.string, "done, error, or cancelled."),
|
|
@@ -26,4 +26,5 @@ __exportStar(require("./package-remote-checker"), exports);
|
|
|
26
26
|
__exportStar(require("./package-seed-installer"), exports);
|
|
27
27
|
__exportStar(require("./package-tarball"), exports);
|
|
28
28
|
__exportStar(require("./peers-core-installer"), exports);
|
|
29
|
+
__exportStar(require("./peers-core-trust-anchor"), exports);
|
|
29
30
|
__exportStar(require("./types"), exports);
|
|
@@ -2,6 +2,16 @@ import type { IPackageVersion } from "../data/package-versions";
|
|
|
2
2
|
import type { IAppNav } from "../types/app-nav";
|
|
3
3
|
/** Prefix for the persistent variable name that stores a package's signing secret key. */
|
|
4
4
|
export declare const PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
|
|
5
|
+
/**
|
|
6
|
+
* Whether `publicKey` is a publisher key that must never be trusted again.
|
|
7
|
+
*
|
|
8
|
+
* The built-in list is {@link peersCoreRevokedPublishPublicKeys}; callers (and tests) may pass
|
|
9
|
+
* their own. An empty or missing key is not "revoked", it is simply not a key.
|
|
10
|
+
*
|
|
11
|
+
* @param publicKey - base64url Ed25519 public key to check
|
|
12
|
+
* @param revoked - list to check against; defaults to the peers-core revocations
|
|
13
|
+
*/
|
|
14
|
+
export declare function isRevokedPublisherKey(publicKey: string | undefined, revoked?: readonly string[]): boolean;
|
|
5
15
|
/**
|
|
6
16
|
* The subset of PackageVersion fields that the publisher signs.
|
|
7
17
|
* The `publicKey` field is included so verifiers know which key to check against.
|
|
@@ -47,8 +57,13 @@ export interface IVerifyAuthorSignatureResult {
|
|
|
47
57
|
* is NOT in the signature itself — it must be provided via `expectedPublicKey` (TOFU check)
|
|
48
58
|
* to reconstruct the payload and verify.
|
|
49
59
|
*
|
|
60
|
+
* A key in {@link peersCoreRevokedPublishPublicKeys} never verifies, even with a
|
|
61
|
+
* mathematically valid signature: a revoked key may be in an attacker's hands, so anything it
|
|
62
|
+
* signed after revocation is untrustworthy and anything before is already installed.
|
|
63
|
+
*
|
|
50
64
|
* @param pv - The PackageVersion record with `packageAuthorSignature` set
|
|
51
65
|
* @param expectedPublicKey - The public key to verify against (from Package.publishPublicKey)
|
|
52
|
-
* @returns `{ valid, publicKey }` — valid is true if signature verifies
|
|
66
|
+
* @returns `{ valid, publicKey }` — valid is true if signature verifies, key matches, and the
|
|
67
|
+
* key is not revoked
|
|
53
68
|
*/
|
|
54
69
|
export declare function verifyPackageAuthorSignature(pv: AuthorSignableFields & Pick<IPackageVersion, "packageAuthorSignature">, expectedPublicKey: string): IVerifyAuthorSignatureResult;
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PACKAGE_SIGNING_KEY_PREFIX = void 0;
|
|
4
|
+
exports.isRevokedPublisherKey = isRevokedPublisherKey;
|
|
4
5
|
exports.buildAuthorSignedPayload = buildAuthorSignedPayload;
|
|
5
6
|
exports.signPackageAuthor = signPackageAuthor;
|
|
6
7
|
exports.verifyPackageAuthorSignature = verifyPackageAuthorSignature;
|
|
7
8
|
const nacl = require("tweetnacl");
|
|
8
9
|
const tweetnacl_util_1 = require("tweetnacl-util");
|
|
9
10
|
const keys_1 = require("../keys");
|
|
11
|
+
const system_ids_1 = require("../system-ids");
|
|
10
12
|
/** Prefix for the persistent variable name that stores a package's signing secret key. */
|
|
11
13
|
exports.PACKAGE_SIGNING_KEY_PREFIX = "packageSigningKey_";
|
|
14
|
+
/**
|
|
15
|
+
* Whether `publicKey` is a publisher key that must never be trusted again.
|
|
16
|
+
*
|
|
17
|
+
* The built-in list is {@link peersCoreRevokedPublishPublicKeys}; callers (and tests) may pass
|
|
18
|
+
* their own. An empty or missing key is not "revoked", it is simply not a key.
|
|
19
|
+
*
|
|
20
|
+
* @param publicKey - base64url Ed25519 public key to check
|
|
21
|
+
* @param revoked - list to check against; defaults to the peers-core revocations
|
|
22
|
+
*/
|
|
23
|
+
function isRevokedPublisherKey(publicKey, revoked = system_ids_1.peersCoreRevokedPublishPublicKeys) {
|
|
24
|
+
return !!publicKey && revoked.includes(publicKey);
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* Extracts the signable payload from a PackageVersion record.
|
|
14
28
|
* Only includes optional fields when they have a value (undefined fields are omitted
|
|
@@ -59,13 +73,18 @@ function signPackageAuthor(pv, secretKey) {
|
|
|
59
73
|
* is NOT in the signature itself — it must be provided via `expectedPublicKey` (TOFU check)
|
|
60
74
|
* to reconstruct the payload and verify.
|
|
61
75
|
*
|
|
76
|
+
* A key in {@link peersCoreRevokedPublishPublicKeys} never verifies, even with a
|
|
77
|
+
* mathematically valid signature: a revoked key may be in an attacker's hands, so anything it
|
|
78
|
+
* signed after revocation is untrustworthy and anything before is already installed.
|
|
79
|
+
*
|
|
62
80
|
* @param pv - The PackageVersion record with `packageAuthorSignature` set
|
|
63
81
|
* @param expectedPublicKey - The public key to verify against (from Package.publishPublicKey)
|
|
64
|
-
* @returns `{ valid, publicKey }` — valid is true if signature verifies
|
|
82
|
+
* @returns `{ valid, publicKey }` — valid is true if signature verifies, key matches, and the
|
|
83
|
+
* key is not revoked
|
|
65
84
|
*/
|
|
66
85
|
function verifyPackageAuthorSignature(pv, expectedPublicKey) {
|
|
67
86
|
const result = { valid: false, publicKey: expectedPublicKey };
|
|
68
|
-
if (!pv.packageAuthorSignature) {
|
|
87
|
+
if (!pv.packageAuthorSignature || isRevokedPublisherKey(expectedPublicKey)) {
|
|
69
88
|
return result;
|
|
70
89
|
}
|
|
71
90
|
try {
|
|
@@ -11,6 +11,7 @@ const package_versions_1 = require("../data/package-versions");
|
|
|
11
11
|
const packages_1 = require("../data/packages");
|
|
12
12
|
const utils_1 = require("../utils");
|
|
13
13
|
const package_author_signing_1 = require("./package-author-signing");
|
|
14
|
+
const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
|
|
14
15
|
// --- Module-level state ---
|
|
15
16
|
const RETRY_DELAYS_MS = [2_000, 10_000, 30_000, 60_000, 120_000];
|
|
16
17
|
const pvPropagateMap = {};
|
|
@@ -190,6 +191,12 @@ async function retryOrFail(source, dest, pv, retryCnt, reason) {
|
|
|
190
191
|
* transient failures (missing source key).
|
|
191
192
|
*/
|
|
192
193
|
async function verifyOrEstablishTofu(sourceContext, targetContext, targetPkg, pv) {
|
|
194
|
+
// peers-core pinned to a revoked key: move the pin to the host's current key first so a
|
|
195
|
+
// version signed by the replacement key can be accepted.
|
|
196
|
+
const repinned = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(targetContext, targetPkg);
|
|
197
|
+
if (repinned !== targetPkg) {
|
|
198
|
+
targetPkg.publishPublicKey = repinned.publishPublicKey;
|
|
199
|
+
}
|
|
193
200
|
if (targetPkg.publishPublicKey) {
|
|
194
201
|
if (!(0, package_author_signing_1.verifyPackageAuthorSignature)(pv, targetPkg.publishPublicKey).valid) {
|
|
195
202
|
console.error(`[propagation] Invalid signature for ${pv.packageVersionId}`);
|
|
@@ -201,6 +208,11 @@ async function verifyOrEstablishTofu(sourceContext, targetContext, targetPkg, pv
|
|
|
201
208
|
const sourcePkg = await (0, packages_1.Packages)(sourceContext).get(targetPkg.packageId);
|
|
202
209
|
if (!sourcePkg?.publishPublicKey)
|
|
203
210
|
return false;
|
|
211
|
+
// Never adopt a revoked key as a trust anchor, however it reached the source group.
|
|
212
|
+
if ((0, package_author_signing_1.isRevokedPublisherKey)(sourcePkg.publishPublicKey)) {
|
|
213
|
+
console.error(`[propagation] Refusing revoked publisher key for ${targetPkg.packageId} from ${sourceContext.dataContextId}`);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
204
216
|
if (!(0, package_author_signing_1.verifyPackageAuthorSignature)(pv, sourcePkg.publishPublicKey).valid) {
|
|
205
217
|
console.error(`[propagation] Invalid signature for ${pv.packageVersionId} (TOFU)`);
|
|
206
218
|
return false;
|
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const package_versions_1 = require("../data/package-versions");
|
|
4
4
|
const keys_1 = require("../keys");
|
|
5
|
+
const systemIds = require("../system-ids");
|
|
5
6
|
const utils_1 = require("../utils");
|
|
6
7
|
const package_author_signing_1 = require("./package-author-signing");
|
|
7
8
|
const package_propagation_1 = require("./package-propagation");
|
|
8
9
|
// --- Mocks ---
|
|
10
|
+
// Pin the host to a fresh "current" peers-core key and revoke a fresh "old" one so the
|
|
11
|
+
// revocation tests do not depend on the real constants.
|
|
12
|
+
jest.mock("../system-ids", () => {
|
|
13
|
+
const actual = jest.requireActual("../system-ids");
|
|
14
|
+
const { newKeys: makeKeys } = jest.requireActual("../keys");
|
|
15
|
+
const revoked = makeKeys();
|
|
16
|
+
const current = makeKeys();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
peersCorePublishPublicKey: current.publicKey,
|
|
20
|
+
peersCoreRevokedPublishPublicKeys: [revoked.publicKey],
|
|
21
|
+
__testKeys: { revoked, current },
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
const { revoked: revokedCoreKeys, current: currentCoreKeys } = systemIds.__testKeys;
|
|
9
25
|
const mockGetUserRole = jest.fn();
|
|
10
26
|
jest.mock("../data/group-permissions", () => ({
|
|
11
27
|
getUserRole: (...args) => mockGetUserRole(...args),
|
|
@@ -589,6 +605,114 @@ describe("discoverAndPropagateVersions", () => {
|
|
|
589
605
|
expect(results).toHaveLength(0);
|
|
590
606
|
consoleError.mockRestore();
|
|
591
607
|
});
|
|
608
|
+
describe("revoked publisher keys", () => {
|
|
609
|
+
function twoGroupSetup() {
|
|
610
|
+
const groupA = "group-a";
|
|
611
|
+
const groupB = "group-b";
|
|
612
|
+
const groups = new Map([
|
|
613
|
+
[groupA, makeDataContext(groupA)],
|
|
614
|
+
[groupB, makeDataContext(groupB)],
|
|
615
|
+
]);
|
|
616
|
+
const roles = new Map([
|
|
617
|
+
[groupA, 60],
|
|
618
|
+
[groupB, 60],
|
|
619
|
+
]);
|
|
620
|
+
return { groupA, groupB, userContext: makeUserContext({ userId, groups }), roles };
|
|
621
|
+
}
|
|
622
|
+
let consoleError;
|
|
623
|
+
let consoleWarn;
|
|
624
|
+
beforeEach(() => {
|
|
625
|
+
consoleError = jest.spyOn(console, "error").mockImplementation(() => { });
|
|
626
|
+
consoleWarn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
627
|
+
});
|
|
628
|
+
afterEach(() => {
|
|
629
|
+
consoleError.mockRestore();
|
|
630
|
+
consoleWarn.mockRestore();
|
|
631
|
+
});
|
|
632
|
+
it("refuses to establish TOFU from a source pinned to a revoked key", async () => {
|
|
633
|
+
const { groupA, groupB, userContext, roles } = twoGroupSetup();
|
|
634
|
+
const packageId = (0, utils_1.newid)();
|
|
635
|
+
const sourcePkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
|
|
636
|
+
const targetPkg = makePkg({ packageId, publishPublicKey: "" });
|
|
637
|
+
const pv = makeSignedPVWithSecretKey(packageId, revokedCoreKeys.secretKey, {
|
|
638
|
+
version: "2.0.0",
|
|
639
|
+
});
|
|
640
|
+
setupMocksForScenario({
|
|
641
|
+
packages: new Map([[groupB, [targetPkg]]]),
|
|
642
|
+
pvs: new Map([
|
|
643
|
+
[groupA, [pv]],
|
|
644
|
+
[groupB, []],
|
|
645
|
+
]),
|
|
646
|
+
roles,
|
|
647
|
+
sourcePackages: new Map([[groupA, new Map([[packageId, sourcePkg]])]]),
|
|
648
|
+
});
|
|
649
|
+
const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
|
|
650
|
+
expect(results).toHaveLength(0);
|
|
651
|
+
expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
|
|
652
|
+
expect(mockPvSignAndSave).not.toHaveBeenCalled();
|
|
653
|
+
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining("Refusing revoked publisher key"));
|
|
654
|
+
});
|
|
655
|
+
it("re-pins peers-core from a revoked key and accepts a version signed by the current key", async () => {
|
|
656
|
+
const { groupA, groupB, userContext, roles } = twoGroupSetup();
|
|
657
|
+
const packageId = systemIds.peersCorePackageId;
|
|
658
|
+
const sourcePkg = makePkg({ packageId, publishPublicKey: currentCoreKeys.publicKey });
|
|
659
|
+
const targetPkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
|
|
660
|
+
const pv = makeSignedPVWithSecretKey(packageId, currentCoreKeys.secretKey, {
|
|
661
|
+
version: "2.0.0",
|
|
662
|
+
});
|
|
663
|
+
setupMocksForScenario({
|
|
664
|
+
packages: new Map([[groupB, [targetPkg]]]),
|
|
665
|
+
pvs: new Map([
|
|
666
|
+
[groupA, [pv]],
|
|
667
|
+
[groupB, []],
|
|
668
|
+
]),
|
|
669
|
+
roles,
|
|
670
|
+
sourcePackages: new Map([[groupA, new Map([[packageId, sourcePkg]])]]),
|
|
671
|
+
});
|
|
672
|
+
const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
|
|
673
|
+
expect(results).toHaveLength(1);
|
|
674
|
+
expect(results[0]).toMatchObject({ groupId: groupB, packageId, version: "2.0.0" });
|
|
675
|
+
expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.objectContaining({ packageId, publishPublicKey: currentCoreKeys.publicKey }));
|
|
676
|
+
});
|
|
677
|
+
it("rejects a peers-core version signed by the revoked key even where that key is pinned", async () => {
|
|
678
|
+
const { groupA, groupB, userContext, roles } = twoGroupSetup();
|
|
679
|
+
const packageId = systemIds.peersCorePackageId;
|
|
680
|
+
const targetPkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
|
|
681
|
+
const pv = makeSignedPVWithSecretKey(packageId, revokedCoreKeys.secretKey, {
|
|
682
|
+
version: "2.0.0",
|
|
683
|
+
});
|
|
684
|
+
setupMocksForScenario({
|
|
685
|
+
packages: new Map([[groupB, [targetPkg]]]),
|
|
686
|
+
pvs: new Map([
|
|
687
|
+
[groupA, [pv]],
|
|
688
|
+
[groupB, []],
|
|
689
|
+
]),
|
|
690
|
+
roles,
|
|
691
|
+
});
|
|
692
|
+
const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
|
|
693
|
+
expect(results).toHaveLength(0);
|
|
694
|
+
expect(mockPvSignAndSave).not.toHaveBeenCalled();
|
|
695
|
+
});
|
|
696
|
+
it("does not re-pin other packages pinned to a revoked key; their versions are rejected", async () => {
|
|
697
|
+
const { groupA, groupB, userContext, roles } = twoGroupSetup();
|
|
698
|
+
const packageId = (0, utils_1.newid)();
|
|
699
|
+
const targetPkg = makePkg({ packageId, publishPublicKey: revokedCoreKeys.publicKey });
|
|
700
|
+
const pv = makeSignedPVWithSecretKey(packageId, revokedCoreKeys.secretKey, {
|
|
701
|
+
version: "2.0.0",
|
|
702
|
+
});
|
|
703
|
+
setupMocksForScenario({
|
|
704
|
+
packages: new Map([[groupB, [targetPkg]]]),
|
|
705
|
+
pvs: new Map([
|
|
706
|
+
[groupA, [pv]],
|
|
707
|
+
[groupB, []],
|
|
708
|
+
]),
|
|
709
|
+
roles,
|
|
710
|
+
});
|
|
711
|
+
const results = await (0, package_propagation_1.discoverAndPropagateVersions)(userContext);
|
|
712
|
+
expect(results).toHaveLength(0);
|
|
713
|
+
expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
|
|
714
|
+
});
|
|
715
|
+
});
|
|
592
716
|
it("skips PV when packageVersionId already exists in target (dedup)", async () => {
|
|
593
717
|
const groupA = "group-a";
|
|
594
718
|
const groupB = "group-b";
|
|
@@ -19,6 +19,7 @@ const package_author_signing_1 = require("./package-author-signing");
|
|
|
19
19
|
const package_installer_1 = require("./package-installer");
|
|
20
20
|
const package_propagation_1 = require("./package-propagation");
|
|
21
21
|
const package_tarball_1 = require("./package-tarball");
|
|
22
|
+
const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
|
|
22
23
|
// Module-level memoization: unique by `updateUrl + ":" + tag`
|
|
23
24
|
const remoteCheckCache = new Map();
|
|
24
25
|
/**
|
|
@@ -44,9 +45,13 @@ function checkPackageRemoteForNewVersion(packageId, updateUrl, tag, http) {
|
|
|
44
45
|
* group does not already have the remote PackageVersion.
|
|
45
46
|
*/
|
|
46
47
|
async function checkAndInstallPackageRemoteVersion(dataContext, pkg, tag, http) {
|
|
47
|
-
|
|
48
|
+
const updateUrl = pkg.updateUrl;
|
|
49
|
+
if (!updateUrl || pkg.disabled)
|
|
48
50
|
return null;
|
|
49
|
-
|
|
51
|
+
// A peers-core row pinned to a revoked publisher key would reject every new release; move
|
|
52
|
+
// it to the host's current anchor before looking at the remote.
|
|
53
|
+
await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(dataContext, pkg);
|
|
54
|
+
const result = await checkPackageRemoteForNewVersion(pkg.packageId, updateUrl, tag, http);
|
|
50
55
|
if (!result)
|
|
51
56
|
return null;
|
|
52
57
|
const existing = await (0, package_versions_1.PackageVersions)(dataContext).get(result.pointer.packageVersionId);
|
|
@@ -32,13 +32,30 @@ const mockResolveDevicePackageVersion = jest.fn();
|
|
|
32
32
|
jest.mock("../data/package-version-resolver", () => ({
|
|
33
33
|
resolveDevicePackageVersion: (...args) => mockResolveDevicePackageVersion(...args),
|
|
34
34
|
}));
|
|
35
|
+
// Pin the host to a fresh "current" peers-core key and revoke a fresh "old" one so the
|
|
36
|
+
// revocation tests do not depend on the real constants.
|
|
37
|
+
jest.mock("../system-ids", () => {
|
|
38
|
+
const actual = jest.requireActual("../system-ids");
|
|
39
|
+
const { newKeys: makeKeys } = jest.requireActual("../keys");
|
|
40
|
+
const revoked = makeKeys();
|
|
41
|
+
const current = makeKeys();
|
|
42
|
+
return {
|
|
43
|
+
...actual,
|
|
44
|
+
peersCorePublishPublicKey: current.publicKey,
|
|
45
|
+
peersCoreRevokedPublishPublicKeys: [revoked.publicKey],
|
|
46
|
+
__testKeys: { revoked, current },
|
|
47
|
+
};
|
|
48
|
+
});
|
|
35
49
|
const file_types_1 = require("../data/files/file.types");
|
|
36
50
|
const keys_1 = require("../keys");
|
|
51
|
+
const systemIds = require("../system-ids");
|
|
37
52
|
const package_author_signing_1 = require("./package-author-signing");
|
|
38
53
|
const package_remote_checker_1 = require("./package-remote-checker");
|
|
39
54
|
const package_tarball_1 = require("./package-tarball");
|
|
55
|
+
// PKG_ID is peers-core's id so the trust-anchor re-pin path is exercised.
|
|
40
56
|
const PKG_ID = "00mh0wlipjixk2gqmurbwee0o";
|
|
41
57
|
const PV_ID = "00mpdg7xstwt3oe51x9brk15i";
|
|
58
|
+
const { revoked: revokedCoreKeys, current: currentCoreKeys } = systemIds.__testKeys;
|
|
42
59
|
function makeContext(dataContextId) {
|
|
43
60
|
return { dataContextId };
|
|
44
61
|
}
|
|
@@ -234,6 +251,54 @@ describe("package-remote-checker", () => {
|
|
|
234
251
|
expect(http.fetchJson).toHaveBeenCalledTimes(1);
|
|
235
252
|
expect(mockSaveBundleFile).toHaveBeenCalled();
|
|
236
253
|
});
|
|
254
|
+
it("re-pins peers-core from a revoked key and installs a release signed by the current key", async () => {
|
|
255
|
+
expect(PKG_ID).toBe(systemIds.peersCorePackageId);
|
|
256
|
+
const ctx = makeContext("user-ctx");
|
|
257
|
+
const { pointer, tarball } = await makeSignedRemoteResult(currentCoreKeys.secretKey, currentCoreKeys.publicKey);
|
|
258
|
+
let pkg = {
|
|
259
|
+
packageId: PKG_ID,
|
|
260
|
+
name: "peers-core",
|
|
261
|
+
publishPublicKey: revokedCoreKeys.publicKey,
|
|
262
|
+
updateUrl: "https://example.com/peers-core",
|
|
263
|
+
};
|
|
264
|
+
const http = {
|
|
265
|
+
fetchJson: jest.fn().mockResolvedValue(pointer),
|
|
266
|
+
fetchBinary: jest.fn().mockResolvedValue(tarball),
|
|
267
|
+
};
|
|
268
|
+
mockPackageVersionsGet.mockResolvedValue(undefined);
|
|
269
|
+
mockPackagesGet.mockImplementation(async () => pkg);
|
|
270
|
+
mockPackagesSignAndSave.mockImplementation(async (_ctx, saved) => {
|
|
271
|
+
pkg = saved;
|
|
272
|
+
});
|
|
273
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
274
|
+
const installed = await (0, package_remote_checker_1.checkAndInstallPackageRemoteVersion)(ctx, pkg, "stable", http);
|
|
275
|
+
expect(installed?.packageVersion.packageVersionId).toBe(PV_ID);
|
|
276
|
+
expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ packageId: PKG_ID, publishPublicKey: currentCoreKeys.publicKey }));
|
|
277
|
+
expect(pkg.publishPublicKey).toBe(currentCoreKeys.publicKey);
|
|
278
|
+
warn.mockRestore();
|
|
279
|
+
});
|
|
280
|
+
it("rejects a remote release signed by a revoked key", async () => {
|
|
281
|
+
const ctx = makeContext("user-ctx");
|
|
282
|
+
const { pointer, tarball } = await makeSignedRemoteResult(revokedCoreKeys.secretKey, revokedCoreKeys.publicKey);
|
|
283
|
+
const pkg = {
|
|
284
|
+
packageId: PKG_ID,
|
|
285
|
+
name: "peers-core",
|
|
286
|
+
publishPublicKey: currentCoreKeys.publicKey,
|
|
287
|
+
updateUrl: "https://example.com/peers-core-old",
|
|
288
|
+
};
|
|
289
|
+
const http = {
|
|
290
|
+
fetchJson: jest.fn().mockResolvedValue(pointer),
|
|
291
|
+
fetchBinary: jest.fn().mockResolvedValue(tarball),
|
|
292
|
+
};
|
|
293
|
+
mockPackageVersionsGet.mockResolvedValue(undefined);
|
|
294
|
+
mockPackagesGet.mockResolvedValue(pkg);
|
|
295
|
+
const error = jest.spyOn(console, "error").mockImplementation(() => { });
|
|
296
|
+
const installed = await (0, package_remote_checker_1.checkAndInstallPackageRemoteVersion)(ctx, pkg, "stable", http);
|
|
297
|
+
expect(installed).toBeNull();
|
|
298
|
+
expect(mockSaveBundleFile).not.toHaveBeenCalled();
|
|
299
|
+
expect(mockPackageVersionsSignAndSave).not.toHaveBeenCalled();
|
|
300
|
+
error.mockRestore();
|
|
301
|
+
});
|
|
237
302
|
});
|
|
238
303
|
describe("installRemotePackageVersion", () => {
|
|
239
304
|
it("rejects publisher key mismatch before saving files", async () => {
|
|
@@ -43,6 +43,10 @@ export interface IEnsurePeersCoreOpts {
|
|
|
43
43
|
* Ensure peers-core is installed (with a usable, loaded version) in
|
|
44
44
|
* `dataContext`, applying the layered strategy described in the module docs.
|
|
45
45
|
*
|
|
46
|
+
* Before anything else, an already-installed peers-core whose pinned publisher key has been
|
|
47
|
+
* revoked is re-pinned to the host's current key (see `peers-core-trust-anchor.ts`), so the
|
|
48
|
+
* later remote check can accept releases signed by the replacement key.
|
|
49
|
+
*
|
|
46
50
|
* @returns true if an install occurred, false if it was already present or no
|
|
47
51
|
* strategy succeeded.
|
|
48
52
|
*/
|
|
@@ -23,16 +23,22 @@ const packages_utils_1 = require("../data/packages.utils");
|
|
|
23
23
|
const system_ids_1 = require("../system-ids");
|
|
24
24
|
const package_remote_checker_1 = require("./package-remote-checker");
|
|
25
25
|
const package_seed_installer_1 = require("./package-seed-installer");
|
|
26
|
+
const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
|
|
26
27
|
const PEERS_CORE_NAME = "peers-core";
|
|
27
28
|
const PEERS_CORE_DESCRIPTION = "Core system package for Peers platform";
|
|
28
29
|
/**
|
|
29
30
|
* Ensure peers-core is installed (with a usable, loaded version) in
|
|
30
31
|
* `dataContext`, applying the layered strategy described in the module docs.
|
|
31
32
|
*
|
|
33
|
+
* Before anything else, an already-installed peers-core whose pinned publisher key has been
|
|
34
|
+
* revoked is re-pinned to the host's current key (see `peers-core-trust-anchor.ts`), so the
|
|
35
|
+
* later remote check can accept releases signed by the replacement key.
|
|
36
|
+
*
|
|
32
37
|
* @returns true if an install occurred, false if it was already present or no
|
|
33
38
|
* strategy succeeded.
|
|
34
39
|
*/
|
|
35
40
|
async function ensurePeersCore(userContext, dataContext, opts) {
|
|
41
|
+
await (0, peers_core_trust_anchor_1.repinInstalledPeersCoreIfRevoked)(dataContext);
|
|
36
42
|
const existingPvs = await (0, package_versions_1.PackageVersions)(dataContext).list({ packageId: system_ids_1.peersCorePackageId });
|
|
37
43
|
if (existingPvs.length > 0)
|
|
38
44
|
return false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const mockPvList = jest.fn();
|
|
4
|
+
jest.mock("../data/package-versions", () => ({
|
|
5
|
+
PackageVersions: (ctx) => ({
|
|
6
|
+
list: (...args) => mockPvList(ctx, ...args),
|
|
7
|
+
}),
|
|
8
|
+
}));
|
|
9
|
+
const mockRepinInstalled = jest.fn();
|
|
10
|
+
jest.mock("./peers-core-trust-anchor", () => ({
|
|
11
|
+
repinInstalledPeersCoreIfRevoked: (...args) => mockRepinInstalled(...args),
|
|
12
|
+
}));
|
|
13
|
+
jest.mock("../data/packages", () => ({ Packages: () => ({}) }));
|
|
14
|
+
jest.mock("../data/packages.utils", () => ({ copyPackageToAnotherDataContext: jest.fn() }));
|
|
15
|
+
jest.mock("../data/package-version-resolver", () => ({
|
|
16
|
+
resolveDevicePackageVersion: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
jest.mock("./package-remote-checker", () => ({
|
|
19
|
+
checkAndInstallPackageRemoteVersion: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
jest.mock("./package-seed-installer", () => ({
|
|
22
|
+
ensurePackageRecord: jest.fn(),
|
|
23
|
+
seedPackageInContext: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
const system_ids_1 = require("../system-ids");
|
|
26
|
+
const peers_core_installer_1 = require("./peers-core-installer");
|
|
27
|
+
describe("ensurePeersCore", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
mockRepinInstalled.mockResolvedValue(null);
|
|
31
|
+
});
|
|
32
|
+
it("re-pins a revoked peers-core key before checking for existing versions", async () => {
|
|
33
|
+
const dataContext = { dataContextId: "user-ctx" };
|
|
34
|
+
const order = [];
|
|
35
|
+
mockRepinInstalled.mockImplementation(async () => {
|
|
36
|
+
order.push("repin");
|
|
37
|
+
return null;
|
|
38
|
+
});
|
|
39
|
+
mockPvList.mockImplementation(async () => {
|
|
40
|
+
order.push("list");
|
|
41
|
+
return [{ packageVersionId: "existing" }];
|
|
42
|
+
});
|
|
43
|
+
const installed = await (0, peers_core_installer_1.ensurePeersCore)({}, dataContext, {});
|
|
44
|
+
expect(installed).toBe(false);
|
|
45
|
+
expect(mockRepinInstalled).toHaveBeenCalledWith(dataContext);
|
|
46
|
+
expect(mockPvList).toHaveBeenCalledWith(dataContext, { packageId: system_ids_1.peersCorePackageId });
|
|
47
|
+
expect(order).toEqual(["repin", "list"]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DataContext } from "../context/data-context";
|
|
2
|
+
import { type IPackage } from "../data/packages";
|
|
3
|
+
/**
|
|
4
|
+
* Migration of the peers-core trust anchor after a publisher-key rotation.
|
|
5
|
+
*
|
|
6
|
+
* `Packages.publishPublicKey` is pinned on first install (TOFU) and otherwise never changes, so
|
|
7
|
+
* a client that installed peers-core under a key that was later revoked would keep that key
|
|
8
|
+
* forever and reject every version signed by its replacement. This module replaces a pinned
|
|
9
|
+
* key that appears in {@link peersCoreRevokedPublishPublicKeys} with the host's current
|
|
10
|
+
* {@link peersCorePublishPublicKey}. Only peers-core is handled here: it is the one package
|
|
11
|
+
* whose trust anchor ships inside the host.
|
|
12
|
+
*/
|
|
13
|
+
/** Options for {@link repinPeersCoreIfRevoked}, mainly for tests. */
|
|
14
|
+
export interface IRepinOptions {
|
|
15
|
+
/** Current trusted key. Defaults to {@link peersCorePublishPublicKey}. */
|
|
16
|
+
currentPublicKey?: string;
|
|
17
|
+
/** Keys that must be replaced. Defaults to {@link peersCoreRevokedPublishPublicKeys}. */
|
|
18
|
+
revokedPublicKeys?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether `pkg` is the peers-core package pinned to a revoked publisher key.
|
|
22
|
+
*
|
|
23
|
+
* @param pkg - package row to inspect
|
|
24
|
+
* @param opts - alternative key lists (tests)
|
|
25
|
+
*/
|
|
26
|
+
export declare function needsPeersCoreRepin(pkg: Pick<IPackage, "packageId" | "publishPublicKey">, opts?: IRepinOptions): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* If `pkg` is peers-core pinned to a revoked key, re-pin it to the current key in
|
|
29
|
+
* `dataContext` and return the updated row; otherwise return `pkg` unchanged.
|
|
30
|
+
*
|
|
31
|
+
* Safe to call on every remote check and propagation: it is a no-op unless the pin is revoked.
|
|
32
|
+
* In a group where the caller may not write package rows, `signAndSave` throws; the error is
|
|
33
|
+
* logged and the original row returned, so verification keeps failing closed there until an
|
|
34
|
+
* admin's device performs the re-pin and the row syncs.
|
|
35
|
+
*
|
|
36
|
+
* @param dataContext - context holding the package row
|
|
37
|
+
* @param pkg - the package row as currently loaded
|
|
38
|
+
* @param opts - alternative key lists (tests)
|
|
39
|
+
* @returns the (possibly re-pinned) package row
|
|
40
|
+
*/
|
|
41
|
+
export declare function repinPeersCoreIfRevoked(dataContext: DataContext, pkg: IPackage, opts?: IRepinOptions): Promise<IPackage>;
|
|
42
|
+
/**
|
|
43
|
+
* Look up peers-core in `dataContext` and re-pin it if its key is revoked.
|
|
44
|
+
*
|
|
45
|
+
* @returns the package row when peers-core is installed in the context, else null
|
|
46
|
+
*/
|
|
47
|
+
export declare function repinInstalledPeersCoreIfRevoked(dataContext: DataContext, opts?: IRepinOptions): Promise<IPackage | null>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.needsPeersCoreRepin = needsPeersCoreRepin;
|
|
4
|
+
exports.repinPeersCoreIfRevoked = repinPeersCoreIfRevoked;
|
|
5
|
+
exports.repinInstalledPeersCoreIfRevoked = repinInstalledPeersCoreIfRevoked;
|
|
6
|
+
const packages_1 = require("../data/packages");
|
|
7
|
+
const system_ids_1 = require("../system-ids");
|
|
8
|
+
const package_author_signing_1 = require("./package-author-signing");
|
|
9
|
+
/**
|
|
10
|
+
* Whether `pkg` is the peers-core package pinned to a revoked publisher key.
|
|
11
|
+
*
|
|
12
|
+
* @param pkg - package row to inspect
|
|
13
|
+
* @param opts - alternative key lists (tests)
|
|
14
|
+
*/
|
|
15
|
+
function needsPeersCoreRepin(pkg, opts = {}) {
|
|
16
|
+
const current = opts.currentPublicKey ?? system_ids_1.peersCorePublishPublicKey;
|
|
17
|
+
const revoked = opts.revokedPublicKeys ?? system_ids_1.peersCoreRevokedPublishPublicKeys;
|
|
18
|
+
return (pkg.packageId === system_ids_1.peersCorePackageId &&
|
|
19
|
+
!!current &&
|
|
20
|
+
pkg.publishPublicKey !== current &&
|
|
21
|
+
(0, package_author_signing_1.isRevokedPublisherKey)(pkg.publishPublicKey, revoked));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* If `pkg` is peers-core pinned to a revoked key, re-pin it to the current key in
|
|
25
|
+
* `dataContext` and return the updated row; otherwise return `pkg` unchanged.
|
|
26
|
+
*
|
|
27
|
+
* Safe to call on every remote check and propagation: it is a no-op unless the pin is revoked.
|
|
28
|
+
* In a group where the caller may not write package rows, `signAndSave` throws; the error is
|
|
29
|
+
* logged and the original row returned, so verification keeps failing closed there until an
|
|
30
|
+
* admin's device performs the re-pin and the row syncs.
|
|
31
|
+
*
|
|
32
|
+
* @param dataContext - context holding the package row
|
|
33
|
+
* @param pkg - the package row as currently loaded
|
|
34
|
+
* @param opts - alternative key lists (tests)
|
|
35
|
+
* @returns the (possibly re-pinned) package row
|
|
36
|
+
*/
|
|
37
|
+
async function repinPeersCoreIfRevoked(dataContext, pkg, opts = {}) {
|
|
38
|
+
if (!needsPeersCoreRepin(pkg, opts))
|
|
39
|
+
return pkg;
|
|
40
|
+
const current = opts.currentPublicKey ?? system_ids_1.peersCorePublishPublicKey;
|
|
41
|
+
try {
|
|
42
|
+
const updated = { ...pkg, publishPublicKey: current };
|
|
43
|
+
await (0, packages_1.Packages)(dataContext).signAndSave(updated);
|
|
44
|
+
console.warn(`[peers-core-trust-anchor] Replaced revoked peers-core publisher key in ${dataContext.dataContextId}`);
|
|
45
|
+
return updated;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error(`[peers-core-trust-anchor] Could not re-pin peers-core in ${dataContext.dataContextId}:`, err);
|
|
49
|
+
return pkg;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Look up peers-core in `dataContext` and re-pin it if its key is revoked.
|
|
54
|
+
*
|
|
55
|
+
* @returns the package row when peers-core is installed in the context, else null
|
|
56
|
+
*/
|
|
57
|
+
async function repinInstalledPeersCoreIfRevoked(dataContext, opts = {}) {
|
|
58
|
+
const pkg = await (0, packages_1.Packages)(dataContext).get(system_ids_1.peersCorePackageId);
|
|
59
|
+
if (!pkg)
|
|
60
|
+
return null;
|
|
61
|
+
return repinPeersCoreIfRevoked(dataContext, pkg, opts);
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const keys_1 = require("../keys");
|
|
4
|
+
const systemIds = require("../system-ids");
|
|
5
|
+
const utils_1 = require("../utils");
|
|
6
|
+
const package_author_signing_1 = require("./package-author-signing");
|
|
7
|
+
const peers_core_trust_anchor_1 = require("./peers-core-trust-anchor");
|
|
8
|
+
// Pin the host to a fresh "current" key and revoke a fresh "old" key so the tests do not
|
|
9
|
+
// depend on the real constants. Keys are created inside the factory because jest hoists
|
|
10
|
+
// `jest.mock` above module-level declarations.
|
|
11
|
+
jest.mock("../system-ids", () => {
|
|
12
|
+
const actual = jest.requireActual("../system-ids");
|
|
13
|
+
const { newKeys: makeKeys } = jest.requireActual("../keys");
|
|
14
|
+
const revoked = makeKeys();
|
|
15
|
+
const current = makeKeys();
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
peersCorePublishPublicKey: current.publicKey,
|
|
19
|
+
peersCoreRevokedPublishPublicKeys: [revoked.publicKey],
|
|
20
|
+
__testKeys: { revoked, current },
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
const { revoked: revokedKeys, current: currentKeys } = systemIds.__testKeys;
|
|
24
|
+
const mockPackagesGet = jest.fn();
|
|
25
|
+
const mockPackagesSignAndSave = jest.fn();
|
|
26
|
+
jest.mock("../data/packages", () => ({
|
|
27
|
+
Packages: () => ({
|
|
28
|
+
get: (...args) => mockPackagesGet(...args),
|
|
29
|
+
signAndSave: (...args) => mockPackagesSignAndSave(...args),
|
|
30
|
+
}),
|
|
31
|
+
}));
|
|
32
|
+
function makeCtx(id = "ctx") {
|
|
33
|
+
return { dataContextId: id };
|
|
34
|
+
}
|
|
35
|
+
function makePkg(overrides) {
|
|
36
|
+
return {
|
|
37
|
+
packageId: systemIds.peersCorePackageId,
|
|
38
|
+
name: "peers-core",
|
|
39
|
+
publishPublicKey: revokedKeys.publicKey,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function makePV() {
|
|
44
|
+
return {
|
|
45
|
+
packageId: systemIds.peersCorePackageId,
|
|
46
|
+
packageVersionId: (0, utils_1.newid)(),
|
|
47
|
+
version: "1.0.0",
|
|
48
|
+
versionTag: "stable",
|
|
49
|
+
packageBundleFileHash: "hash",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
jest.clearAllMocks();
|
|
54
|
+
jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
55
|
+
jest.spyOn(console, "error").mockImplementation(() => { });
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
jest.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
describe("isRevokedPublisherKey", () => {
|
|
61
|
+
it("uses the host revocation list by default", () => {
|
|
62
|
+
expect((0, package_author_signing_1.isRevokedPublisherKey)(revokedKeys.publicKey)).toBe(true);
|
|
63
|
+
expect((0, package_author_signing_1.isRevokedPublisherKey)(currentKeys.publicKey)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it("accepts an explicit list", () => {
|
|
66
|
+
expect((0, package_author_signing_1.isRevokedPublisherKey)("abc", ["abc"])).toBe(true);
|
|
67
|
+
expect((0, package_author_signing_1.isRevokedPublisherKey)(revokedKeys.publicKey, [])).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it("treats empty or missing keys as not revoked", () => {
|
|
70
|
+
expect((0, package_author_signing_1.isRevokedPublisherKey)("", ["", "x"])).toBe(false);
|
|
71
|
+
expect((0, package_author_signing_1.isRevokedPublisherKey)(undefined)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe("verifyPackageAuthorSignature with a revoked key", () => {
|
|
75
|
+
it("rejects a mathematically valid signature from a revoked key", () => {
|
|
76
|
+
const pv = { ...makePV(), packageAuthorSignature: "" };
|
|
77
|
+
pv.packageAuthorSignature = (0, package_author_signing_1.signPackageAuthor)(pv, revokedKeys.secretKey);
|
|
78
|
+
const result = (0, package_author_signing_1.verifyPackageAuthorSignature)(pv, revokedKeys.publicKey);
|
|
79
|
+
expect(result).toEqual({ valid: false, publicKey: revokedKeys.publicKey });
|
|
80
|
+
});
|
|
81
|
+
it("still accepts the current key", () => {
|
|
82
|
+
const pv = { ...makePV(), packageAuthorSignature: "" };
|
|
83
|
+
pv.packageAuthorSignature = (0, package_author_signing_1.signPackageAuthor)(pv, currentKeys.secretKey);
|
|
84
|
+
expect((0, package_author_signing_1.verifyPackageAuthorSignature)(pv, currentKeys.publicKey).valid).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("needsPeersCoreRepin", () => {
|
|
88
|
+
it("is true only for peers-core pinned to a revoked key", () => {
|
|
89
|
+
expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg())).toBe(true);
|
|
90
|
+
expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg({ publishPublicKey: currentKeys.publicKey }))).toBe(false);
|
|
91
|
+
expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg({ publishPublicKey: "" }))).toBe(false);
|
|
92
|
+
expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg({ packageId: (0, utils_1.newid)() }))).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it("honours explicit key lists", () => {
|
|
95
|
+
const other = (0, keys_1.newKeys)();
|
|
96
|
+
const pkg = makePkg({ publishPublicKey: other.publicKey });
|
|
97
|
+
expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(pkg)).toBe(false);
|
|
98
|
+
expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(pkg, {
|
|
99
|
+
currentPublicKey: currentKeys.publicKey,
|
|
100
|
+
revokedPublicKeys: [other.publicKey],
|
|
101
|
+
})).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it("is false when the host has no current key to pin", () => {
|
|
104
|
+
expect((0, peers_core_trust_anchor_1.needsPeersCoreRepin)(makePkg(), { currentPublicKey: "" })).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("repinPeersCoreIfRevoked", () => {
|
|
108
|
+
it("re-pins and saves when the key is revoked", async () => {
|
|
109
|
+
mockPackagesSignAndSave.mockResolvedValue(undefined);
|
|
110
|
+
const pkg = makePkg();
|
|
111
|
+
const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx(), pkg);
|
|
112
|
+
expect(result).not.toBe(pkg);
|
|
113
|
+
expect(result.publishPublicKey).toBe(currentKeys.publicKey);
|
|
114
|
+
expect(mockPackagesSignAndSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
115
|
+
packageId: systemIds.peersCorePackageId,
|
|
116
|
+
publishPublicKey: currentKeys.publicKey,
|
|
117
|
+
}));
|
|
118
|
+
expect(console.warn).toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
it("returns the same row and does not save when nothing is revoked", async () => {
|
|
121
|
+
const pkg = makePkg({ publishPublicKey: currentKeys.publicKey });
|
|
122
|
+
const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx(), pkg);
|
|
123
|
+
expect(result).toBe(pkg);
|
|
124
|
+
expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
it("does not touch other packages even when their key is revoked", async () => {
|
|
127
|
+
const pkg = makePkg({ packageId: (0, utils_1.newid)() });
|
|
128
|
+
const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx(), pkg);
|
|
129
|
+
expect(result).toBe(pkg);
|
|
130
|
+
expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
it("returns the original row when the save is not permitted", async () => {
|
|
133
|
+
mockPackagesSignAndSave.mockRejectedValue(new Error("not a writer"));
|
|
134
|
+
const pkg = makePkg();
|
|
135
|
+
const result = await (0, peers_core_trust_anchor_1.repinPeersCoreIfRevoked)(makeCtx("group-ro"), pkg);
|
|
136
|
+
expect(result).toBe(pkg);
|
|
137
|
+
expect(result.publishPublicKey).toBe(revokedKeys.publicKey);
|
|
138
|
+
expect(console.error).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
describe("repinInstalledPeersCoreIfRevoked", () => {
|
|
142
|
+
it("returns null when peers-core is not installed", async () => {
|
|
143
|
+
mockPackagesGet.mockResolvedValue(undefined);
|
|
144
|
+
expect(await (0, peers_core_trust_anchor_1.repinInstalledPeersCoreIfRevoked)(makeCtx())).toBeNull();
|
|
145
|
+
expect(mockPackagesSignAndSave).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
it("loads peers-core and re-pins it", async () => {
|
|
148
|
+
mockPackagesGet.mockResolvedValue(makePkg());
|
|
149
|
+
mockPackagesSignAndSave.mockResolvedValue(undefined);
|
|
150
|
+
const result = await (0, peers_core_trust_anchor_1.repinInstalledPeersCoreIfRevoked)(makeCtx());
|
|
151
|
+
expect(mockPackagesGet).toHaveBeenCalledWith(systemIds.peersCorePackageId);
|
|
152
|
+
expect(result?.publishPublicKey).toBe(currentKeys.publicKey);
|
|
153
|
+
expect(mockPackagesSignAndSave).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
});
|
package/dist/system-ids.d.ts
CHANGED
|
@@ -23,8 +23,28 @@ export declare const createGroupToolId = "000peers0tool0creategroup";
|
|
|
23
23
|
export declare const peersCorePackageId = "00mh0wlipjixk2gqmurbwee0o";
|
|
24
24
|
/** Canonical source repository for peers-core. */
|
|
25
25
|
export declare const peersCorePackageRepoUrl = "https://github.com/peers-app/peers-core";
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Trusted publishing key for official peers-core releases (Ed25519, base64url).
|
|
28
|
+
*
|
|
29
|
+
* This is the bootstrap trust anchor shipped with every host. It seeds `Packages.publishPublicKey`
|
|
30
|
+
* for peers-core and is what `verifyPackageAuthorSignature` checks remote and propagated
|
|
31
|
+
* peers-core versions against. The same key is registered as the active key of subject
|
|
32
|
+
* {@link peersCorePackageId} in the peers-services key registry (`GET /api/v1/keys/:subjectId`).
|
|
33
|
+
*
|
|
34
|
+
* Rotation: generate a new pair with `scripts/generate-publisher-key.mjs`, move the previous
|
|
35
|
+
* value into {@link peersCoreRevokedPublishPublicKeys}, seed the registry, and ship a host release.
|
|
36
|
+
*/
|
|
37
|
+
export declare const peersCorePublishPublicKey = "8S1pAWvka0RZauJnwM13n7qO01NtdGV7FSU_aSJw0dc";
|
|
38
|
+
/**
|
|
39
|
+
* peers-core publishing keys that must never be trusted again, whatever a local `Packages` row
|
|
40
|
+
* or a propagating peer says. A version signed by one of these fails author verification, a
|
|
41
|
+
* peer offering one as a TOFU anchor is refused, and `ensurePeersCore` replaces a pinned
|
|
42
|
+
* revoked key with {@link peersCorePublishPublicKey}.
|
|
43
|
+
*
|
|
44
|
+
* Append-only. Entries are retained forever so a client coming back after a long time still
|
|
45
|
+
* rejects them.
|
|
46
|
+
*/
|
|
47
|
+
export declare const peersCoreRevokedPublishPublicKeys: readonly string[];
|
|
28
48
|
/**
|
|
29
49
|
* Stable id of the peers **system** itself, used as the `providerPackageId` for
|
|
30
50
|
* built-in system contracts (contracts the platform provides rather than a package).
|
|
@@ -109,6 +129,7 @@ export declare const localFoldersToolIds: {
|
|
|
109
129
|
readonly readRange: "00mukk9dwdih6mh8rrofph80o";
|
|
110
130
|
readonly index: "00mukk9dwdm01bxg3hrby6pli";
|
|
111
131
|
readonly getFileBlocks: "00mumw1pmmf3y2ujfdc99h4ib";
|
|
132
|
+
readonly assemble: "00mungara9ixeolgxukzj2aku";
|
|
112
133
|
readonly getJob: "00mukk9dwd1c1f51xjysssgtv";
|
|
113
134
|
readonly cancelJob: "00mukk9dweizvif4ghp6kfa92";
|
|
114
135
|
readonly move: "00mukk9dwer8wsw5eiwis8tzm";
|
package/dist/system-ids.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
|
|
3
|
+
exports.localFoldersToolIds = exports.localFoldersContractId = exports.identityToolIds = exports.identityContractId = exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCoreRevokedPublishPublicKeys = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
|
|
4
4
|
/** Stable user ID that owns built-in Peers system records. */
|
|
5
5
|
exports.peersRootUserId = "000peers0user000000000001";
|
|
6
6
|
/** Default assistant used when no user-selected assistant is available. */
|
|
@@ -27,8 +27,31 @@ exports.createGroupToolId = "000peers0tool0creategroup";
|
|
|
27
27
|
exports.peersCorePackageId = "00mh0wlipjixk2gqmurbwee0o";
|
|
28
28
|
/** Canonical source repository for peers-core. */
|
|
29
29
|
exports.peersCorePackageRepoUrl = "https://github.com/peers-app/peers-core";
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Trusted publishing key for official peers-core releases (Ed25519, base64url).
|
|
32
|
+
*
|
|
33
|
+
* This is the bootstrap trust anchor shipped with every host. It seeds `Packages.publishPublicKey`
|
|
34
|
+
* for peers-core and is what `verifyPackageAuthorSignature` checks remote and propagated
|
|
35
|
+
* peers-core versions against. The same key is registered as the active key of subject
|
|
36
|
+
* {@link peersCorePackageId} in the peers-services key registry (`GET /api/v1/keys/:subjectId`).
|
|
37
|
+
*
|
|
38
|
+
* Rotation: generate a new pair with `scripts/generate-publisher-key.mjs`, move the previous
|
|
39
|
+
* value into {@link peersCoreRevokedPublishPublicKeys}, seed the registry, and ship a host release.
|
|
40
|
+
*/
|
|
41
|
+
exports.peersCorePublishPublicKey = "8S1pAWvka0RZauJnwM13n7qO01NtdGV7FSU_aSJw0dc";
|
|
42
|
+
/**
|
|
43
|
+
* peers-core publishing keys that must never be trusted again, whatever a local `Packages` row
|
|
44
|
+
* or a propagating peer says. A version signed by one of these fails author verification, a
|
|
45
|
+
* peer offering one as a TOFU anchor is refused, and `ensurePeersCore` replaces a pinned
|
|
46
|
+
* revoked key with {@link peersCorePublishPublicKey}.
|
|
47
|
+
*
|
|
48
|
+
* Append-only. Entries are retained forever so a client coming back after a long time still
|
|
49
|
+
* rejects them.
|
|
50
|
+
*/
|
|
51
|
+
exports.peersCoreRevokedPublishPublicKeys = [
|
|
52
|
+
// Exposed in packaged macOS builds v0.19.9 through v0.25.11 (2026-10-02 incident).
|
|
53
|
+
"S7cQqnvVz-FVDO6Lk5zC7epSvcP_-NyBVZz9gdcj73c",
|
|
54
|
+
];
|
|
32
55
|
/**
|
|
33
56
|
* Stable id of the peers **system** itself, used as the `providerPackageId` for
|
|
34
57
|
* built-in system contracts (contracts the platform provides rather than a package).
|
|
@@ -113,6 +136,7 @@ exports.localFoldersToolIds = {
|
|
|
113
136
|
readRange: "00mukk9dwdih6mh8rrofph80o",
|
|
114
137
|
index: "00mukk9dwdm01bxg3hrby6pli",
|
|
115
138
|
getFileBlocks: "00mumw1pmmf3y2ujfdc99h4ib",
|
|
139
|
+
assemble: "00mungara9ixeolgxukzj2aku",
|
|
116
140
|
getJob: "00mukk9dwd1c1f51xjysssgtv",
|
|
117
141
|
cancelJob: "00mukk9dweizvif4ghp6kfa92",
|
|
118
142
|
move: "00mukk9dwer8wsw5eiwis8tzm",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-sdk",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.12",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-sdk.git"
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
"build": "npm run clean && tsc",
|
|
29
29
|
"dev": "tsc -w",
|
|
30
30
|
"test": "jest",
|
|
31
|
-
"release:
|
|
32
|
-
"
|
|
33
|
-
"release:
|
|
31
|
+
"release:guard": "node release-guard.js",
|
|
32
|
+
"prepublishOnly": "npm run release:guard",
|
|
33
|
+
"release:patch": "npm run release:guard && npm run build && npm version patch && git push && git push --tags",
|
|
34
|
+
"release:minor": "npm run release:guard && npm run build && npm version minor && git push && git push --tags",
|
|
35
|
+
"release:major": "npm run release:guard && npm run build && npm version major && git push && git push --tags",
|
|
34
36
|
"release": "npm run release:patch"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|