@claudian-collab/protocol 3.1.0 → 3.2.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.
@@ -0,0 +1,884 @@
1
+ import { CollabError } from './CollabError.mjs';
2
+ import { hasUtf8ByteLengthAtMost, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
3
+ export const COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES = Object.freeze([
4
+ 'collecting-readiness',
5
+ 'source-quiesced',
6
+ 'checkpoint-received',
7
+ 'checkpoint-validated',
8
+ 'claims-retained',
9
+ 'repository-published',
10
+ 'source-relinquished',
11
+ 'cloud-activated',
12
+ 'completed',
13
+ ]);
14
+ export const COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES = Object.freeze([
15
+ 'collecting-readiness',
16
+ 'cloud-quiesced',
17
+ 'checkpoint-captured',
18
+ 'target-staged',
19
+ 'claims-retained',
20
+ 'cloud-relinquished',
21
+ 'lan-activated',
22
+ 'completed',
23
+ ]);
24
+ export const COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES = Object.freeze([
25
+ 'cancel-intent',
26
+ 'target-invalidated',
27
+ 'target-cleaned',
28
+ 'source-reopened',
29
+ 'cancelled',
30
+ ]);
31
+ export const COLLAB_AUTHORITY_TRANSFER_CANCELLABLE_PHASES = Object.freeze([
32
+ 'collecting-readiness',
33
+ 'source-quiesced',
34
+ 'checkpoint-received',
35
+ 'checkpoint-validated',
36
+ 'claims-retained',
37
+ 'repository-published',
38
+ 'cloud-quiesced',
39
+ 'checkpoint-captured',
40
+ 'target-staged',
41
+ 'cancel-intent',
42
+ 'target-invalidated',
43
+ 'target-cleaned',
44
+ 'source-reopened',
45
+ ]);
46
+ export const COLLAB_AUTHORITY_TRANSFER_OPERATIONS = Object.freeze([
47
+ 'requestLanToCloudTransfer',
48
+ 'acceptLanToCloudTransferTarget',
49
+ 'beginLanToCloudTransfer',
50
+ 'getProjectAuthorityTransfer',
51
+ 'getAuthorityTransferReceiptVerifier',
52
+ 'rotateTransferredMembershipClaims',
53
+ 'acknowledgeTransferredMembershipClaimBatch',
54
+ 'getTransferredMembershipClaim',
55
+ 'claimTransferredMembership',
56
+ 'acknowledgeTransferredMembershipClaimRedemption',
57
+ 'commitLanToCloudRelinquishment',
58
+ 'beginCloudToLanTransfer',
59
+ 'acceptCloudToLanTransferTarget',
60
+ 'reportCloudToLanTargetStaged',
61
+ 'confirmCloudToLanTargetActive',
62
+ 'cancelProjectAuthorityTransfer',
63
+ ]);
64
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
65
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
66
+ const BASE64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
67
+ function invalidPayload(field) {
68
+ return new CollabError({ code: 'protocol-payload-invalid', safeContext: { field } });
69
+ }
70
+ function record(value, field) {
71
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
72
+ throw invalidPayload(field);
73
+ }
74
+ return value;
75
+ }
76
+ function exactRecord(value, field, keys) {
77
+ const source = record(value, field);
78
+ const expected = new Set(keys);
79
+ if (!keys.every(key => Object.hasOwn(source, key))
80
+ || Object.keys(source).some(key => !expected.has(key)))
81
+ throw invalidPayload(field);
82
+ return source;
83
+ }
84
+ function token(source, field, validate = isCollabOpaqueId) {
85
+ const value = source[field];
86
+ if (typeof value !== 'string' || !validate(value))
87
+ throw invalidPayload(field);
88
+ return value;
89
+ }
90
+ function boundedString(source, field, maximumBytes) {
91
+ const value = source[field];
92
+ if (typeof value !== 'string'
93
+ || value.length === 0
94
+ || !hasUtf8ByteLengthAtMost(value, maximumBytes))
95
+ throw invalidPayload(field);
96
+ return value;
97
+ }
98
+ function positiveInteger(source, field) {
99
+ const value = source[field];
100
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) {
101
+ throw invalidPayload(field);
102
+ }
103
+ return value;
104
+ }
105
+ function timestamp(source, field) {
106
+ const value = source[field];
107
+ if (typeof value !== 'string'
108
+ || value.length > 64
109
+ || Number.isNaN(Date.parse(value))
110
+ || new Date(value).toISOString() !== value)
111
+ throw invalidPayload(field);
112
+ return value;
113
+ }
114
+ function sha256(source, field) {
115
+ const value = source[field];
116
+ if (typeof value !== 'string' || !SHA256_PATTERN.test(value))
117
+ throw invalidPayload(field);
118
+ return value;
119
+ }
120
+ function base64url(source, field, maximumBytes = 4096) {
121
+ const value = boundedString(source, field, maximumBytes);
122
+ if (!BASE64URL_PATTERN.test(value))
123
+ throw invalidPayload(field);
124
+ return value;
125
+ }
126
+ function fixedBase64url(source, field, decodedBytes) {
127
+ const encodedLength = Math.ceil(decodedBytes * 4 / 3);
128
+ const value = base64url(source, field, encodedLength);
129
+ const finalIndex = BASE64URL_ALPHABET.indexOf(value[value.length - 1]);
130
+ const remainder = value.length % 4;
131
+ if (value.length !== encodedLength
132
+ || remainder === 1
133
+ || (remainder === 2 && (finalIndex & 15) !== 0)
134
+ || (remainder === 3 && (finalIndex & 3) !== 0))
135
+ throw invalidPayload(field);
136
+ return value;
137
+ }
138
+ function literal(source, field, values) {
139
+ const value = source[field];
140
+ if (typeof value !== 'string' || !values.includes(value))
141
+ throw invalidPayload(field);
142
+ return value;
143
+ }
144
+ function authority(value, field) {
145
+ const source = exactRecord(value, field, ['generation', 'kind']);
146
+ return {
147
+ generation: positiveInteger(source, 'generation'),
148
+ kind: literal(source, 'kind', ['cloud', 'lan']),
149
+ };
150
+ }
151
+ function absoluteTargetUrl(source) {
152
+ const targetUrl = boundedString(source, 'targetUrl', 2048);
153
+ let parsed;
154
+ try {
155
+ parsed = new URL(targetUrl);
156
+ }
157
+ catch {
158
+ throw invalidPayload('targetUrl');
159
+ }
160
+ if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
161
+ || parsed.username.length > 0
162
+ || parsed.password.length > 0
163
+ || targetUrl.includes('?')
164
+ || targetUrl.includes('#'))
165
+ throw invalidPayload('targetUrl');
166
+ return targetUrl;
167
+ }
168
+ export function decodeCollabAuthorityTransferProposal(value) {
169
+ const source = exactRecord(value, 'proposal', [
170
+ 'expectedSourceAuthority',
171
+ 'idempotencyKey',
172
+ 'projectId',
173
+ 'proposedByMemberId',
174
+ 'proposedAt',
175
+ 'targetAuthorityKind',
176
+ 'targetUrl',
177
+ ]);
178
+ const expectedSourceAuthority = authority(source.expectedSourceAuthority, 'expectedSourceAuthority');
179
+ const targetAuthorityKind = literal(source, 'targetAuthorityKind', ['cloud', 'lan']);
180
+ if (expectedSourceAuthority.kind === targetAuthorityKind) {
181
+ throw invalidPayload('targetAuthorityKind');
182
+ }
183
+ return {
184
+ expectedSourceAuthority,
185
+ idempotencyKey: token(source, 'idempotencyKey'),
186
+ projectId: token(source, 'projectId', isCollabProjectId),
187
+ proposedByMemberId: token(source, 'proposedByMemberId', isCollabMemberId),
188
+ proposedAt: timestamp(source, 'proposedAt'),
189
+ targetAuthorityKind,
190
+ targetUrl: absoluteTargetUrl(source),
191
+ };
192
+ }
193
+ function claimItem(value) {
194
+ const source = exactRecord(value, 'claim', ['claim', 'memberId']);
195
+ return {
196
+ claim: base64url(source, 'claim'),
197
+ memberId: token(source, 'memberId', isCollabMemberId),
198
+ };
199
+ }
200
+ export function decodeCollabTransferredMembershipClaimBatch(value) {
201
+ const source = exactRecord(value, 'claimBatch', [
202
+ 'batchRevision',
203
+ 'batchSha256',
204
+ 'checkpointSha256',
205
+ 'claims',
206
+ 'expiresAt',
207
+ 'projectId',
208
+ 'targetAuthorityGeneration',
209
+ 'transferId',
210
+ ]);
211
+ if (!Array.isArray(source.claims)) {
212
+ throw invalidPayload('claims');
213
+ }
214
+ const claims = source.claims.map(claimItem);
215
+ if (claims.some((item, index) => index > 0
216
+ && claims[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
217
+ || new Set(claims.map(item => item.claim)).size !== claims.length)
218
+ throw invalidPayload('claims');
219
+ return {
220
+ batchRevision: positiveInteger(source, 'batchRevision'),
221
+ batchSha256: sha256(source, 'batchSha256'),
222
+ checkpointSha256: sha256(source, 'checkpointSha256'),
223
+ claims: Object.freeze(claims),
224
+ expiresAt: timestamp(source, 'expiresAt'),
225
+ projectId: token(source, 'projectId', isCollabProjectId),
226
+ targetAuthorityGeneration: positiveInteger(source, 'targetAuthorityGeneration'),
227
+ transferId: token(source, 'transferId'),
228
+ };
229
+ }
230
+ export function encodeCollabTransferredMembershipClaimBatchDigestInput(batch) {
231
+ const decoded = decodeCollabTransferredMembershipClaimBatch(batch);
232
+ return JSON.stringify({
233
+ batchRevision: decoded.batchRevision,
234
+ checkpointSha256: decoded.checkpointSha256,
235
+ claims: decoded.claims,
236
+ expiresAt: decoded.expiresAt,
237
+ projectId: decoded.projectId,
238
+ targetAuthorityGeneration: decoded.targetAuthorityGeneration,
239
+ transferId: decoded.transferId,
240
+ });
241
+ }
242
+ export function decodeCollabTransferredMembershipClaimCustodyReceipt(value) {
243
+ const source = exactRecord(value, 'custodyReceipt', [
244
+ 'batchRevision',
245
+ 'batchSha256',
246
+ 'checkpointSha256',
247
+ 'committedAt',
248
+ 'custodyAuthority',
249
+ 'operationIntentId',
250
+ 'projectId',
251
+ 'receiptId',
252
+ 'submittedByMemberId',
253
+ 'targetAuthorityGeneration',
254
+ 'transferId',
255
+ ]);
256
+ const custodyAuthority = authority(source.custodyAuthority, 'custodyAuthority');
257
+ const targetAuthorityGeneration = positiveInteger(source, 'targetAuthorityGeneration');
258
+ if (targetAuthorityGeneration !== custodyAuthority.generation + 1) {
259
+ throw invalidPayload('targetAuthorityGeneration');
260
+ }
261
+ return {
262
+ batchRevision: positiveInteger(source, 'batchRevision'),
263
+ batchSha256: sha256(source, 'batchSha256'),
264
+ checkpointSha256: sha256(source, 'checkpointSha256'),
265
+ committedAt: timestamp(source, 'committedAt'),
266
+ custodyAuthority,
267
+ operationIntentId: token(source, 'operationIntentId'),
268
+ projectId: token(source, 'projectId', isCollabProjectId),
269
+ receiptId: token(source, 'receiptId'),
270
+ submittedByMemberId: token(source, 'submittedByMemberId', isCollabMemberId),
271
+ targetAuthorityGeneration,
272
+ transferId: token(source, 'transferId'),
273
+ };
274
+ }
275
+ export function decodeCollabTransferredMembershipClaim(value) {
276
+ const source = exactRecord(value, 'claim', [
277
+ 'claim',
278
+ 'expiresAt',
279
+ 'memberId',
280
+ 'projectId',
281
+ 'targetAuthorityGeneration',
282
+ 'transferId',
283
+ ]);
284
+ return {
285
+ claim: base64url(source, 'claim'),
286
+ expiresAt: timestamp(source, 'expiresAt'),
287
+ memberId: token(source, 'memberId', isCollabMemberId),
288
+ projectId: token(source, 'projectId', isCollabProjectId),
289
+ targetAuthorityGeneration: positiveInteger(source, 'targetAuthorityGeneration'),
290
+ transferId: token(source, 'transferId'),
291
+ };
292
+ }
293
+ const REDEMPTION_RECEIPT_SIGNING_PAYLOAD_KEYS = [
294
+ 'checkpointSha256',
295
+ 'claimSha256',
296
+ 'memberId',
297
+ 'operationIntentId',
298
+ 'projectId',
299
+ 'receiptId',
300
+ 'receiptKeyId',
301
+ 'redeemedAt',
302
+ 'signatureAlgorithm',
303
+ 'targetAuthorityGeneration',
304
+ 'transferId',
305
+ ];
306
+ function redemptionReceiptSigningPayload(source) {
307
+ return {
308
+ checkpointSha256: sha256(source, 'checkpointSha256'),
309
+ claimSha256: sha256(source, 'claimSha256'),
310
+ memberId: token(source, 'memberId', isCollabMemberId),
311
+ operationIntentId: token(source, 'operationIntentId'),
312
+ projectId: token(source, 'projectId', isCollabProjectId),
313
+ receiptId: token(source, 'receiptId'),
314
+ receiptKeyId: boundedString(source, 'receiptKeyId', 256),
315
+ redeemedAt: timestamp(source, 'redeemedAt'),
316
+ signatureAlgorithm: literal(source, 'signatureAlgorithm', ['ed25519']),
317
+ targetAuthorityGeneration: positiveInteger(source, 'targetAuthorityGeneration'),
318
+ transferId: token(source, 'transferId'),
319
+ };
320
+ }
321
+ export function encodeCollabTransferredMembershipRedemptionReceiptSigningInput(payload) {
322
+ const source = exactRecord(payload, 'redemptionReceiptSigningPayload', REDEMPTION_RECEIPT_SIGNING_PAYLOAD_KEYS);
323
+ return JSON.stringify({
324
+ domain: 'claudian-collab.transferred-membership-redemption-receipt.v1',
325
+ payload: redemptionReceiptSigningPayload(source),
326
+ });
327
+ }
328
+ export function decodeCollabTransferredMembershipRedemptionReceipt(value) {
329
+ const source = exactRecord(value, 'redemptionReceipt', [
330
+ ...REDEMPTION_RECEIPT_SIGNING_PAYLOAD_KEYS,
331
+ 'signature',
332
+ ]);
333
+ const payload = redemptionReceiptSigningPayload(source);
334
+ return {
335
+ checkpointSha256: payload.checkpointSha256,
336
+ claimSha256: payload.claimSha256,
337
+ memberId: payload.memberId,
338
+ operationIntentId: payload.operationIntentId,
339
+ projectId: payload.projectId,
340
+ receiptId: payload.receiptId,
341
+ receiptKeyId: payload.receiptKeyId,
342
+ redeemedAt: payload.redeemedAt,
343
+ signature: fixedBase64url(source, 'signature', 64),
344
+ signatureAlgorithm: payload.signatureAlgorithm,
345
+ targetAuthorityGeneration: payload.targetAuthorityGeneration,
346
+ transferId: payload.transferId,
347
+ };
348
+ }
349
+ const RELINQUISHMENT_PROOF_SIGNING_PAYLOAD_KEYS = [
350
+ 'batchRevision',
351
+ 'batchSha256',
352
+ 'certificateAlgorithm',
353
+ 'checkpointSha256',
354
+ 'committedAt',
355
+ 'operationIntentId',
356
+ 'projectId',
357
+ 'sourceAuthority',
358
+ 'sourceHostMemberId',
359
+ 'targetAuthority',
360
+ 'transferId',
361
+ ];
362
+ function authorityRelinquishmentProofSigningPayload(source) {
363
+ const sourceAuthority = authority(source.sourceAuthority, 'sourceAuthority');
364
+ const targetAuthority = authority(source.targetAuthority, 'targetAuthority');
365
+ const sourceHostMemberId = source.sourceHostMemberId === null
366
+ ? null
367
+ : token(source, 'sourceHostMemberId', isCollabMemberId);
368
+ if (sourceAuthority.kind === targetAuthority.kind
369
+ || targetAuthority.generation !== sourceAuthority.generation + 1
370
+ || (sourceAuthority.kind === 'lan') !== (sourceHostMemberId !== null))
371
+ throw invalidPayload('targetAuthority');
372
+ return {
373
+ batchRevision: positiveInteger(source, 'batchRevision'),
374
+ batchSha256: sha256(source, 'batchSha256'),
375
+ certificateAlgorithm: literal(source, 'certificateAlgorithm', ['ed25519']),
376
+ checkpointSha256: sha256(source, 'checkpointSha256'),
377
+ committedAt: timestamp(source, 'committedAt'),
378
+ operationIntentId: token(source, 'operationIntentId'),
379
+ projectId: token(source, 'projectId', isCollabProjectId),
380
+ sourceAuthority,
381
+ sourceHostMemberId,
382
+ targetAuthority,
383
+ transferId: token(source, 'transferId'),
384
+ };
385
+ }
386
+ export function encodeCollabAuthorityRelinquishmentProofSigningInput(payload) {
387
+ const source = exactRecord(payload, 'relinquishmentProofSigningPayload', RELINQUISHMENT_PROOF_SIGNING_PAYLOAD_KEYS);
388
+ return JSON.stringify({
389
+ domain: 'claudian-collab.authority-relinquishment-proof.v1',
390
+ payload: authorityRelinquishmentProofSigningPayload(source),
391
+ });
392
+ }
393
+ export function decodeCollabAuthorityRelinquishmentProof(value) {
394
+ const source = exactRecord(value, 'relinquishmentProof', [
395
+ ...RELINQUISHMENT_PROOF_SIGNING_PAYLOAD_KEYS,
396
+ 'certificate',
397
+ ]);
398
+ const payload = authorityRelinquishmentProofSigningPayload(source);
399
+ return {
400
+ batchRevision: payload.batchRevision,
401
+ batchSha256: payload.batchSha256,
402
+ certificate: fixedBase64url(source, 'certificate', 64),
403
+ certificateAlgorithm: payload.certificateAlgorithm,
404
+ checkpointSha256: payload.checkpointSha256,
405
+ committedAt: payload.committedAt,
406
+ operationIntentId: payload.operationIntentId,
407
+ projectId: payload.projectId,
408
+ sourceAuthority: payload.sourceAuthority,
409
+ sourceHostMemberId: payload.sourceHostMemberId,
410
+ targetAuthority: payload.targetAuthority,
411
+ transferId: payload.transferId,
412
+ };
413
+ }
414
+ const LAN_TO_CLOUD_PHASE_SET = new Set(COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES);
415
+ const CLOUD_TO_LAN_PHASE_SET = new Set(COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES);
416
+ const CANCELLATION_PHASE_SET = new Set(COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES);
417
+ const CANCELLABLE_PHASE_SET = new Set(COLLAB_AUTHORITY_TRANSFER_CANCELLABLE_PHASES);
418
+ const LAN_TO_CLOUD_CHECKPOINT_REQUIRED_PHASE_SET = new Set(COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES.slice(2));
419
+ const CLOUD_TO_LAN_CHECKPOINT_REQUIRED_PHASE_SET = new Set(COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.slice(2));
420
+ const LAN_TO_CLOUD_BATCH_REQUIRED_PHASE_SET = new Set(COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES.slice(4));
421
+ const CLOUD_TO_LAN_BATCH_REQUIRED_PHASE_SET = new Set(COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.slice(4));
422
+ const LAN_TO_CLOUD_RELINQUISHMENT_REQUIRED_PHASE_SET = new Set(COLLAB_LAN_TO_CLOUD_TRANSFER_PHASES.slice(6));
423
+ const CLOUD_TO_LAN_RELINQUISHMENT_REQUIRED_PHASE_SET = new Set(COLLAB_CLOUD_TO_LAN_TRANSFER_PHASES.slice(5));
424
+ function transferPhase(source, direction) {
425
+ const value = source.phase;
426
+ const directionSet = direction === 'lan-to-cloud'
427
+ ? LAN_TO_CLOUD_PHASE_SET
428
+ : CLOUD_TO_LAN_PHASE_SET;
429
+ if (typeof value !== 'string' || (!directionSet.has(value) && !CANCELLATION_PHASE_SET.has(value))) {
430
+ throw invalidPayload('phase');
431
+ }
432
+ return value;
433
+ }
434
+ export function decodeCollabAuthorityTransferLifecycleFence(value) {
435
+ const source = exactRecord(value, 'transferLifecycleFence', [
436
+ 'batchRevision',
437
+ 'batchSha256',
438
+ 'checkpointSha256',
439
+ 'direction',
440
+ 'phase',
441
+ ]);
442
+ const direction = literal(source, 'direction', ['cloud-to-lan', 'lan-to-cloud']);
443
+ const phase = transferPhase(source, direction);
444
+ const checkpointSha256 = source.checkpointSha256 === null
445
+ ? null
446
+ : sha256(source, 'checkpointSha256');
447
+ const batchRevision = source.batchRevision === null
448
+ ? null
449
+ : positiveInteger(source, 'batchRevision');
450
+ const batchSha256 = source.batchSha256 === null
451
+ ? null
452
+ : sha256(source, 'batchSha256');
453
+ if ((batchRevision === null) !== (batchSha256 === null)
454
+ || (batchRevision !== null && checkpointSha256 === null)
455
+ || ((direction === 'lan-to-cloud'
456
+ ? LAN_TO_CLOUD_CHECKPOINT_REQUIRED_PHASE_SET
457
+ : CLOUD_TO_LAN_CHECKPOINT_REQUIRED_PHASE_SET).has(phase)
458
+ && checkpointSha256 === null)
459
+ || ((direction === 'lan-to-cloud'
460
+ ? LAN_TO_CLOUD_BATCH_REQUIRED_PHASE_SET
461
+ : CLOUD_TO_LAN_BATCH_REQUIRED_PHASE_SET).has(phase)
462
+ && batchRevision === null))
463
+ throw invalidPayload('claimBatch');
464
+ const relinquishmentRequired = (direction === 'lan-to-cloud'
465
+ ? LAN_TO_CLOUD_RELINQUISHMENT_REQUIRED_PHASE_SET
466
+ : CLOUD_TO_LAN_RELINQUISHMENT_REQUIRED_PHASE_SET).has(phase);
467
+ return {
468
+ batchRevision,
469
+ batchSha256,
470
+ checkpointSha256,
471
+ direction,
472
+ phase,
473
+ relinquishmentRequired,
474
+ };
475
+ }
476
+ export function decodeCollabAuthorityTransferStatus(value) {
477
+ const source = exactRecord(value, 'transferStatus', [
478
+ 'batchRevision',
479
+ 'batchSha256',
480
+ 'checkpointSha256',
481
+ 'createdAt',
482
+ 'direction',
483
+ 'expiresAt',
484
+ 'phase',
485
+ 'projectId',
486
+ 'relinquishmentProof',
487
+ 'sourceAuthority',
488
+ 'state',
489
+ 'targetAuthority',
490
+ 'targetUrl',
491
+ 'transferId',
492
+ 'updatedAt',
493
+ ]);
494
+ const lifecycleFence = decodeCollabAuthorityTransferLifecycleFence({
495
+ batchRevision: source.batchRevision,
496
+ batchSha256: source.batchSha256,
497
+ checkpointSha256: source.checkpointSha256,
498
+ direction: source.direction,
499
+ phase: source.phase,
500
+ });
501
+ const { batchRevision, batchSha256, checkpointSha256, direction, phase, relinquishmentRequired, } = lifecycleFence;
502
+ const sourceAuthority = authority(source.sourceAuthority, 'sourceAuthority');
503
+ const targetAuthority = authority(source.targetAuthority, 'targetAuthority');
504
+ const state = literal(source, 'state', ['active', 'cancelled', 'completed']);
505
+ if (sourceAuthority.kind === targetAuthority.kind
506
+ || targetAuthority.generation !== sourceAuthority.generation + 1
507
+ || (direction === 'lan-to-cloud'
508
+ && (sourceAuthority.kind !== 'lan' || targetAuthority.kind !== 'cloud'))
509
+ || (direction === 'cloud-to-lan'
510
+ && (sourceAuthority.kind !== 'cloud' || targetAuthority.kind !== 'lan'))
511
+ || (state === 'cancelled' && phase !== 'cancelled')
512
+ || (state === 'completed' && phase !== 'completed')
513
+ || (state === 'active' && (phase === 'cancelled' || phase === 'completed')))
514
+ throw invalidPayload('transferStatus');
515
+ const relinquishmentProof = source.relinquishmentProof === null
516
+ ? null
517
+ : decodeCollabAuthorityRelinquishmentProof(source.relinquishmentProof);
518
+ if (relinquishmentRequired !== (relinquishmentProof !== null)
519
+ || (relinquishmentProof !== null && (relinquishmentProof.batchRevision !== batchRevision
520
+ || relinquishmentProof.batchSha256 !== batchSha256
521
+ || relinquishmentProof.checkpointSha256 !== checkpointSha256
522
+ || relinquishmentProof.projectId !== source.projectId
523
+ || relinquishmentProof.sourceAuthority.generation !== sourceAuthority.generation
524
+ || relinquishmentProof.sourceAuthority.kind !== sourceAuthority.kind
525
+ || relinquishmentProof.targetAuthority.generation !== targetAuthority.generation
526
+ || relinquishmentProof.targetAuthority.kind !== targetAuthority.kind
527
+ || relinquishmentProof.transferId !== source.transferId)))
528
+ throw invalidPayload('relinquishmentProof');
529
+ const createdAt = timestamp(source, 'createdAt');
530
+ const updatedAt = timestamp(source, 'updatedAt');
531
+ const expiresAt = timestamp(source, 'expiresAt');
532
+ if (Date.parse(updatedAt) < Date.parse(createdAt)
533
+ || Date.parse(expiresAt) <= Date.parse(updatedAt))
534
+ throw invalidPayload('transferStatus');
535
+ return {
536
+ batchRevision,
537
+ batchSha256,
538
+ checkpointSha256,
539
+ createdAt,
540
+ direction,
541
+ expiresAt,
542
+ phase,
543
+ projectId: token(source, 'projectId', isCollabProjectId),
544
+ relinquishmentProof,
545
+ sourceAuthority,
546
+ state,
547
+ targetAuthority,
548
+ targetUrl: absoluteTargetUrl(source),
549
+ transferId: token(source, 'transferId'),
550
+ updatedAt,
551
+ };
552
+ }
553
+ function mutationFields(source) {
554
+ return {
555
+ idempotencyKey: token(source, 'idempotencyKey'),
556
+ projectId: token(source, 'projectId', isCollabProjectId),
557
+ };
558
+ }
559
+ function decodeRequestLanToCloudTransfer(value) {
560
+ const source = exactRecord(value, 'request', [
561
+ 'expectedAuthorityGeneration',
562
+ 'idempotencyKey',
563
+ 'projectId',
564
+ 'targetUrl',
565
+ ]);
566
+ return {
567
+ expectedAuthorityGeneration: positiveInteger(source, 'expectedAuthorityGeneration'),
568
+ ...mutationFields(source),
569
+ targetUrl: absoluteTargetUrl(source),
570
+ };
571
+ }
572
+ function decodeAcceptLanToCloudTransferTarget(value) {
573
+ const source = exactRecord(value, 'request', [
574
+ 'expectedAuthorityGeneration',
575
+ 'idempotencyKey',
576
+ 'projectId',
577
+ 'targetUrl',
578
+ 'transferId',
579
+ ]);
580
+ return {
581
+ expectedAuthorityGeneration: positiveInteger(source, 'expectedAuthorityGeneration'),
582
+ ...mutationFields(source),
583
+ targetUrl: absoluteTargetUrl(source),
584
+ transferId: token(source, 'transferId'),
585
+ };
586
+ }
587
+ function decodeBeginLanToCloudTransfer(value) {
588
+ const source = exactRecord(value, 'request', [
589
+ 'checkpointManifestSha256',
590
+ 'expectedSourceAuthorityGeneration',
591
+ 'idempotencyKey',
592
+ 'projectId',
593
+ 'sourceHostMemberId',
594
+ 'sourceProof',
595
+ 'targetUrl',
596
+ 'transferId',
597
+ ]);
598
+ return {
599
+ checkpointManifestSha256: sha256(source, 'checkpointManifestSha256'),
600
+ expectedSourceAuthorityGeneration: positiveInteger(source, 'expectedSourceAuthorityGeneration'),
601
+ ...mutationFields(source),
602
+ sourceHostMemberId: token(source, 'sourceHostMemberId', isCollabMemberId),
603
+ sourceProof: base64url(source, 'sourceProof'),
604
+ targetUrl: absoluteTargetUrl(source),
605
+ transferId: token(source, 'transferId'),
606
+ };
607
+ }
608
+ function decodeGetProjectAuthorityTransfer(value) {
609
+ const source = exactRecord(value, 'request', ['projectId', 'transferId']);
610
+ return {
611
+ projectId: token(source, 'projectId', isCollabProjectId),
612
+ transferId: token(source, 'transferId'),
613
+ };
614
+ }
615
+ function decodeAuthorityTransferReceiptVerifier(value) {
616
+ const source = exactRecord(value, 'receiptVerifier', [
617
+ 'projectId',
618
+ 'receiptKeyId',
619
+ 'receiptPublicKey',
620
+ 'receiptPublicKeyEncoding',
621
+ 'signatureAlgorithm',
622
+ 'transferId',
623
+ ]);
624
+ return {
625
+ projectId: token(source, 'projectId', isCollabProjectId),
626
+ receiptKeyId: boundedString(source, 'receiptKeyId', 256),
627
+ receiptPublicKey: fixedBase64url(source, 'receiptPublicKey', 32),
628
+ receiptPublicKeyEncoding: literal(source, 'receiptPublicKeyEncoding', ['base64url-raw']),
629
+ signatureAlgorithm: literal(source, 'signatureAlgorithm', ['ed25519']),
630
+ transferId: token(source, 'transferId'),
631
+ };
632
+ }
633
+ function decodeRotateTransferredMembershipClaims(value) {
634
+ const source = exactRecord(value, 'request', [
635
+ 'expectedBatchRevision',
636
+ 'expectedBatchSha256',
637
+ 'idempotencyKey',
638
+ 'projectId',
639
+ 'transferId',
640
+ ]);
641
+ return {
642
+ expectedBatchRevision: positiveInteger(source, 'expectedBatchRevision'),
643
+ expectedBatchSha256: sha256(source, 'expectedBatchSha256'),
644
+ ...mutationFields(source),
645
+ transferId: token(source, 'transferId'),
646
+ };
647
+ }
648
+ function decodeAcknowledgeTransferredMembershipClaimBatch(value) {
649
+ const source = exactRecord(value, 'request', [
650
+ 'batchRevision',
651
+ 'batchSha256',
652
+ 'idempotencyKey',
653
+ 'operationIntentId',
654
+ 'projectId',
655
+ 'transferId',
656
+ ]);
657
+ return {
658
+ batchRevision: positiveInteger(source, 'batchRevision'),
659
+ batchSha256: sha256(source, 'batchSha256'),
660
+ ...mutationFields(source),
661
+ operationIntentId: token(source, 'operationIntentId'),
662
+ transferId: token(source, 'transferId'),
663
+ };
664
+ }
665
+ function decodeGetTransferredMembershipClaim(value) {
666
+ return decodeGetProjectAuthorityTransfer(value);
667
+ }
668
+ function decodeClaimTransferredMembership(value) {
669
+ const raw = record(value, 'request');
670
+ const hasCredentialHash = Object.hasOwn(raw, 'credentialHash');
671
+ const source = exactRecord(value, 'request', hasCredentialHash
672
+ ? ['claim', 'credentialHash', 'idempotencyKey', 'projectId', 'transferId']
673
+ : ['claim', 'idempotencyKey', 'projectId', 'transferId']);
674
+ const common = {
675
+ claim: base64url(source, 'claim'),
676
+ ...mutationFields(source),
677
+ transferId: token(source, 'transferId'),
678
+ };
679
+ return hasCredentialHash
680
+ ? {
681
+ ...common,
682
+ credentialHash: sha256(source, 'credentialHash'),
683
+ }
684
+ : common;
685
+ }
686
+ function decodeAcknowledgeTransferredMembershipClaimRedemption(value) {
687
+ const source = exactRecord(value, 'request', [
688
+ 'idempotencyKey',
689
+ 'projectId',
690
+ 'receipt',
691
+ 'transferId',
692
+ ]);
693
+ const common = mutationFields(source);
694
+ const transferId = token(source, 'transferId');
695
+ const receipt = decodeCollabTransferredMembershipRedemptionReceipt(source.receipt);
696
+ if (receipt.projectId !== common.projectId || receipt.transferId !== transferId) {
697
+ throw invalidPayload('receipt');
698
+ }
699
+ return { ...common, receipt, transferId };
700
+ }
701
+ function decodeCommitLanToCloudRelinquishment(value) {
702
+ const source = exactRecord(value, 'request', [
703
+ 'idempotencyKey',
704
+ 'projectId',
705
+ 'proof',
706
+ 'transferId',
707
+ ]);
708
+ const common = mutationFields(source);
709
+ const transferId = token(source, 'transferId');
710
+ const proof = decodeCollabAuthorityRelinquishmentProof(source.proof);
711
+ if (proof.projectId !== common.projectId || proof.transferId !== transferId) {
712
+ throw invalidPayload('proof');
713
+ }
714
+ return { ...common, proof, transferId };
715
+ }
716
+ function decodeBeginCloudToLanTransfer(value) {
717
+ const source = exactRecord(value, 'request', [
718
+ 'expectedAuthorityGeneration',
719
+ 'idempotencyKey',
720
+ 'projectId',
721
+ 'targetHostMemberId',
722
+ 'targetUrl',
723
+ ]);
724
+ return {
725
+ expectedAuthorityGeneration: positiveInteger(source, 'expectedAuthorityGeneration'),
726
+ ...mutationFields(source),
727
+ targetHostMemberId: token(source, 'targetHostMemberId', isCollabMemberId),
728
+ targetUrl: absoluteTargetUrl(source),
729
+ };
730
+ }
731
+ function decodeAcceptCloudToLanTransferTarget(value) {
732
+ const source = exactRecord(value, 'request', [
733
+ 'idempotencyKey',
734
+ 'projectId',
735
+ 'targetHostMemberId',
736
+ 'targetProof',
737
+ 'transferId',
738
+ ]);
739
+ return {
740
+ ...mutationFields(source),
741
+ targetHostMemberId: token(source, 'targetHostMemberId', isCollabMemberId),
742
+ targetProof: base64url(source, 'targetProof'),
743
+ transferId: token(source, 'transferId'),
744
+ };
745
+ }
746
+ function decodeReportCloudToLanTargetStaged(value) {
747
+ const source = exactRecord(value, 'request', [
748
+ 'checkpointSha256',
749
+ 'claimBatch',
750
+ 'idempotencyKey',
751
+ 'projectId',
752
+ 'stageSha256',
753
+ 'targetAuthority',
754
+ 'targetProof',
755
+ 'transferId',
756
+ ]);
757
+ const targetAuthority = authority(source.targetAuthority, 'targetAuthority');
758
+ if (targetAuthority.kind !== 'lan')
759
+ throw invalidPayload('targetAuthority');
760
+ const common = mutationFields(source);
761
+ const transferId = token(source, 'transferId');
762
+ const checkpointSha256 = sha256(source, 'checkpointSha256');
763
+ const claimBatch = decodeCollabTransferredMembershipClaimBatch(source.claimBatch);
764
+ if (claimBatch.projectId !== common.projectId
765
+ || claimBatch.transferId !== transferId
766
+ || claimBatch.checkpointSha256 !== checkpointSha256
767
+ || claimBatch.targetAuthorityGeneration !== targetAuthority.generation)
768
+ throw invalidPayload('claimBatch');
769
+ return {
770
+ checkpointSha256,
771
+ claimBatch,
772
+ ...common,
773
+ stageSha256: sha256(source, 'stageSha256'),
774
+ targetAuthority,
775
+ targetProof: base64url(source, 'targetProof'),
776
+ transferId,
777
+ };
778
+ }
779
+ function decodeConfirmCloudToLanTargetActive(value) {
780
+ const source = exactRecord(value, 'request', [
781
+ 'idempotencyKey',
782
+ 'projectId',
783
+ 'relinquishmentProof',
784
+ 'targetActivationProof',
785
+ 'transferId',
786
+ ]);
787
+ const common = mutationFields(source);
788
+ const transferId = token(source, 'transferId');
789
+ const relinquishmentProof = decodeCollabAuthorityRelinquishmentProof(source.relinquishmentProof);
790
+ if (relinquishmentProof.projectId !== common.projectId
791
+ || relinquishmentProof.transferId !== transferId
792
+ || relinquishmentProof.sourceAuthority.kind !== 'cloud'
793
+ || relinquishmentProof.targetAuthority.kind !== 'lan')
794
+ throw invalidPayload('relinquishmentProof');
795
+ return {
796
+ ...common,
797
+ relinquishmentProof,
798
+ targetActivationProof: base64url(source, 'targetActivationProof'),
799
+ transferId,
800
+ };
801
+ }
802
+ function decodeCancelProjectAuthorityTransfer(value) {
803
+ const source = exactRecord(value, 'request', [
804
+ 'expectedPhase',
805
+ 'idempotencyKey',
806
+ 'projectId',
807
+ 'transferId',
808
+ ]);
809
+ const expectedPhase = source.expectedPhase;
810
+ if (typeof expectedPhase !== 'string'
811
+ || !CANCELLABLE_PHASE_SET.has(expectedPhase))
812
+ throw invalidPayload('expectedPhase');
813
+ return {
814
+ expectedPhase: expectedPhase,
815
+ ...mutationFields(source),
816
+ transferId: token(source, 'transferId'),
817
+ };
818
+ }
819
+ export function decodeCollabAuthorityTransferOperationRequest(operation, value) {
820
+ const decoded = (() => {
821
+ switch (operation) {
822
+ case 'requestLanToCloudTransfer': return decodeRequestLanToCloudTransfer(value);
823
+ case 'acceptLanToCloudTransferTarget': return decodeAcceptLanToCloudTransferTarget(value);
824
+ case 'beginLanToCloudTransfer': return decodeBeginLanToCloudTransfer(value);
825
+ case 'getProjectAuthorityTransfer': return decodeGetProjectAuthorityTransfer(value);
826
+ case 'getAuthorityTransferReceiptVerifier':
827
+ return decodeGetProjectAuthorityTransfer(value);
828
+ case 'rotateTransferredMembershipClaims':
829
+ return decodeRotateTransferredMembershipClaims(value);
830
+ case 'acknowledgeTransferredMembershipClaimBatch':
831
+ return decodeAcknowledgeTransferredMembershipClaimBatch(value);
832
+ case 'getTransferredMembershipClaim': return decodeGetTransferredMembershipClaim(value);
833
+ case 'claimTransferredMembership': return decodeClaimTransferredMembership(value);
834
+ case 'acknowledgeTransferredMembershipClaimRedemption':
835
+ return decodeAcknowledgeTransferredMembershipClaimRedemption(value);
836
+ case 'commitLanToCloudRelinquishment':
837
+ return decodeCommitLanToCloudRelinquishment(value);
838
+ case 'beginCloudToLanTransfer': return decodeBeginCloudToLanTransfer(value);
839
+ case 'acceptCloudToLanTransferTarget': return decodeAcceptCloudToLanTransferTarget(value);
840
+ case 'reportCloudToLanTargetStaged': return decodeReportCloudToLanTargetStaged(value);
841
+ case 'confirmCloudToLanTargetActive': return decodeConfirmCloudToLanTargetActive(value);
842
+ case 'cancelProjectAuthorityTransfer': return decodeCancelProjectAuthorityTransfer(value);
843
+ }
844
+ })();
845
+ return decoded;
846
+ }
847
+ function decodeRedemptionAcknowledgement(value) {
848
+ const source = exactRecord(value, 'redemptionAcknowledgement', [
849
+ 'acknowledgedAt',
850
+ 'memberId',
851
+ 'projectId',
852
+ 'receiptId',
853
+ 'transferId',
854
+ ]);
855
+ return {
856
+ acknowledgedAt: timestamp(source, 'acknowledgedAt'),
857
+ memberId: token(source, 'memberId', isCollabMemberId),
858
+ projectId: token(source, 'projectId', isCollabProjectId),
859
+ receiptId: token(source, 'receiptId'),
860
+ transferId: token(source, 'transferId'),
861
+ };
862
+ }
863
+ export function decodeCollabAuthorityTransferOperationResponse(operation, value) {
864
+ const decoded = (() => {
865
+ switch (operation) {
866
+ case 'rotateTransferredMembershipClaims':
867
+ return decodeCollabTransferredMembershipClaimBatch(value);
868
+ case 'acknowledgeTransferredMembershipClaimBatch':
869
+ case 'reportCloudToLanTargetStaged':
870
+ return decodeCollabTransferredMembershipClaimCustodyReceipt(value);
871
+ case 'getTransferredMembershipClaim':
872
+ return decodeCollabTransferredMembershipClaim(value);
873
+ case 'claimTransferredMembership':
874
+ return decodeCollabTransferredMembershipRedemptionReceipt(value);
875
+ case 'acknowledgeTransferredMembershipClaimRedemption':
876
+ return decodeRedemptionAcknowledgement(value);
877
+ case 'getAuthorityTransferReceiptVerifier':
878
+ return decodeAuthorityTransferReceiptVerifier(value);
879
+ default:
880
+ return decodeCollabAuthorityTransferStatus(value);
881
+ }
882
+ })();
883
+ return decoded;
884
+ }