@peers-app/peers-sdk 0.20.6 → 0.21.1
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/data/devices.d.ts +4 -4
- package/dist/data/voice-messages.d.ts +2 -2
- package/dist/device/binary-peer-connection-v2.d.ts +19 -2
- package/dist/device/binary-peer-connection-v2.js +127 -20
- package/dist/device/binary-peer-connection-v2.test.js +60 -0
- package/dist/device/binary-peer-connection.d.ts +13 -2
- package/dist/device/binary-peer-connection.js +77 -14
- package/dist/device/binary-peer-connection.test.js +21 -4
- package/dist/device/connection.d.ts +1 -1
- package/dist/device/device.d.ts +1 -1
- package/dist/device-pairing/constants.d.ts +34 -0
- package/dist/device-pairing/constants.js +37 -0
- package/dist/device-pairing/crypto.d.ts +45 -0
- package/dist/device-pairing/crypto.js +135 -0
- package/dist/device-pairing/device-pairing.test.d.ts +1 -0
- package/dist/device-pairing/device-pairing.test.js +174 -0
- package/dist/device-pairing/index.d.ts +6 -0
- package/dist/device-pairing/index.js +22 -0
- package/dist/device-pairing/invitation.d.ts +13 -0
- package/dist/device-pairing/invitation.js +85 -0
- package/dist/device-pairing/signaling.d.ts +9 -0
- package/dist/device-pairing/signaling.js +29 -0
- package/dist/device-pairing/transcript.d.ts +7 -0
- package/dist/device-pairing/transcript.js +38 -0
- package/dist/device-pairing/types.d.ts +1023 -0
- package/dist/device-pairing/types.js +304 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/package-installer/index.d.ts +1 -0
- package/dist/package-installer/index.js +1 -0
- package/dist/package-installer/package-author-signer.d.ts +25 -0
- package/dist/package-installer/package-author-signer.js +57 -0
- package/dist/package-installer/package-installer.d.ts +27 -0
- package/dist/package-installer/package-installer.js +88 -18
- package/dist/package-installer/package-installer.test.js +49 -0
- package/dist/package-installer/types.d.ts +1 -1
- package/dist/package-loader/package-loader.d.ts +3 -3
- package/dist/package-loader/package-loader.js +14 -4
- package/dist/package-loader/package-loader.test.d.ts +1 -0
- package/dist/package-loader/package-loader.test.js +81 -0
- package/dist/rpc-types.d.ts +14 -0
- package/dist/rpc-types.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.devicePairingStateSchema = exports.devicePairingPhaseSchema = exports.signedPairingInstallationReceiptSchema = exports.pairingInstallationReceiptContentsSchema = exports.pairingCredentialEnvelopeSchema = exports.pairingCredentialContentsSchema = exports.signedPairingApprovalSchema = exports.pairingApprovalContentsSchema = exports.pairingDescriptionSchema = exports.pairingDevicePresentationSchema = exports.devicePairingTranscriptSchema = exports.devicePairingIceServersAckSchema = exports.devicePairingRoomReadyAckSchema = exports.devicePairingSignalingAckSchema = exports.devicePairingSignalingErrorCodeSchema = exports.devicePairingDeleteRoomRequestSchema = exports.devicePairingJoinRoomRequestSchema = exports.devicePairingCreateRoomRequestSchema = exports.pairingSignalSchema = exports.unsignedPairingSignalSchema = exports.pairingIceServerSchema = exports.pairingIceCandidateSchema = exports.pairingSessionDescriptionSchema = exports.devicePairingInvitationSchema = exports.pairingRoleSchema = exports.pairingEndpointNonceSchema = exports.pairingHashSchema = exports.pairingRoomSecretSchema = exports.pairingRoomIdSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const devices_1 = require("../data/devices");
|
|
6
|
+
const zod_types_1 = require("../types/zod-types");
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
const base64UrlSchema = zod_1.z.string().regex(/^[A-Za-z0-9_-]+$/);
|
|
9
|
+
/** A random 128-bit pairing-room identifier encoded as unpadded base64url. */
|
|
10
|
+
exports.pairingRoomIdSchema = base64UrlSchema.length(22);
|
|
11
|
+
/** A random 256-bit pairing-room secret encoded as unpadded base64url. */
|
|
12
|
+
exports.pairingRoomSecretSchema = base64UrlSchema.length(43);
|
|
13
|
+
/** A SHA-256 value encoded as unpadded base64url. */
|
|
14
|
+
exports.pairingHashSchema = base64UrlSchema.length(43);
|
|
15
|
+
/** A random 256-bit endpoint nonce encoded as unpadded base64url. */
|
|
16
|
+
exports.pairingEndpointNonceSchema = base64UrlSchema.length(43);
|
|
17
|
+
/** Source and destination roles in a two-party pairing room. */
|
|
18
|
+
exports.pairingRoleSchema = zod_1.z.enum(["source", "destination"]);
|
|
19
|
+
/** Versioned invitation shared out-of-band from the signed-in source. */
|
|
20
|
+
exports.devicePairingInvitationSchema = zod_1.z
|
|
21
|
+
.object({
|
|
22
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
23
|
+
roomId: exports.pairingRoomIdSchema,
|
|
24
|
+
roomSecret: exports.pairingRoomSecretSchema,
|
|
25
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
26
|
+
serviceOrigin: zod_1.z
|
|
27
|
+
.string()
|
|
28
|
+
.url()
|
|
29
|
+
.max(2048)
|
|
30
|
+
.refine((value) => {
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
return (url.protocol === "https:" || url.protocol === "http:") && url.origin === value;
|
|
33
|
+
}, "Expected an HTTP(S) origin without a path"),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
/** WebRTC session description carried inside an authenticated signaling message. */
|
|
37
|
+
exports.pairingSessionDescriptionSchema = zod_1.z
|
|
38
|
+
.object({
|
|
39
|
+
type: zod_1.z.enum(["offer", "answer"]),
|
|
40
|
+
sdp: zod_1.z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(64 * 1024),
|
|
44
|
+
endpointNonce: exports.pairingEndpointNonceSchema,
|
|
45
|
+
})
|
|
46
|
+
.strict();
|
|
47
|
+
/** ICE candidate carried inside an authenticated signaling message. */
|
|
48
|
+
exports.pairingIceCandidateSchema = zod_1.z
|
|
49
|
+
.object({
|
|
50
|
+
candidate: zod_1.z.string().min(1).max(4096),
|
|
51
|
+
sdpMid: zod_1.z.string().max(256).nullable().optional(),
|
|
52
|
+
sdpMLineIndex: zod_1.z.number().int().min(0).max(65535).nullable().optional(),
|
|
53
|
+
usernameFragment: zod_1.z.string().max(256).nullable().optional(),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
const iceUrlSchema = zod_1.z
|
|
57
|
+
.string()
|
|
58
|
+
.min(1)
|
|
59
|
+
.max(2048)
|
|
60
|
+
.refine((value) => value.startsWith("stun:") || value.startsWith("turn:") || value.startsWith("turns:"), "Expected a STUN or TURN URL");
|
|
61
|
+
/** Validated ICE server configuration issued to a pairing-room member. */
|
|
62
|
+
exports.pairingIceServerSchema = zod_1.z
|
|
63
|
+
.object({
|
|
64
|
+
urls: zod_1.z.union([iceUrlSchema, zod_1.z.array(iceUrlSchema).min(1).max(16)]),
|
|
65
|
+
username: zod_1.z.string().min(1).max(256).optional(),
|
|
66
|
+
credential: zod_1.z.string().min(1).max(512).optional(),
|
|
67
|
+
})
|
|
68
|
+
.strict()
|
|
69
|
+
.refine((value) => (value.username === undefined && value.credential === undefined) ||
|
|
70
|
+
(value.username !== undefined && value.credential !== undefined), "TURN username and credential must be supplied together");
|
|
71
|
+
const pairingSignalBase = {
|
|
72
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
73
|
+
roomId: exports.pairingRoomIdSchema,
|
|
74
|
+
senderRole: exports.pairingRoleSchema,
|
|
75
|
+
sequence: zod_1.z.number().int().positive(),
|
|
76
|
+
};
|
|
77
|
+
/** Unsigned offer, answer, or ICE message before its signaling MAC is attached. */
|
|
78
|
+
exports.unsignedPairingSignalSchema = zod_1.z.discriminatedUnion("kind", [
|
|
79
|
+
zod_1.z
|
|
80
|
+
.object({
|
|
81
|
+
...pairingSignalBase,
|
|
82
|
+
kind: zod_1.z.literal("offer"),
|
|
83
|
+
payload: exports.pairingSessionDescriptionSchema.extend({ type: zod_1.z.literal("offer") }).strict(),
|
|
84
|
+
})
|
|
85
|
+
.strict(),
|
|
86
|
+
zod_1.z
|
|
87
|
+
.object({
|
|
88
|
+
...pairingSignalBase,
|
|
89
|
+
kind: zod_1.z.literal("answer"),
|
|
90
|
+
payload: exports.pairingSessionDescriptionSchema.extend({ type: zod_1.z.literal("answer") }).strict(),
|
|
91
|
+
})
|
|
92
|
+
.strict(),
|
|
93
|
+
zod_1.z
|
|
94
|
+
.object({
|
|
95
|
+
...pairingSignalBase,
|
|
96
|
+
kind: zod_1.z.literal("candidate"),
|
|
97
|
+
payload: exports.pairingIceCandidateSchema,
|
|
98
|
+
})
|
|
99
|
+
.strict(),
|
|
100
|
+
]);
|
|
101
|
+
/** HMAC-authenticated offer, answer, or ICE signaling message. */
|
|
102
|
+
exports.pairingSignalSchema = zod_1.z.discriminatedUnion("kind", [
|
|
103
|
+
zod_1.z
|
|
104
|
+
.object({
|
|
105
|
+
...pairingSignalBase,
|
|
106
|
+
kind: zod_1.z.literal("offer"),
|
|
107
|
+
payload: exports.pairingSessionDescriptionSchema.extend({ type: zod_1.z.literal("offer") }).strict(),
|
|
108
|
+
mac: exports.pairingHashSchema,
|
|
109
|
+
})
|
|
110
|
+
.strict(),
|
|
111
|
+
zod_1.z
|
|
112
|
+
.object({
|
|
113
|
+
...pairingSignalBase,
|
|
114
|
+
kind: zod_1.z.literal("answer"),
|
|
115
|
+
payload: exports.pairingSessionDescriptionSchema.extend({ type: zod_1.z.literal("answer") }).strict(),
|
|
116
|
+
mac: exports.pairingHashSchema,
|
|
117
|
+
})
|
|
118
|
+
.strict(),
|
|
119
|
+
zod_1.z
|
|
120
|
+
.object({
|
|
121
|
+
...pairingSignalBase,
|
|
122
|
+
kind: zod_1.z.literal("candidate"),
|
|
123
|
+
payload: exports.pairingIceCandidateSchema,
|
|
124
|
+
mac: exports.pairingHashSchema,
|
|
125
|
+
})
|
|
126
|
+
.strict(),
|
|
127
|
+
]);
|
|
128
|
+
/** Request from the source to create a bounded signaling room. */
|
|
129
|
+
exports.devicePairingCreateRoomRequestSchema = zod_1.z
|
|
130
|
+
.object({
|
|
131
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
132
|
+
roomId: exports.pairingRoomIdSchema,
|
|
133
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
134
|
+
roomAccessTokenHash: exports.pairingHashSchema,
|
|
135
|
+
})
|
|
136
|
+
.strict();
|
|
137
|
+
/** Request from the destination to occupy the second room slot. */
|
|
138
|
+
exports.devicePairingJoinRoomRequestSchema = zod_1.z
|
|
139
|
+
.object({
|
|
140
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
141
|
+
roomId: exports.pairingRoomIdSchema,
|
|
142
|
+
roomAccessToken: exports.pairingHashSchema,
|
|
143
|
+
})
|
|
144
|
+
.strict();
|
|
145
|
+
/** Request from the source to tear down its signaling room. */
|
|
146
|
+
exports.devicePairingDeleteRoomRequestSchema = zod_1.z
|
|
147
|
+
.object({
|
|
148
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
149
|
+
roomId: exports.pairingRoomIdSchema,
|
|
150
|
+
roomAccessToken: exports.pairingHashSchema,
|
|
151
|
+
})
|
|
152
|
+
.strict();
|
|
153
|
+
/** Public signaling-service error codes that do not echo attacker input. */
|
|
154
|
+
exports.devicePairingSignalingErrorCodeSchema = zod_1.z.enum([
|
|
155
|
+
"invalid",
|
|
156
|
+
"not-found",
|
|
157
|
+
"occupied",
|
|
158
|
+
"expired",
|
|
159
|
+
"forbidden",
|
|
160
|
+
"rate-limited",
|
|
161
|
+
"too-large",
|
|
162
|
+
"out-of-order",
|
|
163
|
+
]);
|
|
164
|
+
const devicePairingSignalingFailureSchema = zod_1.z
|
|
165
|
+
.object({
|
|
166
|
+
ok: zod_1.z.literal(false),
|
|
167
|
+
code: exports.devicePairingSignalingErrorCodeSchema,
|
|
168
|
+
})
|
|
169
|
+
.strict();
|
|
170
|
+
/** Acknowledgement returned by every signaling-room command. */
|
|
171
|
+
exports.devicePairingSignalingAckSchema = zod_1.z.discriminatedUnion("ok", [
|
|
172
|
+
zod_1.z.object({ ok: zod_1.z.literal(true) }).strict(),
|
|
173
|
+
devicePairingSignalingFailureSchema,
|
|
174
|
+
]);
|
|
175
|
+
/** Acknowledgement proving room creation/join and its service-authoritative expiry. */
|
|
176
|
+
exports.devicePairingRoomReadyAckSchema = zod_1.z.discriminatedUnion("ok", [
|
|
177
|
+
zod_1.z
|
|
178
|
+
.object({
|
|
179
|
+
ok: zod_1.z.literal(true),
|
|
180
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
181
|
+
})
|
|
182
|
+
.strict(),
|
|
183
|
+
devicePairingSignalingFailureSchema,
|
|
184
|
+
]);
|
|
185
|
+
/** Acknowledgement containing room-bound, short-lived ICE configuration. */
|
|
186
|
+
exports.devicePairingIceServersAckSchema = zod_1.z.discriminatedUnion("ok", [
|
|
187
|
+
zod_1.z
|
|
188
|
+
.object({
|
|
189
|
+
ok: zod_1.z.literal(true),
|
|
190
|
+
iceServers: zod_1.z.array(exports.pairingIceServerSchema).min(1).max(8),
|
|
191
|
+
})
|
|
192
|
+
.strict(),
|
|
193
|
+
devicePairingSignalingFailureSchema,
|
|
194
|
+
]);
|
|
195
|
+
/** Canonical transcript input derived independently by both endpoints. */
|
|
196
|
+
exports.devicePairingTranscriptSchema = zod_1.z
|
|
197
|
+
.object({
|
|
198
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
199
|
+
roomId: exports.pairingRoomIdSchema,
|
|
200
|
+
sourceIdentity: devices_1.deviceInfoSchema,
|
|
201
|
+
destinationIdentity: devices_1.deviceInfoSchema,
|
|
202
|
+
sourceEndpointNonce: exports.pairingEndpointNonceSchema,
|
|
203
|
+
destinationEndpointNonce: exports.pairingEndpointNonceSchema,
|
|
204
|
+
offerFingerprint: zod_1.z.string().min(1).max(256),
|
|
205
|
+
answerFingerprint: zod_1.z.string().min(1).max(256),
|
|
206
|
+
})
|
|
207
|
+
.strict();
|
|
208
|
+
/** Renderer-safe presentation of the remote endpoint. */
|
|
209
|
+
exports.pairingDevicePresentationSchema = zod_1.z
|
|
210
|
+
.object({
|
|
211
|
+
deviceName: devices_1.displayNameSchema.optional(),
|
|
212
|
+
userName: devices_1.displayNameSchema.optional(),
|
|
213
|
+
deviceType: zod_1.z.string().max(64).optional(),
|
|
214
|
+
})
|
|
215
|
+
.strict();
|
|
216
|
+
/** Payload exchanged by `pairing.describe` after the normal Connection handshake. */
|
|
217
|
+
exports.pairingDescriptionSchema = zod_1.z
|
|
218
|
+
.object({
|
|
219
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
220
|
+
transcriptHash: exports.pairingHashSchema,
|
|
221
|
+
device: devices_1.deviceInfoSchema,
|
|
222
|
+
presentation: exports.pairingDevicePresentationSchema,
|
|
223
|
+
})
|
|
224
|
+
.strict();
|
|
225
|
+
/** Contents of a signed, single-use user approval. */
|
|
226
|
+
exports.pairingApprovalContentsSchema = zod_1.z
|
|
227
|
+
.object({
|
|
228
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
229
|
+
approvalId: exports.pairingEndpointNonceSchema,
|
|
230
|
+
transcriptHash: exports.pairingHashSchema,
|
|
231
|
+
role: exports.pairingRoleSchema,
|
|
232
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
233
|
+
})
|
|
234
|
+
.strict();
|
|
235
|
+
/** Signed approval bound to one transcript and endpoint identity. */
|
|
236
|
+
exports.signedPairingApprovalSchema = zod_1.z
|
|
237
|
+
.object({
|
|
238
|
+
contents: exports.pairingApprovalContentsSchema,
|
|
239
|
+
signature: base64UrlSchema.length(86),
|
|
240
|
+
publicKey: base64UrlSchema.length(43),
|
|
241
|
+
})
|
|
242
|
+
.strict();
|
|
243
|
+
/** Account credentials encrypted directly to the temporary destination identity. */
|
|
244
|
+
exports.pairingCredentialContentsSchema = zod_1.z
|
|
245
|
+
.object({
|
|
246
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
247
|
+
transcriptHash: exports.pairingHashSchema,
|
|
248
|
+
expiresAt: zod_1.z.number().int().positive(),
|
|
249
|
+
userId: zod_types_1.zodPeerId,
|
|
250
|
+
secretKey: base64UrlSchema.length(86),
|
|
251
|
+
})
|
|
252
|
+
.strict();
|
|
253
|
+
/** Signed-and-boxed credential envelope sent over the WebRTC Connection. */
|
|
254
|
+
exports.pairingCredentialEnvelopeSchema = zod_1.z
|
|
255
|
+
.object({
|
|
256
|
+
contents: base64UrlSchema.min(1).max(4096),
|
|
257
|
+
nonce: base64UrlSchema.length(32),
|
|
258
|
+
fromPublicKey: base64UrlSchema.length(43),
|
|
259
|
+
})
|
|
260
|
+
.strict();
|
|
261
|
+
/** Contents of the temporary destination identity's signed installation receipt. */
|
|
262
|
+
exports.pairingInstallationReceiptContentsSchema = zod_1.z
|
|
263
|
+
.object({
|
|
264
|
+
version: zod_1.z.literal(constants_1.DEVICE_PAIRING_PROTOCOL_VERSION),
|
|
265
|
+
transcriptHash: exports.pairingHashSchema,
|
|
266
|
+
initializedDevice: devices_1.deviceInfoSchema,
|
|
267
|
+
installedAt: zod_1.z.number().int().positive(),
|
|
268
|
+
})
|
|
269
|
+
.strict();
|
|
270
|
+
/** Receipt proving installation on the authenticated temporary destination endpoint. */
|
|
271
|
+
exports.signedPairingInstallationReceiptSchema = zod_1.z
|
|
272
|
+
.object({
|
|
273
|
+
contents: exports.pairingInstallationReceiptContentsSchema,
|
|
274
|
+
signature: base64UrlSchema.length(86),
|
|
275
|
+
publicKey: base64UrlSchema.length(43),
|
|
276
|
+
})
|
|
277
|
+
.strict();
|
|
278
|
+
/** Renderer-visible phases of the short-lived pairing ceremony. */
|
|
279
|
+
exports.devicePairingPhaseSchema = zod_1.z.enum([
|
|
280
|
+
"idle",
|
|
281
|
+
"inviting",
|
|
282
|
+
"connecting",
|
|
283
|
+
"comparing",
|
|
284
|
+
"waiting-for-approval",
|
|
285
|
+
"transferring",
|
|
286
|
+
"complete",
|
|
287
|
+
"rejected",
|
|
288
|
+
"error",
|
|
289
|
+
]);
|
|
290
|
+
/** Sanitized pairing state safe to publish across the host/renderer boundary. */
|
|
291
|
+
exports.devicePairingStateSchema = zod_1.z
|
|
292
|
+
.object({
|
|
293
|
+
sessionId: zod_types_1.zodPeerId.optional(),
|
|
294
|
+
role: exports.pairingRoleSchema.optional(),
|
|
295
|
+
phase: exports.devicePairingPhaseSchema,
|
|
296
|
+
expiresAt: zod_1.z.number().int().positive().optional(),
|
|
297
|
+
remoteDevice: exports.pairingDevicePresentationSchema.optional(),
|
|
298
|
+
sas: zod_1.z
|
|
299
|
+
.string()
|
|
300
|
+
.regex(/^\d{6}$/)
|
|
301
|
+
.optional(),
|
|
302
|
+
error: zod_1.z.string().max(256).optional(),
|
|
303
|
+
})
|
|
304
|
+
.strict();
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from "./device/get-trust-level-fn";
|
|
|
13
13
|
export * from "./device/socket.type";
|
|
14
14
|
export * from "./device/socket-io-binary-peer";
|
|
15
15
|
export * from "./device/tx-encoding";
|
|
16
|
+
export * from "./device-pairing";
|
|
16
17
|
export * from "./events";
|
|
17
18
|
export * from "./group-invite";
|
|
18
19
|
export * from "./keys";
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ __exportStar(require("./device/get-trust-level-fn"), exports);
|
|
|
34
34
|
__exportStar(require("./device/socket.type"), exports);
|
|
35
35
|
__exportStar(require("./device/socket-io-binary-peer"), exports);
|
|
36
36
|
__exportStar(require("./device/tx-encoding"), exports);
|
|
37
|
+
__exportStar(require("./device-pairing"), exports);
|
|
37
38
|
__exportStar(require("./events"), exports);
|
|
38
39
|
__exportStar(require("./group-invite"), exports);
|
|
39
40
|
__exportStar(require("./keys"), exports);
|
|
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./package-author-signer"), exports);
|
|
17
18
|
__exportStar(require("./package-author-signing"), exports);
|
|
18
19
|
__exportStar(require("./package-cloner"), exports);
|
|
19
20
|
__exportStar(require("./package-creator"), exports);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type AuthorSignableFields } from "./package-author-signing";
|
|
2
|
+
/**
|
|
3
|
+
* Load and decrypt the package author signing secret key from the user's personal group.
|
|
4
|
+
* Returns `undefined` when the key is missing or cannot be decrypted.
|
|
5
|
+
*
|
|
6
|
+
* @param packageId - Package whose `packageSigningKey_<id>` secret pvar to load
|
|
7
|
+
* @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadPackageSigningKey(packageId: string, getUserSecretKey: () => Promise<string>): Promise<string | undefined>;
|
|
10
|
+
/**
|
|
11
|
+
* Sign a package version with the package author's key.
|
|
12
|
+
*
|
|
13
|
+
* Safe to call from Electron (main process) or PWA (single-process client). The
|
|
14
|
+
* signing key is loaded from a secret persistent variable and never returned to
|
|
15
|
+
* callers — only the detached signature is.
|
|
16
|
+
*
|
|
17
|
+
* Returns the base64url detached signature, or `undefined` if the signing key
|
|
18
|
+
* isn't available or signing fails. Callers that require a signature (e.g.
|
|
19
|
+
* remote publish) should treat `undefined` as an error; create/promote may leave
|
|
20
|
+
* `packageAuthorSignature` unset.
|
|
21
|
+
*
|
|
22
|
+
* @param pvFields - Author-signable PackageVersion fields
|
|
23
|
+
* @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
|
|
24
|
+
*/
|
|
25
|
+
export declare function trySignPackageAuthorVersion(pvFields: AuthorSignableFields, getUserSecretKey: () => Promise<string>): Promise<string | undefined>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadPackageSigningKey = loadPackageSigningKey;
|
|
4
|
+
exports.trySignPackageAuthorVersion = trySignPackageAuthorVersion;
|
|
5
|
+
const user_context_singleton_1 = require("../context/user-context-singleton");
|
|
6
|
+
const persistent_vars_1 = require("../data/persistent-vars");
|
|
7
|
+
const keys_1 = require("../keys");
|
|
8
|
+
const package_author_signing_1 = require("./package-author-signing");
|
|
9
|
+
/**
|
|
10
|
+
* Load and decrypt the package author signing secret key from the user's personal group.
|
|
11
|
+
* Returns `undefined` when the key is missing or cannot be decrypted.
|
|
12
|
+
*
|
|
13
|
+
* @param packageId - Package whose `packageSigningKey_<id>` secret pvar to load
|
|
14
|
+
* @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
|
|
15
|
+
*/
|
|
16
|
+
async function loadPackageSigningKey(packageId, getUserSecretKey) {
|
|
17
|
+
try {
|
|
18
|
+
const userContext = await (0, user_context_singleton_1.getUserContext)();
|
|
19
|
+
const secretKey = await getUserSecretKey();
|
|
20
|
+
const keyName = `${package_author_signing_1.PACKAGE_SIGNING_KEY_PREFIX}${packageId}`;
|
|
21
|
+
const keyPvar = await (0, persistent_vars_1.PersistentVars)(userContext.userDataContext).findOne({ name: keyName });
|
|
22
|
+
if (!keyPvar?.value?.value || !keyPvar.isSecret) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return (0, keys_1.decryptData)(keyPvar.value.value, secretKey) || undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sign a package version with the package author's key.
|
|
33
|
+
*
|
|
34
|
+
* Safe to call from Electron (main process) or PWA (single-process client). The
|
|
35
|
+
* signing key is loaded from a secret persistent variable and never returned to
|
|
36
|
+
* callers — only the detached signature is.
|
|
37
|
+
*
|
|
38
|
+
* Returns the base64url detached signature, or `undefined` if the signing key
|
|
39
|
+
* isn't available or signing fails. Callers that require a signature (e.g.
|
|
40
|
+
* remote publish) should treat `undefined` as an error; create/promote may leave
|
|
41
|
+
* `packageAuthorSignature` unset.
|
|
42
|
+
*
|
|
43
|
+
* @param pvFields - Author-signable PackageVersion fields
|
|
44
|
+
* @param getUserSecretKey - Platform callback that returns the signed-in user's secret key
|
|
45
|
+
*/
|
|
46
|
+
async function trySignPackageAuthorVersion(pvFields, getUserSecretKey) {
|
|
47
|
+
try {
|
|
48
|
+
const signingKey = await loadPackageSigningKey(pvFields.packageId, getUserSecretKey);
|
|
49
|
+
if (!signingKey) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return (0, package_author_signing_1.signPackageAuthor)(pvFields, signingKey);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DataContext } from "../context/data-context";
|
|
2
|
+
import type { IPackageVersion } from "../data/package-versions";
|
|
2
3
|
import type { IInstallOpts, IInstallResult, IPackageInfo, IPackageInstallerDeps } from "./types";
|
|
3
4
|
/**
|
|
4
5
|
* Install or update a dev PackageVersion from locally-built bundle files.
|
|
@@ -9,6 +10,20 @@ import type { IInstallOpts, IInstallResult, IPackageInfo, IPackageInstallerDeps
|
|
|
9
10
|
* triggers the package loader and device version resolution.
|
|
10
11
|
*/
|
|
11
12
|
export declare function installPackageFromBundles(dataContext: DataContext, deps: IPackageInstallerDeps, packageId: string, opts?: IInstallOpts): Promise<IInstallResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Ensure a PackageVersion has `appNavs` from its package definition.
|
|
15
|
+
* No-ops when appNavs are already present or the package cannot be loaded.
|
|
16
|
+
*
|
|
17
|
+
* Used to repair PVs that were created with `skipLoad` (startup sync while the
|
|
18
|
+
* group was not on `dev`) and later activated/promoted without nav metadata.
|
|
19
|
+
*
|
|
20
|
+
* @param dataContext - Group or personal data context holding the PV
|
|
21
|
+
* @param deps - Filesystem / path helpers
|
|
22
|
+
* @param packageId - Package that owns the version
|
|
23
|
+
* @param packageVersionId - Specific PV to repair
|
|
24
|
+
* @returns The PV after any update, or `undefined` if not found
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensurePackageVersionAppNavs(dataContext: DataContext, deps: IPackageInstallerDeps, packageId: string, packageVersionId: string): Promise<IPackageVersion | undefined>;
|
|
12
27
|
/**
|
|
13
28
|
* Install bundles for all non-disabled packages in a data context.
|
|
14
29
|
* Errors are caught per-package so one failure doesn't block others.
|
|
@@ -30,3 +45,15 @@ export declare function saveBundleFile(dataContext: DataContext, content: string
|
|
|
30
45
|
* Uses `deps.fs.readJson` instead of `require()` — no cache issues.
|
|
31
46
|
*/
|
|
32
47
|
export declare function getPackageInfo(deps: IPackageInstallerDeps, localPath: string): Promise<IPackageInfo>;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the local filesystem path for a package based on device vars.
|
|
50
|
+
* Awaits the loading promise to ensure the DB-stored value is available
|
|
51
|
+
* before reading. Falls back to paths from other data contexts on this device,
|
|
52
|
+
* then to `{packagesRootDir}/{name-slug}` when that directory exists.
|
|
53
|
+
*
|
|
54
|
+
* @param packageId - Package to resolve
|
|
55
|
+
* @param dataContext - Preferred data context for the `packageLocalPath_*` device var
|
|
56
|
+
* @param deps - Filesystem / path helpers used by the package installer
|
|
57
|
+
* @returns Absolute (or deps-resolved) path to the package directory
|
|
58
|
+
*/
|
|
59
|
+
export declare function getPackageLocalPath(packageId: string, dataContext: DataContext, deps: IPackageInstallerDeps): Promise<string>;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.installPackageFromBundles = installPackageFromBundles;
|
|
4
|
+
exports.ensurePackageVersionAppNavs = ensurePackageVersionAppNavs;
|
|
4
5
|
exports.installAllPackageBundles = installAllPackageBundles;
|
|
5
6
|
exports.saveBundleFile = saveBundleFile;
|
|
6
7
|
exports.getPackageInfo = getPackageInfo;
|
|
8
|
+
exports.getPackageLocalPath = getPackageLocalPath;
|
|
7
9
|
const file_types_1 = require("../data/files/file.types");
|
|
8
10
|
const files_1 = require("../data/files/files");
|
|
9
11
|
const package_version_resolver_1 = require("../data/package-version-resolver");
|
|
10
12
|
const package_versions_1 = require("../data/package-versions");
|
|
11
13
|
const packages_1 = require("../data/packages");
|
|
12
14
|
const persistent_vars_1 = require("../data/persistent-vars");
|
|
15
|
+
const rpc_types_1 = require("../rpc-types");
|
|
13
16
|
const utils_1 = require("../utils");
|
|
14
17
|
/**
|
|
15
18
|
* Install or update a dev PackageVersion from locally-built bundle files.
|
|
@@ -50,14 +53,22 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
|
|
|
50
53
|
const uiHash = uiBundle ? (0, file_types_1.computeFileHash)(uiBundle) : undefined;
|
|
51
54
|
// 5. Compute PV hash
|
|
52
55
|
const pvHash = (0, package_versions_1.computePackageVersionHash)(version, "", packageHash, routesHash, uiHash);
|
|
53
|
-
// 6. Early exit if unchanged
|
|
56
|
+
// 6. Early exit if unchanged (still repair missing appNavs — see syncDevBundles skipLoad)
|
|
54
57
|
const pvTable = (0, package_versions_1.PackageVersions)(dataContext);
|
|
55
58
|
const existingPv = await pvTable.findOne({ packageId, versionTag });
|
|
56
59
|
if (existingPv && existingPv.packageVersionHash === pvHash) {
|
|
60
|
+
if (existingPv.appNavs?.length) {
|
|
61
|
+
return {
|
|
62
|
+
packageVersion: existingPv,
|
|
63
|
+
loaded: undefined,
|
|
64
|
+
unchanged: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const loaded = await loadAndPersistAppNavs(dataContext, packageId, existingPv, localPath);
|
|
57
68
|
return {
|
|
58
69
|
packageVersion: existingPv,
|
|
59
|
-
loaded
|
|
60
|
-
unchanged:
|
|
70
|
+
loaded,
|
|
71
|
+
unchanged: !loaded?.appNavs?.length,
|
|
61
72
|
};
|
|
62
73
|
}
|
|
63
74
|
// 7. Save bundle files (deduped by hash)
|
|
@@ -91,22 +102,13 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
|
|
|
91
102
|
createdAt: now,
|
|
92
103
|
};
|
|
93
104
|
const savedPv = await pvTable.signAndSave(pv);
|
|
94
|
-
// 9. Load package
|
|
105
|
+
// 9. Load package for in-memory use and/or appNavs backfill.
|
|
106
|
+
// `skipLoad` is used by startup sync when the group is not on `dev` (avoid
|
|
107
|
+
// activating), but we must still persist appNavs or the apps list stays empty
|
|
108
|
+
// after that PV is later activated/promoted.
|
|
95
109
|
let loaded;
|
|
96
|
-
if (!opts?.skipLoad) {
|
|
97
|
-
|
|
98
|
-
if (pkg) {
|
|
99
|
-
loaded = await dataContext.packageLoader.loadPackage(pkg, {
|
|
100
|
-
force: true,
|
|
101
|
-
localPath,
|
|
102
|
-
packageVersionId: savedPv.packageVersionId,
|
|
103
|
-
});
|
|
104
|
-
// Extract appNavs from loaded instance onto the PV
|
|
105
|
-
if (loaded?.appNavs && JSON.stringify(loaded.appNavs) !== JSON.stringify(savedPv.appNavs)) {
|
|
106
|
-
savedPv.appNavs = loaded.appNavs;
|
|
107
|
-
await pvTable.signAndSave(savedPv);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
+
if (!opts?.skipLoad || !savedPv.appNavs?.length) {
|
|
111
|
+
loaded = await loadAndPersistAppNavs(dataContext, packageId, savedPv, localPath);
|
|
110
112
|
}
|
|
111
113
|
// 10. Update package record if needed (description, first-install activation)
|
|
112
114
|
const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
|
|
@@ -134,6 +136,69 @@ async function installPackageFromBundles(dataContext, deps, packageId, opts) {
|
|
|
134
136
|
}
|
|
135
137
|
return { packageVersion: savedPv, loaded, unchanged: false };
|
|
136
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Load a package bundle and persist `appNavs` onto the PackageVersion when the
|
|
141
|
+
* definition provides them.
|
|
142
|
+
*
|
|
143
|
+
* If the PV already has a `packageAuthorSignature`, refresh it (or clear it when
|
|
144
|
+
* the signing key is unavailable) because `appNavs` is part of the author-signed
|
|
145
|
+
* payload.
|
|
146
|
+
*/
|
|
147
|
+
async function loadAndPersistAppNavs(dataContext, packageId, pv, localPath) {
|
|
148
|
+
const pkg = await (0, packages_1.Packages)(dataContext).get(packageId);
|
|
149
|
+
if (!pkg)
|
|
150
|
+
return undefined;
|
|
151
|
+
const loaded = await dataContext.packageLoader.loadPackage(pkg, {
|
|
152
|
+
force: true,
|
|
153
|
+
localPath,
|
|
154
|
+
packageVersionId: pv.packageVersionId,
|
|
155
|
+
});
|
|
156
|
+
if (!loaded?.appNavs?.length)
|
|
157
|
+
return loaded;
|
|
158
|
+
if (JSON.stringify(loaded.appNavs) === JSON.stringify(pv.appNavs))
|
|
159
|
+
return loaded;
|
|
160
|
+
pv.appNavs = loaded.appNavs;
|
|
161
|
+
if (pv.packageAuthorSignature) {
|
|
162
|
+
const authorSig = await rpc_types_1.rpcServerCalls.signPackageAuthorVersion({
|
|
163
|
+
packageId: pv.packageId,
|
|
164
|
+
packageVersionId: pv.packageVersionId,
|
|
165
|
+
version: pv.version,
|
|
166
|
+
versionTag: pv.versionTag ?? "",
|
|
167
|
+
packageBundleFileHash: pv.packageBundleFileHash,
|
|
168
|
+
routesBundleFileHash: pv.routesBundleFileHash,
|
|
169
|
+
uiBundleFileHash: pv.uiBundleFileHash,
|
|
170
|
+
appNavs: pv.appNavs,
|
|
171
|
+
createdBy: pv.createdBy,
|
|
172
|
+
createdAt: pv.createdAt,
|
|
173
|
+
});
|
|
174
|
+
pv.packageAuthorSignature = authorSig;
|
|
175
|
+
}
|
|
176
|
+
await (0, package_versions_1.PackageVersions)(dataContext).signAndSave(pv);
|
|
177
|
+
return loaded;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Ensure a PackageVersion has `appNavs` from its package definition.
|
|
181
|
+
* No-ops when appNavs are already present or the package cannot be loaded.
|
|
182
|
+
*
|
|
183
|
+
* Used to repair PVs that were created with `skipLoad` (startup sync while the
|
|
184
|
+
* group was not on `dev`) and later activated/promoted without nav metadata.
|
|
185
|
+
*
|
|
186
|
+
* @param dataContext - Group or personal data context holding the PV
|
|
187
|
+
* @param deps - Filesystem / path helpers
|
|
188
|
+
* @param packageId - Package that owns the version
|
|
189
|
+
* @param packageVersionId - Specific PV to repair
|
|
190
|
+
* @returns The PV after any update, or `undefined` if not found
|
|
191
|
+
*/
|
|
192
|
+
async function ensurePackageVersionAppNavs(dataContext, deps, packageId, packageVersionId) {
|
|
193
|
+
const pv = await (0, package_versions_1.PackageVersions)(dataContext).get(packageVersionId);
|
|
194
|
+
if (!pv)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (pv.appNavs?.length)
|
|
197
|
+
return pv;
|
|
198
|
+
const localPath = await getPackageLocalPath(packageId, dataContext, deps);
|
|
199
|
+
await loadAndPersistAppNavs(dataContext, packageId, pv, localPath);
|
|
200
|
+
return pv;
|
|
201
|
+
}
|
|
137
202
|
/**
|
|
138
203
|
* Install bundles for all non-disabled packages in a data context.
|
|
139
204
|
* Errors are caught per-package so one failure doesn't block others.
|
|
@@ -247,6 +312,11 @@ function packageNameToFolderSlug(name) {
|
|
|
247
312
|
* Awaits the loading promise to ensure the DB-stored value is available
|
|
248
313
|
* before reading. Falls back to paths from other data contexts on this device,
|
|
249
314
|
* then to `{packagesRootDir}/{name-slug}` when that directory exists.
|
|
315
|
+
*
|
|
316
|
+
* @param packageId - Package to resolve
|
|
317
|
+
* @param dataContext - Preferred data context for the `packageLocalPath_*` device var
|
|
318
|
+
* @param deps - Filesystem / path helpers used by the package installer
|
|
319
|
+
* @returns Absolute (or deps-resolved) path to the package directory
|
|
250
320
|
*/
|
|
251
321
|
async function getPackageLocalPath(packageId, dataContext, deps) {
|
|
252
322
|
const candidates = [];
|
|
@@ -440,6 +440,55 @@ describe("installPackageFromBundles", () => {
|
|
|
440
440
|
const secondCall = mockSignAndSavePV.mock.calls[1][0];
|
|
441
441
|
expect(secondCall.appNavs).toEqual(appNavs);
|
|
442
442
|
});
|
|
443
|
+
it("backfills appNavs even when skipLoad is true", async () => {
|
|
444
|
+
const { deps } = setupBasicInstall();
|
|
445
|
+
const appNavs = [{ name: "App", iconClassName: "bi bi-app", navigationPath: "app" }];
|
|
446
|
+
const mockLoadPackage = jest.fn().mockResolvedValue({ packageId, appNavs });
|
|
447
|
+
const dc = mockDataContext({
|
|
448
|
+
packageLoader: { loadPackage: mockLoadPackage },
|
|
449
|
+
});
|
|
450
|
+
await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, {
|
|
451
|
+
skipLoad: true,
|
|
452
|
+
skipResolve: true,
|
|
453
|
+
});
|
|
454
|
+
expect(mockLoadPackage).toHaveBeenCalled();
|
|
455
|
+
expect(mockSignAndSavePV).toHaveBeenCalledTimes(2);
|
|
456
|
+
expect(mockSignAndSavePV.mock.calls[1][0].appNavs).toEqual(appNavs);
|
|
457
|
+
});
|
|
458
|
+
it("repairs missing appNavs on unchanged early-return", async () => {
|
|
459
|
+
const { deps } = setupBasicInstall();
|
|
460
|
+
const packageHash = (0, file_types_1.computeFileHash)(packageBundle);
|
|
461
|
+
const routesHash = (0, file_types_1.computeFileHash)(routesBundle);
|
|
462
|
+
const uiHash = (0, file_types_1.computeFileHash)(uiBundle);
|
|
463
|
+
const { computePackageVersionHash } = jest.requireActual("../data/package-versions");
|
|
464
|
+
const expectedPvHash = computePackageVersionHash("1.2.3", "", packageHash, routesHash, uiHash);
|
|
465
|
+
const appNavs = [{ name: "App", iconClassName: "bi bi-app", navigationPath: "app" }];
|
|
466
|
+
mockFindOnePV.mockResolvedValue({
|
|
467
|
+
packageVersionId: (0, utils_1.newid)(),
|
|
468
|
+
packageId,
|
|
469
|
+
version: "1.2.3",
|
|
470
|
+
versionTag: "dev",
|
|
471
|
+
packageVersionHash: expectedPvHash,
|
|
472
|
+
packageBundleFileHash: packageHash,
|
|
473
|
+
routesBundleFileHash: routesHash,
|
|
474
|
+
uiBundleFileHash: uiHash,
|
|
475
|
+
appNavs: undefined,
|
|
476
|
+
signature: "sig",
|
|
477
|
+
createdBy: "user1",
|
|
478
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
479
|
+
});
|
|
480
|
+
const mockLoadPackage = jest.fn().mockResolvedValue({ packageId, appNavs });
|
|
481
|
+
const dc = mockDataContext({
|
|
482
|
+
packageLoader: { loadPackage: mockLoadPackage },
|
|
483
|
+
});
|
|
484
|
+
const result = await (0, package_installer_1.installPackageFromBundles)(dc, deps, packageId, {
|
|
485
|
+
skipLoad: true,
|
|
486
|
+
skipResolve: true,
|
|
487
|
+
});
|
|
488
|
+
expect(mockLoadPackage).toHaveBeenCalled();
|
|
489
|
+
expect(result.packageVersion.appNavs).toEqual(appNavs);
|
|
490
|
+
expect(mockSignAndSavePV).toHaveBeenCalledTimes(1);
|
|
491
|
+
});
|
|
443
492
|
it("calls updatePackagePrefs and resolveDevicePackageVersion when skipResolve is false", async () => {
|
|
444
493
|
const { deps } = setupBasicInstall();
|
|
445
494
|
const dc = mockDataContext();
|