lody 0.62.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/diff-line-counts-BLxwWP6r.js +675 -0
- package/dist/chunks/embedded-prompt-D__ujYMd.js +4 -0
- package/dist/chunks/index-B9fKl40V.js +308 -0
- package/dist/chunks/index-CUUne095.js +33 -0
- package/dist/chunks/index-v46gaGAl.js +17 -0
- package/dist/chunks/loro_wasm_bg-N-tMVpou.js +5197 -0
- package/dist/chunks/review-viewer-C61Z0Yud.js +80 -0
- package/dist/chunks/sparse-text-D7zcV2O5.js +649 -0
- package/dist/chunks/turn-diff-replay-qRat9u1k.js +311 -0
- package/dist/diff-worker.js +11 -0
- package/dist/index.js +13382 -14116
- package/dist/turn-diff-replay-worker.js +16 -0
- package/package.json +15 -12
- package/dist/chunks/index-DekwrpNR.js +0 -449
- package/dist/chunks/index-Dr2JkTB2.js +0 -10121
- package/dist/chunks/loro_wasm_bg-BV-n7JyC.js +0 -5198
- package/dist/chunks/share-link-U1EVLOdF.js +0 -974
|
@@ -1,974 +0,0 @@
|
|
|
1
|
-
import { s as deriveSessionKeyBytes, q as decryptEnvelopeV1, l as computeBlobDigest, d as CodeSessionError, t as encryptEnvelopeV1, P as utf8Decode, Q as utf8Encode, k as canonicalJson, cN as getSubtle, cO as toArrayBuffer, f as base64urlEncode, N as signHostPresence, Y as verifyHostPresence, c as validateMailboxSegment, I as randomBytes, D as DEFAULT_CODE_COLLAB_FEATURES_V1, w as generateContentKey, V as verifierFromPrivateKey, y as generateSigningPrivateKey, x as generatePreferredSigningPrivateKey, C as CORE_CODE_COLLAB_FEATURES_V1, a as validateSpaceUuid, R as validateBlobPathPrefix, v as validateBucketId, b as validateStreamPrefix, e as base64urlDecode, j as buildStreamPrefix, S as validateSerializedSigningPrivateKey, T as validateSerializedVerifierKey, __tla as __tla_0 } from "../index.js";
|
|
2
|
-
let serializeShareLinkV1, unwrapEncryptedEphemeralStateEnvelopeV1, validateShareLinkSecretV1, validateShareServerUrl, verifyHostPresenceV1, REQUIRED_CORE_CODE_COLLAB_FEATURES_V1, assertCompatibleSessionStateV1, assertSessionNotExpiredV1, assertShareLinkMatchesSessionStateV1, buildBlobEnvelopeAadV1, buildEphemeralEnvelopeAadV1, computeEphemeralStateKeyV1, createCursorStateV1, createEncryptedEphemeralStateEnvelopeV1, createHostPresenceV1, createInitialSessionStateV1, createKeyId, createParticipantPresenceV1, createSelectionStateV1, createShareLinkSecretV1, decryptBlobObjectV1, decryptEphemeralPayloadV1, encryptBlobObjectV1, encryptEphemeralPayloadV1, generateHostSessionSecretsV1, parseCursorStateV1, parseEncryptedBlobObjectV1, parseHostPresenceV1, parseParticipantPresenceV1, parseSelectionStateV1, parseShareLinkV1, serializeEncryptedBlobObjectV1;
|
|
3
|
-
let __tla = Promise.all([
|
|
4
|
-
(() => {
|
|
5
|
-
try {
|
|
6
|
-
return __tla_0;
|
|
7
|
-
} catch {
|
|
8
|
-
}
|
|
9
|
-
})()
|
|
10
|
-
]).then(async () => {
|
|
11
|
-
buildBlobEnvelopeAadV1 = function(input) {
|
|
12
|
-
return {
|
|
13
|
-
v: 1,
|
|
14
|
-
kind: "blob",
|
|
15
|
-
spaceUuid: input.spaceUuid,
|
|
16
|
-
fileId: input.fileId,
|
|
17
|
-
digest: input.digest,
|
|
18
|
-
kid: input.kid
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
encryptBlobObjectV1 = async function(input) {
|
|
22
|
-
const digest = await computeBlobDigest(input.contentKey, input.plaintext);
|
|
23
|
-
const blobKey = await deriveSessionKeyBytes(input.contentKey, "blob");
|
|
24
|
-
const envelope = await encryptEnvelopeV1({
|
|
25
|
-
keyBytes: blobKey,
|
|
26
|
-
kid: input.contentKeyId,
|
|
27
|
-
aad: buildBlobEnvelopeAadV1({
|
|
28
|
-
spaceUuid: input.spaceUuid,
|
|
29
|
-
fileId: input.fileId,
|
|
30
|
-
digest,
|
|
31
|
-
kid: input.contentKeyId
|
|
32
|
-
}),
|
|
33
|
-
plaintext: input.plaintext,
|
|
34
|
-
nonce: input.nonce
|
|
35
|
-
});
|
|
36
|
-
return {
|
|
37
|
-
digest,
|
|
38
|
-
envelope
|
|
39
|
-
};
|
|
40
|
-
};
|
|
41
|
-
decryptBlobObjectV1 = async function(input) {
|
|
42
|
-
const blobKey = await deriveSessionKeyBytes(input.contentKey, "blob");
|
|
43
|
-
const plaintext = await decryptEnvelopeV1({
|
|
44
|
-
keyBytes: blobKey,
|
|
45
|
-
envelope: input.envelope,
|
|
46
|
-
expectedAad: buildBlobEnvelopeAadV1({
|
|
47
|
-
spaceUuid: input.spaceUuid,
|
|
48
|
-
fileId: input.fileId,
|
|
49
|
-
digest: input.digest,
|
|
50
|
-
kid: input.envelope.kid
|
|
51
|
-
})
|
|
52
|
-
});
|
|
53
|
-
const actualDigest = await computeBlobDigest(input.contentKey, plaintext);
|
|
54
|
-
if (actualDigest !== input.digest) {
|
|
55
|
-
throw new CodeSessionError("blob_digest_mismatch", "Blob plaintext digest does not match BlobRef");
|
|
56
|
-
}
|
|
57
|
-
return plaintext;
|
|
58
|
-
};
|
|
59
|
-
serializeEncryptedBlobObjectV1 = function(object) {
|
|
60
|
-
return utf8Encode(canonicalJson(object));
|
|
61
|
-
};
|
|
62
|
-
parseEncryptedBlobObjectV1 = function(bytes) {
|
|
63
|
-
const parsed = JSON.parse(utf8Decode(bytes));
|
|
64
|
-
if (!parsed || typeof parsed !== "object") {
|
|
65
|
-
throw new CodeSessionError("invalid_blob_object", "Encrypted blob object must be an object");
|
|
66
|
-
}
|
|
67
|
-
const value = parsed;
|
|
68
|
-
if (typeof value.digest !== "string" || !isEncryptedEnvelopeV1$1(value.envelope)) {
|
|
69
|
-
throw new CodeSessionError("invalid_blob_object", "Encrypted blob object is malformed");
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
digest: value.digest,
|
|
73
|
-
envelope: value.envelope
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
function isEncryptedEnvelopeV1$1(value) {
|
|
77
|
-
if (!value || typeof value !== "object") return false;
|
|
78
|
-
const envelope = value;
|
|
79
|
-
return envelope.v === 1 && envelope.alg === "A256GCM" && typeof envelope.kid === "string" && typeof envelope.nonce === "string" && typeof envelope.aad === "string" && typeof envelope.ciphertext === "string";
|
|
80
|
-
}
|
|
81
|
-
buildEphemeralEnvelopeAadV1 = function(input) {
|
|
82
|
-
return {
|
|
83
|
-
v: 1,
|
|
84
|
-
kind: "ephemeral",
|
|
85
|
-
spaceUuid: input.spaceUuid,
|
|
86
|
-
streamId: input.streamId,
|
|
87
|
-
channel: input.channel,
|
|
88
|
-
kid: input.kid
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
computeEphemeralStateKeyV1 = async function(input) {
|
|
92
|
-
const ephemeralKey = await deriveSessionKeyBytes(input.contentKey, "ephemeral");
|
|
93
|
-
const key = await getSubtle().importKey("raw", toArrayBuffer(ephemeralKey), {
|
|
94
|
-
name: "HMAC",
|
|
95
|
-
hash: "SHA-256"
|
|
96
|
-
}, false, [
|
|
97
|
-
"sign"
|
|
98
|
-
]);
|
|
99
|
-
const payload = utf8Encode(canonicalJson({
|
|
100
|
-
v: 1,
|
|
101
|
-
kind: "ephemeral-state-key",
|
|
102
|
-
spaceUuid: input.spaceUuid,
|
|
103
|
-
streamId: input.streamId,
|
|
104
|
-
channel: input.channel,
|
|
105
|
-
subject: input.subject
|
|
106
|
-
}));
|
|
107
|
-
const signature = await getSubtle().sign("HMAC", key, toArrayBuffer(payload));
|
|
108
|
-
return `esk1-${base64urlEncode(new Uint8Array(signature))}`;
|
|
109
|
-
};
|
|
110
|
-
createEncryptedEphemeralStateEnvelopeV1 = function(input) {
|
|
111
|
-
if (!input.stateKey.startsWith("esk1-")) {
|
|
112
|
-
throw new CodeSessionError("invalid_ephemeral_state_key", "Ephemeral state key is invalid");
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
v: 1,
|
|
116
|
-
kind: "loro-code-collab/ephemeral-state",
|
|
117
|
-
stateKey: input.stateKey,
|
|
118
|
-
envelope: input.envelope
|
|
119
|
-
};
|
|
120
|
-
};
|
|
121
|
-
unwrapEncryptedEphemeralStateEnvelopeV1 = function(value) {
|
|
122
|
-
if (isEncryptedEnvelopeV1(value)) return value;
|
|
123
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
124
|
-
throw new CodeSessionError("invalid_ephemeral_envelope", "Ephemeral envelope must be an object");
|
|
125
|
-
}
|
|
126
|
-
const record = value;
|
|
127
|
-
if (record.v !== 1 || record.kind !== "loro-code-collab/ephemeral-state" || typeof record.stateKey !== "string" || !isEncryptedEnvelopeV1(record.envelope)) {
|
|
128
|
-
throw new CodeSessionError("invalid_ephemeral_envelope", "Invalid ephemeral state envelope");
|
|
129
|
-
}
|
|
130
|
-
return record.envelope;
|
|
131
|
-
};
|
|
132
|
-
encryptEphemeralPayloadV1 = async function(input) {
|
|
133
|
-
const ephemeralKey = await deriveSessionKeyBytes(input.contentKey, "ephemeral");
|
|
134
|
-
return encryptEnvelopeV1({
|
|
135
|
-
keyBytes: ephemeralKey,
|
|
136
|
-
kid: input.contentKeyId,
|
|
137
|
-
aad: buildEphemeralEnvelopeAadV1({
|
|
138
|
-
spaceUuid: input.spaceUuid,
|
|
139
|
-
streamId: input.streamId,
|
|
140
|
-
channel: input.channel,
|
|
141
|
-
kid: input.contentKeyId
|
|
142
|
-
}),
|
|
143
|
-
plaintext: input.plaintext,
|
|
144
|
-
nonce: input.nonce
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
decryptEphemeralPayloadV1 = async function(input) {
|
|
148
|
-
const ephemeralKey = await deriveSessionKeyBytes(input.contentKey, "ephemeral");
|
|
149
|
-
return decryptEnvelopeV1({
|
|
150
|
-
keyBytes: ephemeralKey,
|
|
151
|
-
envelope: input.envelope,
|
|
152
|
-
expectedAad: buildEphemeralEnvelopeAadV1({
|
|
153
|
-
spaceUuid: input.spaceUuid,
|
|
154
|
-
streamId: input.streamId,
|
|
155
|
-
channel: input.channel,
|
|
156
|
-
kid: input.envelope.kid
|
|
157
|
-
})
|
|
158
|
-
});
|
|
159
|
-
};
|
|
160
|
-
createHostPresenceV1 = async function(input) {
|
|
161
|
-
const signed = await signHostPresence(input);
|
|
162
|
-
return {
|
|
163
|
-
peerId: input.peerId,
|
|
164
|
-
hostEpoch: input.hostEpoch,
|
|
165
|
-
kind: "host",
|
|
166
|
-
status: "online",
|
|
167
|
-
updatedAt: signed.updatedAt,
|
|
168
|
-
nonce: signed.nonce,
|
|
169
|
-
keyId: signed.keyId,
|
|
170
|
-
signature: signed.signature
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
verifyHostPresenceV1 = async function(input) {
|
|
174
|
-
if (input.presence.kind !== "host" || input.presence.status !== "online") return false;
|
|
175
|
-
return verifyHostPresence({
|
|
176
|
-
verifier: input.verifier,
|
|
177
|
-
signed: {
|
|
178
|
-
keyId: input.presence.keyId,
|
|
179
|
-
updatedAt: input.presence.updatedAt,
|
|
180
|
-
nonce: input.presence.nonce,
|
|
181
|
-
signature: input.presence.signature
|
|
182
|
-
},
|
|
183
|
-
spaceUuid: input.spaceUuid,
|
|
184
|
-
peerId: input.presence.peerId,
|
|
185
|
-
hostEpoch: input.presence.hostEpoch,
|
|
186
|
-
maxSkewMs: input.maxSkewMs,
|
|
187
|
-
now: input.now
|
|
188
|
-
});
|
|
189
|
-
};
|
|
190
|
-
parseHostPresenceV1 = function(value) {
|
|
191
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
192
|
-
throw new CodeSessionError("invalid_presence", "Host presence must be an object");
|
|
193
|
-
}
|
|
194
|
-
const presence = value;
|
|
195
|
-
if (typeof presence.peerId !== "string" || typeof presence.hostEpoch !== "string" || presence.kind !== "host" || presence.status !== "online" || typeof presence.updatedAt !== "number" || typeof presence.nonce !== "string" || typeof presence.keyId !== "string" || typeof presence.signature !== "string") {
|
|
196
|
-
throw new CodeSessionError("invalid_presence", "Invalid host presence payload");
|
|
197
|
-
}
|
|
198
|
-
validatePresenceMailboxSegment(presence.peerId, "peerId");
|
|
199
|
-
validatePresenceMailboxSegment(presence.hostEpoch, "hostEpoch");
|
|
200
|
-
if (!isSafeTimestamp(presence.updatedAt)) {
|
|
201
|
-
throw new CodeSessionError("invalid_presence", "Host presence updatedAt must be a non-negative safe integer");
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
peerId: presence.peerId,
|
|
205
|
-
hostEpoch: presence.hostEpoch,
|
|
206
|
-
kind: "host",
|
|
207
|
-
status: "online",
|
|
208
|
-
updatedAt: presence.updatedAt,
|
|
209
|
-
nonce: presence.nonce,
|
|
210
|
-
keyId: presence.keyId,
|
|
211
|
-
signature: presence.signature
|
|
212
|
-
};
|
|
213
|
-
};
|
|
214
|
-
function isEncryptedEnvelopeV1(value) {
|
|
215
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
216
|
-
const envelope = value;
|
|
217
|
-
return envelope.v === 1 && envelope.alg === "A256GCM" && typeof envelope.kid === "string" && typeof envelope.nonce === "string" && typeof envelope.aad === "string" && typeof envelope.ciphertext === "string";
|
|
218
|
-
}
|
|
219
|
-
createParticipantPresenceV1 = function(input) {
|
|
220
|
-
validatePresenceMailboxSegment(input.peerId, "peerId");
|
|
221
|
-
if (!isSafeTimestamp(input.updatedAt)) {
|
|
222
|
-
throw new CodeSessionError("invalid_presence", "Participant presence updatedAt must be a non-negative safe integer");
|
|
223
|
-
}
|
|
224
|
-
return {
|
|
225
|
-
v: 1,
|
|
226
|
-
kind: "participant",
|
|
227
|
-
peerId: input.peerId,
|
|
228
|
-
role: input.role,
|
|
229
|
-
...input.inviteId === void 0 ? {} : {
|
|
230
|
-
inviteId: input.inviteId
|
|
231
|
-
},
|
|
232
|
-
...input.displayName === void 0 ? {} : {
|
|
233
|
-
displayName: input.displayName
|
|
234
|
-
},
|
|
235
|
-
...input.color === void 0 ? {} : {
|
|
236
|
-
color: input.color
|
|
237
|
-
},
|
|
238
|
-
...input.activeFileId === void 0 ? {} : {
|
|
239
|
-
activeFileId: input.activeFileId
|
|
240
|
-
},
|
|
241
|
-
updatedAt: input.updatedAt,
|
|
242
|
-
...input.identity === void 0 ? {} : {
|
|
243
|
-
identity: input.identity
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
createCursorStateV1 = function(input) {
|
|
248
|
-
validatePresenceMailboxSegment(input.peerId, "peerId");
|
|
249
|
-
if (!isSafeTimestamp(input.updatedAt)) {
|
|
250
|
-
throw new CodeSessionError("invalid_cursor", "Cursor updatedAt must be a non-negative safe integer");
|
|
251
|
-
}
|
|
252
|
-
return {
|
|
253
|
-
v: 1,
|
|
254
|
-
kind: "cursor",
|
|
255
|
-
peerId: input.peerId,
|
|
256
|
-
fileId: input.fileId,
|
|
257
|
-
anchor: input.anchor,
|
|
258
|
-
...input.head === void 0 ? {} : {
|
|
259
|
-
head: input.head
|
|
260
|
-
},
|
|
261
|
-
updatedAt: input.updatedAt
|
|
262
|
-
};
|
|
263
|
-
};
|
|
264
|
-
createSelectionStateV1 = function(input) {
|
|
265
|
-
validatePresenceMailboxSegment(input.peerId, "peerId");
|
|
266
|
-
if (!isSafeTimestamp(input.updatedAt)) {
|
|
267
|
-
throw new CodeSessionError("invalid_selection", "Selection updatedAt must be a non-negative safe integer");
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
v: 1,
|
|
271
|
-
kind: "selection",
|
|
272
|
-
peerId: input.peerId,
|
|
273
|
-
fileId: input.fileId,
|
|
274
|
-
anchor: input.anchor,
|
|
275
|
-
head: input.head,
|
|
276
|
-
updatedAt: input.updatedAt
|
|
277
|
-
};
|
|
278
|
-
};
|
|
279
|
-
parseParticipantPresenceV1 = function(value) {
|
|
280
|
-
const record = requireRecord(value, "Participant presence");
|
|
281
|
-
if (record.v !== 1 || record.kind !== "participant" || typeof record.peerId !== "string" || !isPresenceRole(record.role) || !isSafeTimestamp(record.updatedAt)) {
|
|
282
|
-
throw new CodeSessionError("invalid_presence", "Invalid participant presence payload");
|
|
283
|
-
}
|
|
284
|
-
return createParticipantPresenceV1({
|
|
285
|
-
peerId: record.peerId,
|
|
286
|
-
role: record.role,
|
|
287
|
-
inviteId: optionalString(record.inviteId, "inviteId"),
|
|
288
|
-
displayName: optionalString(record.displayName, "displayName"),
|
|
289
|
-
color: optionalString(record.color, "color"),
|
|
290
|
-
activeFileId: optionalString(record.activeFileId, "activeFileId"),
|
|
291
|
-
identity: parseOptionalIdentity(record.identity),
|
|
292
|
-
updatedAt: record.updatedAt
|
|
293
|
-
});
|
|
294
|
-
};
|
|
295
|
-
parseCursorStateV1 = function(value) {
|
|
296
|
-
const record = requireRecord(value, "Cursor state");
|
|
297
|
-
if (record.v !== 1 || record.kind !== "cursor" || typeof record.peerId !== "string" || typeof record.fileId !== "string" || !isSafeTimestamp(record.updatedAt)) {
|
|
298
|
-
throw new CodeSessionError("invalid_cursor", "Invalid cursor state payload");
|
|
299
|
-
}
|
|
300
|
-
return createCursorStateV1({
|
|
301
|
-
peerId: record.peerId,
|
|
302
|
-
fileId: record.fileId,
|
|
303
|
-
anchor: parseCursorAnchor(record.anchor),
|
|
304
|
-
head: record.head === void 0 ? void 0 : parseCursorAnchor(record.head),
|
|
305
|
-
updatedAt: record.updatedAt
|
|
306
|
-
});
|
|
307
|
-
};
|
|
308
|
-
parseSelectionStateV1 = function(value) {
|
|
309
|
-
const record = requireRecord(value, "Selection state");
|
|
310
|
-
if (record.v !== 1 || record.kind !== "selection" || typeof record.peerId !== "string" || typeof record.fileId !== "string" || !isSafeTimestamp(record.updatedAt)) {
|
|
311
|
-
throw new CodeSessionError("invalid_selection", "Invalid selection state payload");
|
|
312
|
-
}
|
|
313
|
-
return createSelectionStateV1({
|
|
314
|
-
peerId: record.peerId,
|
|
315
|
-
fileId: record.fileId,
|
|
316
|
-
anchor: parseCursorAnchor(record.anchor),
|
|
317
|
-
head: parseCursorAnchor(record.head),
|
|
318
|
-
updatedAt: record.updatedAt
|
|
319
|
-
});
|
|
320
|
-
};
|
|
321
|
-
function requireRecord(value, label) {
|
|
322
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
323
|
-
throw new CodeSessionError("invalid_ephemeral_state", `${label} must be an object`);
|
|
324
|
-
}
|
|
325
|
-
return value;
|
|
326
|
-
}
|
|
327
|
-
function parseCursorAnchor(value) {
|
|
328
|
-
const record = requireRecord(value, "Cursor anchor");
|
|
329
|
-
if (typeof record.offset !== "number" || !Number.isFinite(record.offset) || record.offset < 0) {
|
|
330
|
-
throw new CodeSessionError("invalid_cursor", "Cursor anchor offset must be a non-negative number");
|
|
331
|
-
}
|
|
332
|
-
return {
|
|
333
|
-
offset: Math.trunc(record.offset),
|
|
334
|
-
...record.loroCursor === void 0 ? {} : {
|
|
335
|
-
loroCursor: parseEncodedLoroCursor(record.loroCursor)
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
function parseEncodedLoroCursor(value) {
|
|
340
|
-
const record = requireRecord(value, "Loro cursor anchor");
|
|
341
|
-
if (record.v !== 1 || record.kind !== "loro-cursor" || typeof record.encoded !== "string" || record.encoded.length === 0) {
|
|
342
|
-
throw new CodeSessionError("invalid_cursor", "Invalid Loro cursor anchor");
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
v: 1,
|
|
346
|
-
kind: "loro-cursor",
|
|
347
|
-
encoded: record.encoded
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
function parseOptionalIdentity(value) {
|
|
351
|
-
if (value === void 0) return void 0;
|
|
352
|
-
const record = requireRecord(value, "Participant identity");
|
|
353
|
-
if (record.mode !== "anonymous" && record.mode !== "verified") {
|
|
354
|
-
throw new CodeSessionError("invalid_presence", "Invalid participant identity mode");
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
mode: record.mode,
|
|
358
|
-
...optionalStringField("issuer", record.issuer),
|
|
359
|
-
...optionalStringField("subject", record.subject),
|
|
360
|
-
...optionalStringField("userId", record.userId),
|
|
361
|
-
...optionalStringField("displayName", record.displayName),
|
|
362
|
-
...optionalStringField("avatarUrl", record.avatarUrl)
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
function optionalString(value, field) {
|
|
366
|
-
if (value === void 0) return void 0;
|
|
367
|
-
if (typeof value !== "string") {
|
|
368
|
-
throw new CodeSessionError("invalid_presence", `${field} must be a string`);
|
|
369
|
-
}
|
|
370
|
-
return value;
|
|
371
|
-
}
|
|
372
|
-
function optionalStringField(key, value) {
|
|
373
|
-
const parsed = optionalString(value, key);
|
|
374
|
-
return parsed === void 0 ? {} : {
|
|
375
|
-
[key]: parsed
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
function isPresenceRole(value) {
|
|
379
|
-
return value === "host" || value === "write" || value === "read";
|
|
380
|
-
}
|
|
381
|
-
function validatePresenceMailboxSegment(value, label) {
|
|
382
|
-
try {
|
|
383
|
-
validateMailboxSegment(value, label);
|
|
384
|
-
} catch {
|
|
385
|
-
throw new CodeSessionError("invalid_presence", `${label} is invalid`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
function isSafeTimestamp(value) {
|
|
389
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
390
|
-
}
|
|
391
|
-
generateHostSessionSecretsV1 = async function(options = {}) {
|
|
392
|
-
const contentKey = options.contentKey ?? generateContentKey();
|
|
393
|
-
const contentKeyId = options.contentKeyId ?? createKeyId("ck");
|
|
394
|
-
const [readSigningKey, writeSigningKey, hostPresenceSigningKey] = await Promise.all([
|
|
395
|
-
generateSessionSigningKey({
|
|
396
|
-
alg: options.alg,
|
|
397
|
-
keyId: options.readKeyId ?? createKeyId("read")
|
|
398
|
-
}),
|
|
399
|
-
generateSessionSigningKey({
|
|
400
|
-
alg: options.alg,
|
|
401
|
-
keyId: options.writeKeyId ?? createKeyId("write")
|
|
402
|
-
}),
|
|
403
|
-
generateSessionSigningKey({
|
|
404
|
-
alg: options.alg,
|
|
405
|
-
keyId: options.hostPresenceKeyId ?? createKeyId("host")
|
|
406
|
-
})
|
|
407
|
-
]);
|
|
408
|
-
const hostPresenceVerifier = verifierFromPrivateKey(hostPresenceSigningKey);
|
|
409
|
-
return {
|
|
410
|
-
v: 1,
|
|
411
|
-
contentKey: base64urlEncode(contentKey),
|
|
412
|
-
contentKeyId,
|
|
413
|
-
readSigningKey,
|
|
414
|
-
writeSigningKey,
|
|
415
|
-
hostPresenceSigningKey,
|
|
416
|
-
hostPresenceVerifier,
|
|
417
|
-
createShareSecret(input) {
|
|
418
|
-
return createShareLinkSecretV1({
|
|
419
|
-
contentKey: base64urlEncode(contentKey),
|
|
420
|
-
contentKeyId,
|
|
421
|
-
readSigningKey,
|
|
422
|
-
writeSigningKey,
|
|
423
|
-
hostPresenceVerifier,
|
|
424
|
-
...input
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
};
|
|
429
|
-
createShareLinkSecretV1 = function(input) {
|
|
430
|
-
const base = {
|
|
431
|
-
v: 1,
|
|
432
|
-
kind: "loro-code-collab/share-secret",
|
|
433
|
-
inviteId: input.inviteId,
|
|
434
|
-
role: input.role,
|
|
435
|
-
contentKey: input.contentKey,
|
|
436
|
-
contentKeyId: input.contentKeyId,
|
|
437
|
-
readSigningKey: input.readSigningKey,
|
|
438
|
-
hostPresenceVerifier: input.hostPresenceVerifier,
|
|
439
|
-
features: {
|
|
440
|
-
...DEFAULT_CODE_COLLAB_FEATURES_V1,
|
|
441
|
-
...input.features
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
const secretBase = input.expiresAt === void 0 ? base : {
|
|
445
|
-
...base,
|
|
446
|
-
expiresAt: input.expiresAt
|
|
447
|
-
};
|
|
448
|
-
if (input.role === "read") {
|
|
449
|
-
return secretBase;
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
...secretBase,
|
|
453
|
-
writeSigningKey: input.writeSigningKey
|
|
454
|
-
};
|
|
455
|
-
};
|
|
456
|
-
createKeyId = function(prefix) {
|
|
457
|
-
return `${prefix}-${base64urlEncode(randomBytes(8))}`;
|
|
458
|
-
};
|
|
459
|
-
async function generateSessionSigningKey(input) {
|
|
460
|
-
if (input.alg) {
|
|
461
|
-
return generateSigningPrivateKey({
|
|
462
|
-
alg: input.alg,
|
|
463
|
-
keyId: input.keyId
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
return generatePreferredSigningPrivateKey({
|
|
467
|
-
keyId: input.keyId
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
const UTC_RFC3339_MILLIS_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;
|
|
471
|
-
function parseUtcRfc3339Millis(value, label, code) {
|
|
472
|
-
if (!UTC_RFC3339_MILLIS_RE.test(value)) {
|
|
473
|
-
throw new CodeSessionError(code, `${label} must be a valid UTC RFC3339 timestamp`);
|
|
474
|
-
}
|
|
475
|
-
const ms = Date.parse(value);
|
|
476
|
-
if (!Number.isFinite(ms) || new Date(ms).toISOString() !== value) {
|
|
477
|
-
throw new CodeSessionError(code, `${label} must be a valid UTC RFC3339 timestamp`);
|
|
478
|
-
}
|
|
479
|
-
return ms;
|
|
480
|
-
}
|
|
481
|
-
REQUIRED_CORE_CODE_COLLAB_FEATURES_V1 = Object.keys(CORE_CODE_COLLAB_FEATURES_V1);
|
|
482
|
-
createInitialSessionStateV1 = function(input) {
|
|
483
|
-
const readVerifier = verifierFromPrivateKey(input.secrets.readSigningKey);
|
|
484
|
-
if (!input.secrets.writeSigningKey) {
|
|
485
|
-
throw new CodeSessionError("invalid_share_secret", "Host session secret material is missing write signing key");
|
|
486
|
-
}
|
|
487
|
-
const writeVerifier = verifierFromPrivateKey(input.secrets.writeSigningKey);
|
|
488
|
-
const hostPresenceVerifier = input.secrets.hostPresenceVerifier;
|
|
489
|
-
const invite = {
|
|
490
|
-
inviteId: input.inviteId,
|
|
491
|
-
role: input.inviteRole,
|
|
492
|
-
readVerifierKeyId: readVerifier.keyId,
|
|
493
|
-
...input.expiresAt === void 0 ? {} : {
|
|
494
|
-
expiresAt: input.expiresAt
|
|
495
|
-
},
|
|
496
|
-
...input.inviteRole === "write" ? {
|
|
497
|
-
writeVerifierKeyId: writeVerifier.keyId
|
|
498
|
-
} : {}
|
|
499
|
-
};
|
|
500
|
-
return {
|
|
501
|
-
protocolVersion: 1,
|
|
502
|
-
mode: "hosted-code-session",
|
|
503
|
-
spaceUuid: input.spaceUuid,
|
|
504
|
-
createdAt: input.createdAt,
|
|
505
|
-
...input.expiresAt === void 0 ? {} : {
|
|
506
|
-
expiresAt: input.expiresAt
|
|
507
|
-
},
|
|
508
|
-
...input.hostLivenessTimeoutMs === void 0 ? {} : {
|
|
509
|
-
hostLivenessTimeoutMs: input.hostLivenessTimeoutMs
|
|
510
|
-
},
|
|
511
|
-
host: {
|
|
512
|
-
peerId: input.hostPeerId,
|
|
513
|
-
hostEpoch: input.hostEpoch,
|
|
514
|
-
presenceVerifierKeyId: hostPresenceVerifier.keyId
|
|
515
|
-
},
|
|
516
|
-
crypto: {
|
|
517
|
-
contentKeyId: input.secrets.contentKeyId,
|
|
518
|
-
readVerifierKeyId: readVerifier.keyId,
|
|
519
|
-
writeVerifierKeyId: writeVerifier.keyId,
|
|
520
|
-
hostPresenceVerifierKeyId: hostPresenceVerifier.keyId,
|
|
521
|
-
verifierKeys: {
|
|
522
|
-
[readVerifier.keyId]: toSessionVerifierKey(readVerifier, "read"),
|
|
523
|
-
[writeVerifier.keyId]: toSessionVerifierKey(writeVerifier, "write"),
|
|
524
|
-
[hostPresenceVerifier.keyId]: toSessionVerifierKey(hostPresenceVerifier, "host-presence")
|
|
525
|
-
}
|
|
526
|
-
},
|
|
527
|
-
invites: {
|
|
528
|
-
[input.inviteId]: invite
|
|
529
|
-
},
|
|
530
|
-
participants: {
|
|
531
|
-
[input.hostPeerId]: {
|
|
532
|
-
peerId: input.hostPeerId,
|
|
533
|
-
role: "host",
|
|
534
|
-
joinedAt: input.createdAt,
|
|
535
|
-
capabilities: [
|
|
536
|
-
"host",
|
|
537
|
-
"hydrate_text",
|
|
538
|
-
"hydrate_blob",
|
|
539
|
-
"lsp_definition",
|
|
540
|
-
"lsp_references",
|
|
541
|
-
"save_text",
|
|
542
|
-
"resolve_save_conflict"
|
|
543
|
-
]
|
|
544
|
-
}
|
|
545
|
-
},
|
|
546
|
-
features: {
|
|
547
|
-
...DEFAULT_CODE_COLLAB_FEATURES_V1,
|
|
548
|
-
...input.features
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
};
|
|
552
|
-
assertCompatibleSessionStateV1 = function(state, requiredFeaturesOrOptions = REQUIRED_CORE_CODE_COLLAB_FEATURES_V1) {
|
|
553
|
-
assertSessionStateShape(state);
|
|
554
|
-
const options = isReadonlyStringArray(requiredFeaturesOrOptions) ? {
|
|
555
|
-
requiredFeatures: requiredFeaturesOrOptions
|
|
556
|
-
} : requiredFeaturesOrOptions;
|
|
557
|
-
const requiredFeatures = options.requiredFeatures ?? REQUIRED_CORE_CODE_COLLAB_FEATURES_V1;
|
|
558
|
-
if (state.protocolVersion !== 1) {
|
|
559
|
-
throw new CodeSessionError("unsupported_protocol_version", `Unsupported code collaboration protocol version: ${String(state.protocolVersion)}`);
|
|
560
|
-
}
|
|
561
|
-
if (state.mode !== "hosted-code-session") {
|
|
562
|
-
throw new CodeSessionError("unsupported_session_mode", `Unsupported code collaboration session mode: ${String(state.mode)}`);
|
|
563
|
-
}
|
|
564
|
-
parseRfc3339TimestampMs(state.createdAt, "createdAt");
|
|
565
|
-
if (state.expiresAt !== void 0) {
|
|
566
|
-
parseRfc3339TimestampMs(state.expiresAt, "expiresAt");
|
|
567
|
-
}
|
|
568
|
-
if (state.hostLivenessTimeoutMs !== void 0 && (!Number.isSafeInteger(state.hostLivenessTimeoutMs) || state.hostLivenessTimeoutMs < 0)) {
|
|
569
|
-
throw new CodeSessionError("invalid_session_state", "hostLivenessTimeoutMs must be a non-negative safe integer");
|
|
570
|
-
}
|
|
571
|
-
validateSpaceUuid(state.spaceUuid);
|
|
572
|
-
if (options.spaceUuid !== void 0 && state.spaceUuid.toLowerCase() !== options.spaceUuid.toLowerCase()) {
|
|
573
|
-
throw new CodeSessionError("session_route_mismatch", "Session state spaceUuid does not match share link route");
|
|
574
|
-
}
|
|
575
|
-
validateMailboxSegment(state.host.peerId, "host.peerId");
|
|
576
|
-
validateMailboxSegment(state.host.hostEpoch, "host.hostEpoch");
|
|
577
|
-
assertSessionVerifierKey(state, state.crypto.readVerifierKeyId, "read", "readVerifierKeyId");
|
|
578
|
-
assertSessionVerifierKey(state, state.crypto.writeVerifierKeyId, "write", "writeVerifierKeyId");
|
|
579
|
-
assertSessionVerifierKey(state, state.crypto.hostPresenceVerifierKeyId, "host-presence", "hostPresenceVerifierKeyId");
|
|
580
|
-
assertSessionVerifierKey(state, state.host.presenceVerifierKeyId, "host-presence", "host.presenceVerifierKeyId");
|
|
581
|
-
validateInvites(state);
|
|
582
|
-
validateParticipants(state);
|
|
583
|
-
validateSessionFeatures(state.features);
|
|
584
|
-
for (const feature of requiredFeatures) {
|
|
585
|
-
if (!state.features[feature]) {
|
|
586
|
-
throw new CodeSessionError("missing_required_feature", `Session is missing required feature: ${feature}`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
function assertSessionVerifierKey(state, keyId, purpose, label) {
|
|
591
|
-
if (!keyId || typeof keyId !== "string") {
|
|
592
|
-
throw new CodeSessionError("invalid_session_state", `${label} must be a non-empty string`);
|
|
593
|
-
}
|
|
594
|
-
const verifier = state.crypto.verifierKeys[keyId];
|
|
595
|
-
assertRecord(verifier, `verifier key ${keyId}`);
|
|
596
|
-
if (verifier.alg !== "ed25519" && verifier.alg !== "p256") {
|
|
597
|
-
throw new CodeSessionError("invalid_session_state", `verifier key ${keyId} has unsupported alg`);
|
|
598
|
-
}
|
|
599
|
-
if (verifier.purpose !== purpose) {
|
|
600
|
-
throw new CodeSessionError("invalid_session_state", `verifier key ${keyId} purpose mismatch`);
|
|
601
|
-
}
|
|
602
|
-
if (!verifier.publicKey || typeof verifier.publicKey !== "string") {
|
|
603
|
-
throw new CodeSessionError("invalid_session_state", `verifier key ${keyId} publicKey must be a string`);
|
|
604
|
-
}
|
|
605
|
-
if ("privateKey" in verifier) {
|
|
606
|
-
throw new CodeSessionError("invalid_session_state", `verifier key ${keyId} must not include privateKey`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
function validateInvites(state) {
|
|
610
|
-
for (const [inviteId, invite] of Object.entries(state.invites)) {
|
|
611
|
-
assertRecord(invite, `invite ${inviteId}`);
|
|
612
|
-
if (invite.inviteId !== inviteId || !invite.inviteId || typeof invite.inviteId !== "string") {
|
|
613
|
-
throw new CodeSessionError("invalid_session_state", `invite ${inviteId} inviteId mismatch`);
|
|
614
|
-
}
|
|
615
|
-
if (invite.role !== "read" && invite.role !== "write") {
|
|
616
|
-
throw new CodeSessionError("invalid_session_state", `invite ${inviteId} role must be read or write`);
|
|
617
|
-
}
|
|
618
|
-
if (invite.expiresAt !== void 0) {
|
|
619
|
-
parseRfc3339TimestampMs(invite.expiresAt, `invite ${inviteId} expiresAt`);
|
|
620
|
-
}
|
|
621
|
-
assertSessionVerifierKey(state, invite.readVerifierKeyId, "read", `invite ${inviteId} readVerifierKeyId`);
|
|
622
|
-
if (invite.role === "read") {
|
|
623
|
-
if (invite.writeVerifierKeyId !== void 0) {
|
|
624
|
-
throw new CodeSessionError("invalid_session_state", `invite ${inviteId} read role must not include writeVerifierKeyId`);
|
|
625
|
-
}
|
|
626
|
-
} else {
|
|
627
|
-
if (typeof invite.writeVerifierKeyId !== "string") {
|
|
628
|
-
throw new CodeSessionError("invalid_session_state", `invite ${inviteId} write role must include writeVerifierKeyId`);
|
|
629
|
-
}
|
|
630
|
-
assertSessionVerifierKey(state, invite.writeVerifierKeyId, "write", `invite ${inviteId} writeVerifierKeyId`);
|
|
631
|
-
}
|
|
632
|
-
if (invite.maxUses !== void 0 && (!Number.isSafeInteger(invite.maxUses) || invite.maxUses < 0)) {
|
|
633
|
-
throw new CodeSessionError("invalid_session_state", `invite ${inviteId} maxUses must be non-negative`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
function validateParticipants(state) {
|
|
638
|
-
if (!state.participants) return;
|
|
639
|
-
for (const [peerId, participant] of Object.entries(state.participants)) {
|
|
640
|
-
assertRecord(participant, `participant ${peerId}`);
|
|
641
|
-
if (participant.peerId !== peerId) {
|
|
642
|
-
throw new CodeSessionError("invalid_session_state", `participant ${peerId} peerId mismatch`);
|
|
643
|
-
}
|
|
644
|
-
validateMailboxSegment(participant.peerId, `participant ${peerId}.peerId`);
|
|
645
|
-
if (participant.role !== "host" && participant.role !== "write" && participant.role !== "read") {
|
|
646
|
-
throw new CodeSessionError("invalid_session_state", `participant ${peerId} role is invalid`);
|
|
647
|
-
}
|
|
648
|
-
if (participant.joinedAt !== void 0) {
|
|
649
|
-
parseRfc3339TimestampMs(participant.joinedAt, `participant ${peerId} joinedAt`);
|
|
650
|
-
}
|
|
651
|
-
if (participant.capabilities !== void 0 && (!Array.isArray(participant.capabilities) || participant.capabilities.some((capability) => typeof capability !== "string" || capability.length === 0))) {
|
|
652
|
-
throw new CodeSessionError("invalid_session_state", `participant ${peerId} capabilities are invalid`);
|
|
653
|
-
}
|
|
654
|
-
if (participant.inviteId !== void 0 && typeof participant.inviteId !== "string") {
|
|
655
|
-
throw new CodeSessionError("invalid_session_state", `participant ${peerId} inviteId must be a string`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
function validateSessionFeatures(features) {
|
|
660
|
-
for (const [feature, enabled] of Object.entries(features)) {
|
|
661
|
-
if (!feature || typeof enabled !== "boolean") {
|
|
662
|
-
throw new CodeSessionError("invalid_session_state", "features must map non-empty names to booleans");
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
function assertSessionStateShape(state) {
|
|
667
|
-
assertRecord(state, "session state");
|
|
668
|
-
assertRecord(state.host, "session host");
|
|
669
|
-
assertRecord(state.crypto, "session crypto");
|
|
670
|
-
assertRecord(state.crypto.verifierKeys, "session verifier keys");
|
|
671
|
-
assertRecord(state.invites, "session invites");
|
|
672
|
-
assertRecord(state.features, "session features");
|
|
673
|
-
if (state.participants !== void 0) {
|
|
674
|
-
assertRecord(state.participants, "session participants");
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
function assertRecord(value, label) {
|
|
678
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
679
|
-
throw new CodeSessionError("invalid_session_state", `${label} must be an object`);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
assertShareLinkMatchesSessionStateV1 = function(state, secret, options = {}) {
|
|
683
|
-
if (options.routeInviteId !== void 0 && options.routeInviteId !== secret.inviteId) {
|
|
684
|
-
throw new CodeSessionError("session_invite_mismatch", "Share link route inviteId does not match share secret");
|
|
685
|
-
}
|
|
686
|
-
if (state.crypto.contentKeyId !== secret.contentKeyId) {
|
|
687
|
-
throw new CodeSessionError("session_invite_mismatch", "Share secret contentKeyId does not match session state");
|
|
688
|
-
}
|
|
689
|
-
for (const [feature, enabled] of Object.entries(secret.features)) {
|
|
690
|
-
if (state.features[feature] !== enabled) {
|
|
691
|
-
throw new CodeSessionError("session_invite_mismatch", `Share feature ${feature} does not match session state`);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
assertNotExpired(state.expiresAt, "session", options);
|
|
695
|
-
assertNotExpired(secret.expiresAt, "share secret", options);
|
|
696
|
-
const invite = state.invites[secret.inviteId];
|
|
697
|
-
if (!invite || invite.inviteId !== secret.inviteId) {
|
|
698
|
-
throw new CodeSessionError("session_invite_mismatch", "Share invite is not present in session state");
|
|
699
|
-
}
|
|
700
|
-
assertNotExpired(invite.expiresAt, "share invite", options);
|
|
701
|
-
if (invite.role !== secret.role) {
|
|
702
|
-
throw new CodeSessionError("session_invite_mismatch", "Share invite role does not match session state");
|
|
703
|
-
}
|
|
704
|
-
if (invite.readVerifierKeyId !== secret.readSigningKey.keyId) {
|
|
705
|
-
throw new CodeSessionError("session_invite_mismatch", "Share read signing key does not match session invite");
|
|
706
|
-
}
|
|
707
|
-
assertPrivateKeyMatchesSessionVerifier(state, secret.readSigningKey, "read", "Share read signing key");
|
|
708
|
-
if (secret.role === "read") {
|
|
709
|
-
if (invite.writeVerifierKeyId !== void 0) {
|
|
710
|
-
throw new CodeSessionError("session_invite_mismatch", "Read-only share invite must not advertise a write verifier");
|
|
711
|
-
}
|
|
712
|
-
} else {
|
|
713
|
-
const writeSigningKey = secret.writeSigningKey;
|
|
714
|
-
if (!writeSigningKey || invite.writeVerifierKeyId !== writeSigningKey.keyId) {
|
|
715
|
-
throw new CodeSessionError("session_invite_mismatch", "Share write signing key does not match session invite");
|
|
716
|
-
}
|
|
717
|
-
assertPrivateKeyMatchesSessionVerifier(state, writeSigningKey, "write", "Share write signing key");
|
|
718
|
-
}
|
|
719
|
-
if (state.host.presenceVerifierKeyId !== state.crypto.hostPresenceVerifierKeyId || secret.hostPresenceVerifier.keyId !== state.crypto.hostPresenceVerifierKeyId) {
|
|
720
|
-
throw new CodeSessionError("session_invite_mismatch", "Host presence verifier does not match session state");
|
|
721
|
-
}
|
|
722
|
-
assertVerifierMatchesSessionVerifier(state, secret.hostPresenceVerifier, "host-presence", "Host presence verifier");
|
|
723
|
-
};
|
|
724
|
-
assertSessionNotExpiredV1 = function(state, options = {}) {
|
|
725
|
-
assertNotExpired(state.expiresAt, "session", options);
|
|
726
|
-
};
|
|
727
|
-
function isReadonlyStringArray(value) {
|
|
728
|
-
return Array.isArray(value);
|
|
729
|
-
}
|
|
730
|
-
function toSessionVerifierKey(verifier, purpose) {
|
|
731
|
-
return {
|
|
732
|
-
alg: verifier.alg,
|
|
733
|
-
purpose,
|
|
734
|
-
publicKey: verifier.publicKey
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
function assertPrivateKeyMatchesSessionVerifier(state, key, purpose, label) {
|
|
738
|
-
assertVerifierMatchesSessionVerifier(state, verifierFromPrivateKey(key), purpose, label);
|
|
739
|
-
}
|
|
740
|
-
function assertVerifierMatchesSessionVerifier(state, verifier, purpose, label) {
|
|
741
|
-
const sessionVerifier = state.crypto.verifierKeys[verifier.keyId];
|
|
742
|
-
if (!sessionVerifier) {
|
|
743
|
-
throw new CodeSessionError("session_invite_mismatch", `${label} is not present in session verifier keys`);
|
|
744
|
-
}
|
|
745
|
-
if (sessionVerifier.purpose !== purpose || sessionVerifier.alg !== verifier.alg || sessionVerifier.publicKey !== verifier.publicKey) {
|
|
746
|
-
throw new CodeSessionError("session_invite_mismatch", `${label} does not match session verifier keys`);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
function assertNotExpired(expiresAt, label, options) {
|
|
750
|
-
if (expiresAt === void 0 || options.nowMs === void 0) return;
|
|
751
|
-
const expiresAtMs = parseRfc3339TimestampMs(expiresAt, `${label} expiresAt`);
|
|
752
|
-
const skewMs = options.maxClockSkewMs ?? 0;
|
|
753
|
-
if (options.nowMs > expiresAtMs + skewMs) {
|
|
754
|
-
throw new CodeSessionError("session_expired", `${label} has expired`);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
function parseRfc3339TimestampMs(value, label) {
|
|
758
|
-
return parseUtcRfc3339Millis(value, label, "invalid_session_state");
|
|
759
|
-
}
|
|
760
|
-
serializeShareLinkV1 = function(input) {
|
|
761
|
-
validateBucketId(input.bucketId);
|
|
762
|
-
validateSpaceUuid(input.spaceUuid);
|
|
763
|
-
validateShareServerUrl(input.serverUrl);
|
|
764
|
-
if (input.blobServerUrl !== void 0) {
|
|
765
|
-
validateShareServerUrl(input.blobServerUrl);
|
|
766
|
-
}
|
|
767
|
-
if (input.blobPathPrefix !== void 0) {
|
|
768
|
-
validateBlobPathPrefix(input.blobPathPrefix);
|
|
769
|
-
}
|
|
770
|
-
validateShareLinkSecretV1(input.secret);
|
|
771
|
-
const spaceUuid = input.spaceUuid.toLowerCase();
|
|
772
|
-
const streamPrefix = input.streamPrefix ?? buildStreamPrefix(spaceUuid);
|
|
773
|
-
validateStreamPrefix(streamPrefix);
|
|
774
|
-
if (input.inviteId !== input.secret.inviteId) {
|
|
775
|
-
throw new CodeSessionError("invalid_share_link", "inviteId must match share secret");
|
|
776
|
-
}
|
|
777
|
-
const url = new URL("loro://join");
|
|
778
|
-
url.searchParams.set("v", "1");
|
|
779
|
-
url.searchParams.set("server", input.serverUrl);
|
|
780
|
-
url.searchParams.set("bucket", input.bucketId);
|
|
781
|
-
url.searchParams.set("space", spaceUuid);
|
|
782
|
-
url.searchParams.set("prefix", streamPrefix);
|
|
783
|
-
if (input.blobServerUrl !== void 0) {
|
|
784
|
-
url.searchParams.set("blobServer", input.blobServerUrl);
|
|
785
|
-
}
|
|
786
|
-
if (input.blobPathPrefix !== void 0) {
|
|
787
|
-
url.searchParams.set("blobPrefix", input.blobPathPrefix);
|
|
788
|
-
}
|
|
789
|
-
url.searchParams.set("invite", input.inviteId);
|
|
790
|
-
url.hash = `keys=${base64urlEncode(utf8Encode(canonicalJson(input.secret)))}`;
|
|
791
|
-
return url.toString();
|
|
792
|
-
};
|
|
793
|
-
parseShareLinkV1 = function(link) {
|
|
794
|
-
let url;
|
|
795
|
-
try {
|
|
796
|
-
url = new URL(link);
|
|
797
|
-
} catch {
|
|
798
|
-
throw new CodeSessionError("invalid_share_link", "Share link must be a valid loro://join URL");
|
|
799
|
-
}
|
|
800
|
-
if (url.protocol !== "loro:" || url.hostname !== "join") {
|
|
801
|
-
throw new CodeSessionError("invalid_share_link", "Share link must use loro://join");
|
|
802
|
-
}
|
|
803
|
-
if (url.searchParams.get("v") !== "1") {
|
|
804
|
-
throw new CodeSessionError("invalid_share_link", "Share link version must be 1");
|
|
805
|
-
}
|
|
806
|
-
const serverUrl = requiredParam(url, "server");
|
|
807
|
-
validateShareServerUrl(serverUrl);
|
|
808
|
-
const blobServerUrl = optionalParam(url, "blobServer");
|
|
809
|
-
if (blobServerUrl !== void 0) {
|
|
810
|
-
validateShareServerUrl(blobServerUrl);
|
|
811
|
-
}
|
|
812
|
-
const blobPathPrefix = optionalParam(url, "blobPrefix");
|
|
813
|
-
if (blobPathPrefix !== void 0) {
|
|
814
|
-
validateBlobPathPrefix(blobPathPrefix);
|
|
815
|
-
}
|
|
816
|
-
const bucketId = requiredParam(url, "bucket");
|
|
817
|
-
const spaceUuid = requiredParam(url, "space").toLowerCase();
|
|
818
|
-
const streamPrefix = requiredParam(url, "prefix");
|
|
819
|
-
const inviteId = requiredParam(url, "invite");
|
|
820
|
-
validateBucketId(bucketId);
|
|
821
|
-
validateSpaceUuid(spaceUuid);
|
|
822
|
-
validateStreamPrefix(streamPrefix);
|
|
823
|
-
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
824
|
-
const fragment = new URLSearchParams(hash);
|
|
825
|
-
const keys = fragment.get("keys");
|
|
826
|
-
if (!keys) {
|
|
827
|
-
throw new CodeSessionError("invalid_share_link", "Share link fragment is missing keys");
|
|
828
|
-
}
|
|
829
|
-
let secret;
|
|
830
|
-
try {
|
|
831
|
-
secret = JSON.parse(utf8Decode(base64urlDecode(keys)));
|
|
832
|
-
} catch {
|
|
833
|
-
throw new CodeSessionError("invalid_share_link", "Share link fragment contains invalid keys");
|
|
834
|
-
}
|
|
835
|
-
validateShareLinkSecretV1(secret);
|
|
836
|
-
if (secret.inviteId !== inviteId) {
|
|
837
|
-
throw new CodeSessionError("invalid_share_link", "inviteId must match share secret");
|
|
838
|
-
}
|
|
839
|
-
return {
|
|
840
|
-
route: {
|
|
841
|
-
v: 1,
|
|
842
|
-
serverUrl,
|
|
843
|
-
...blobServerUrl === void 0 ? {} : {
|
|
844
|
-
blobServerUrl
|
|
845
|
-
},
|
|
846
|
-
...blobPathPrefix === void 0 ? {} : {
|
|
847
|
-
blobPathPrefix
|
|
848
|
-
},
|
|
849
|
-
bucketId,
|
|
850
|
-
spaceUuid,
|
|
851
|
-
streamPrefix,
|
|
852
|
-
inviteId
|
|
853
|
-
},
|
|
854
|
-
secret
|
|
855
|
-
};
|
|
856
|
-
};
|
|
857
|
-
validateShareLinkSecretV1 = function(secret) {
|
|
858
|
-
if (!secret || typeof secret !== "object" || Array.isArray(secret)) {
|
|
859
|
-
throw new CodeSessionError("invalid_share_secret", "Share secret must be an object");
|
|
860
|
-
}
|
|
861
|
-
if (secret.v !== 1 || secret.kind !== "loro-code-collab/share-secret") {
|
|
862
|
-
throw new CodeSessionError("invalid_share_secret", "Unsupported share secret");
|
|
863
|
-
}
|
|
864
|
-
if (secret.role !== "read" && secret.role !== "write") {
|
|
865
|
-
throw new CodeSessionError("invalid_share_secret", "Share role must be read or write");
|
|
866
|
-
}
|
|
867
|
-
if (!secret.inviteId || !secret.contentKey || !secret.contentKeyId) {
|
|
868
|
-
throw new CodeSessionError("invalid_share_secret", "Share secret is missing required fields");
|
|
869
|
-
}
|
|
870
|
-
if (secret.expiresAt !== void 0) {
|
|
871
|
-
validateRfc3339Timestamp(secret.expiresAt, "expiresAt");
|
|
872
|
-
}
|
|
873
|
-
validateContentKey(secret.contentKey);
|
|
874
|
-
validateFeatureFlags(secret.features);
|
|
875
|
-
validateSerializedSigningPrivateKey(secret.readSigningKey);
|
|
876
|
-
validateSerializedVerifierKey(secret.hostPresenceVerifier);
|
|
877
|
-
if (secret.role === "read" && secret.writeSigningKey !== void 0) {
|
|
878
|
-
throw new CodeSessionError("invalid_share_secret", "Read share links must not include writeSigningKey");
|
|
879
|
-
}
|
|
880
|
-
if (secret.role === "write") {
|
|
881
|
-
validateSerializedSigningPrivateKey(secret.writeSigningKey);
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
function validateContentKey(contentKey) {
|
|
885
|
-
let decoded;
|
|
886
|
-
try {
|
|
887
|
-
decoded = base64urlDecode(contentKey);
|
|
888
|
-
} catch {
|
|
889
|
-
throw new CodeSessionError("invalid_share_secret", "contentKey must be unpadded base64url");
|
|
890
|
-
}
|
|
891
|
-
if (decoded.byteLength !== 32) {
|
|
892
|
-
throw new CodeSessionError("invalid_share_secret", "contentKey must decode to 32 bytes");
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
function validateFeatureFlags(features) {
|
|
896
|
-
if (!features || typeof features !== "object" || Array.isArray(features)) {
|
|
897
|
-
throw new CodeSessionError("invalid_share_secret", "features must be an object");
|
|
898
|
-
}
|
|
899
|
-
for (const [key, value] of Object.entries(features)) {
|
|
900
|
-
if (!key || typeof value !== "boolean") {
|
|
901
|
-
throw new CodeSessionError("invalid_share_secret", "features must map feature names to booleans");
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
validateShareServerUrl = function(serverUrl) {
|
|
906
|
-
let parsed;
|
|
907
|
-
try {
|
|
908
|
-
parsed = new URL(serverUrl);
|
|
909
|
-
} catch {
|
|
910
|
-
throw new CodeSessionError("invalid_share_link", "Share link server must be a valid URL");
|
|
911
|
-
}
|
|
912
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
913
|
-
throw new CodeSessionError("invalid_share_link", "Share link server must use http or https");
|
|
914
|
-
}
|
|
915
|
-
if (parsed.username || parsed.password) {
|
|
916
|
-
throw new CodeSessionError("invalid_share_link", "Share link server must not include credentials");
|
|
917
|
-
}
|
|
918
|
-
if (parsed.search || parsed.hash) {
|
|
919
|
-
throw new CodeSessionError("invalid_share_link", "Share link server must not include query or fragment");
|
|
920
|
-
}
|
|
921
|
-
if (parsed.pathname !== "/" && parsed.pathname !== "") {
|
|
922
|
-
throw new CodeSessionError("invalid_share_link", "Share link server must not include a path");
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
function requiredParam(url, key) {
|
|
926
|
-
const value = url.searchParams.get(key);
|
|
927
|
-
if (!value) {
|
|
928
|
-
throw new CodeSessionError("invalid_share_link", `Share link is missing ${key}`);
|
|
929
|
-
}
|
|
930
|
-
return value;
|
|
931
|
-
}
|
|
932
|
-
function optionalParam(url, key) {
|
|
933
|
-
const value = url.searchParams.get(key);
|
|
934
|
-
return value == null || value.length === 0 ? void 0 : value;
|
|
935
|
-
}
|
|
936
|
-
function validateRfc3339Timestamp(value, label) {
|
|
937
|
-
parseUtcRfc3339Millis(value, label, "invalid_share_secret");
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
export {
|
|
941
|
-
serializeShareLinkV1 as A,
|
|
942
|
-
unwrapEncryptedEphemeralStateEnvelopeV1 as B,
|
|
943
|
-
validateShareLinkSecretV1 as C,
|
|
944
|
-
validateShareServerUrl as D,
|
|
945
|
-
verifyHostPresenceV1 as E,
|
|
946
|
-
REQUIRED_CORE_CODE_COLLAB_FEATURES_V1 as R,
|
|
947
|
-
__tla,
|
|
948
|
-
assertCompatibleSessionStateV1 as a,
|
|
949
|
-
assertSessionNotExpiredV1 as b,
|
|
950
|
-
assertShareLinkMatchesSessionStateV1 as c,
|
|
951
|
-
buildBlobEnvelopeAadV1 as d,
|
|
952
|
-
buildEphemeralEnvelopeAadV1 as e,
|
|
953
|
-
computeEphemeralStateKeyV1 as f,
|
|
954
|
-
createCursorStateV1 as g,
|
|
955
|
-
createEncryptedEphemeralStateEnvelopeV1 as h,
|
|
956
|
-
createHostPresenceV1 as i,
|
|
957
|
-
createInitialSessionStateV1 as j,
|
|
958
|
-
createKeyId as k,
|
|
959
|
-
createParticipantPresenceV1 as l,
|
|
960
|
-
createSelectionStateV1 as m,
|
|
961
|
-
createShareLinkSecretV1 as n,
|
|
962
|
-
decryptBlobObjectV1 as o,
|
|
963
|
-
decryptEphemeralPayloadV1 as p,
|
|
964
|
-
encryptBlobObjectV1 as q,
|
|
965
|
-
encryptEphemeralPayloadV1 as r,
|
|
966
|
-
generateHostSessionSecretsV1 as s,
|
|
967
|
-
parseCursorStateV1 as t,
|
|
968
|
-
parseEncryptedBlobObjectV1 as u,
|
|
969
|
-
parseHostPresenceV1 as v,
|
|
970
|
-
parseParticipantPresenceV1 as w,
|
|
971
|
-
parseSelectionStateV1 as x,
|
|
972
|
-
parseShareLinkV1 as y,
|
|
973
|
-
serializeEncryptedBlobObjectV1 as z
|
|
974
|
-
};
|