@claudian-collab/protocol 3.3.0 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -21
- package/dist/CollabProjectBackupCheckpoint.d.ts +11 -2
- package/dist/CollabProjectBackupCheckpoint.js +96 -106
- package/dist/CollabProjectCheckpoint.d.ts +25 -0
- package/dist/CollabProjectCheckpoint.js +55 -25
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +97 -107
- package/dist/esm/CollabProjectCheckpoint.mjs +50 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ fixed part of the detail so the whole detail stays within
|
|
|
37
37
|
has one compatible final-serialization limit. Request comments and Ticket
|
|
38
38
|
accepted relations also have shared authority-enforced total limits; summary
|
|
39
39
|
counts let complete consumers reject partial or cross-snapshot assembly.
|
|
40
|
-
Cloud binding v2 adds the authority-neutral Project snapshot, nine redacted durable event kinds plus `snapshot.required`, eighteen capability tokens, the exact ordinary Project/Git route catalog, bounded authority-transfer artifact routes, and six private-development bootstrap bindings. Wire v6 includes the
|
|
40
|
+
Cloud binding v2 adds the authority-neutral Project snapshot, nine redacted durable event kinds plus `snapshot.required`, eighteen capability tokens, the exact ordinary Project/Git route catalog, bounded authority-transfer artifact routes, and six private-development bootstrap bindings. Wire v6 includes the Cloud Project-membership operation family. The shared checkpoint container is exactly `checkpoint.json`, `coordination.ndjson`, and `repository.bundle`; authority-transfer and export use wire-visible coordination format v1, and offline backup uses format v3. Backup v3 adds Project invitations, protected invitation and transferred-claim override envelopes, manager-responsibility offers, membership recovery journals, secret-replay tombstones, and compacted Manager-responsibility idempotency tombstones to the existing continuity records. Portable profiles exclude engine storage, paths, credentials, tokens, private keys, and operational refs, and backups exclude plaintext invitation secrets, raw claims, and private keys. Domain-separated canonical UTF-8 inputs define protected-claim AEAD associated data and bind every declared field of transferred-membership redemption receipts and authority-relinquishment proofs before consumer-owned Ed25519 signing or verification. Existing LAN v9 Join, invitation, endpoint, membership lifecycle, Host-transfer, snapshot, event, and HTTP bindings remain independently application-owned; the dedicated LAN authority-transfer transport binds these shared payloads without creating another shared operation registry.
|
|
41
|
+
|
|
42
|
+
Backup coordination artifacts are limited to 256 MiB of actual UTF-8 bytes, including the final newline. The decoder, encoder, and consistency checker apply this same inclusive limit without rewriting principals into an intermediate format. Valid current-format artifacts at the limit are accepted; an additional byte is rejected.
|
|
41
43
|
|
|
42
44
|
## Usage
|
|
43
45
|
|
|
@@ -62,28 +64,14 @@ part of the public surface.
|
|
|
62
64
|
|
|
63
65
|
Package SemVer, canonical wire version, and Cloud binding version are independent concepts.
|
|
64
66
|
|
|
65
|
-
- **Package version** (this `package.json`): `3.3.
|
|
66
|
-
every existing public declaration and runtime behavior baseline. Minor
|
|
67
|
-
releases may add backward-compatible API. Removing or changing an existing
|
|
68
|
-
declaration, export, runtime behavior, codec, error, limit, ref rule,
|
|
69
|
-
operation, or compatibility rule requires a major release. An independently
|
|
70
|
-
proven additive control operation may use a package minor release without a
|
|
71
|
-
wire-version increase; existing operation declarations and fixtures must stay
|
|
72
|
-
exact. Package 3 and wire 6 support exact transfer-claim batches and a
|
|
73
|
-
source-pinned target receipt verifier. Package 3.3 adds the Cloud Project-membership
|
|
74
|
-
operation family and capability tokens and directly replaces the pre-production
|
|
75
|
-
offline backup contract with coordination format v3, without changing
|
|
76
|
-
authority-transfer/export format v1, canonical wire v6, or Cloud binding v2.
|
|
77
|
-
Backup v2 has no supported decoder or migration path. The package version never
|
|
78
|
-
signals wire compatibility by itself.
|
|
67
|
+
- **Package version** (this `package.json`): `3.3.2`. Behavior-preserving implementation refactors and reviewed compatible defect corrections may use a patch release when public declarations, runtime exports, and accepted wire/binding semantics remain unchanged. Minor releases may add backward-compatible API. Removing or incompatibly changing an existing declaration, export, runtime behavior, codec, error, limit, ref rule, operation, or compatibility rule requires a major release. Package API SemVer is classified independently from the wire and Cloud binding contracts, but a compatible package addition cannot exempt a changed wire contract from a wire-version increase or a changed Cloud binding contract from a binding-version increase. The package version never signals wire compatibility by itself.
|
|
79
68
|
- **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `6`. The supported
|
|
80
69
|
range is exactly `[6, 6]`. This is independent from Cloud binding version `2` and the existing application
|
|
81
70
|
LAN control version `9`. Any change to an existing envelope, DTO, operation
|
|
82
|
-
payload shape, or operation definition
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `2`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Binding v1 and wire versions before v6 are unsupported rather than translated or dual-interpreted.
|
|
71
|
+
payload shape, or operation definition requires a new wire-protocol version.
|
|
72
|
+
This includes additive operations: every wire-contract change increases the
|
|
73
|
+
canonical wire version.
|
|
74
|
+
- **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `2`. It versions Cloud routes, bounded transfer streams, and capabilities independently from package and wire versions. Every Cloud binding-contract change increases this version. Binding v1 and wire versions before v6 are unsupported rather than translated or dual-interpreted.
|
|
87
75
|
|
|
88
76
|
### Compatibility behavior
|
|
89
77
|
|
|
@@ -102,14 +90,18 @@ Compatibility is tested with executable fixtures in this package
|
|
|
102
90
|
(`tests/`), and cross-repository contract fixtures pin client and server to
|
|
103
91
|
the same behavior.
|
|
104
92
|
|
|
93
|
+
The compatibility snapshot also fingerprints implementation tokens. Those fingerprints detect edits but cannot establish whether behavior changed. A reviewed implementation-only refactor or approved compatible defect correction uses `compatibility-review.json`, generated with `npm run check:compatibility -- --base <base-sha> --record-implementation-only-review "<reviewed reason>"` after updating the generated snapshot and characterizing the public seams. The reason distinguishes preserved behavior from any approved correction and its executable evidence. The record binds the exact base and candidate snapshots; the classifier independently rejects any changed public declaration, export, or wire/binding semantic fact. Missing or stale evidence cannot authorize an implementation change, and the record never applies to another version or later edit merely because an earlier change was accepted. This review requirement is general, not a prelaunch or one-release compatibility exception, and it is not an automated proof of behavioral equivalence.
|
|
94
|
+
|
|
105
95
|
## Development
|
|
106
96
|
|
|
107
97
|
```bash
|
|
108
98
|
npm run build # compile src/ -> dist/ (CommonJS + declarations)
|
|
109
99
|
npm test # package unit tests
|
|
110
100
|
npm run check:compatibility
|
|
111
|
-
npm run
|
|
101
|
+
npm run release:candidate # build and verify the reviewed release artifact
|
|
102
|
+
npm run verify:pack # smoke-test that exact artifact in a clean consumer
|
|
112
103
|
```
|
|
113
104
|
|
|
114
105
|
`dist/` is generated output and is never committed. The packed artifact is
|
|
115
106
|
reproducible from source: `npm pack` rebuilds via `prepack`.
|
|
107
|
+
The full verification gate constructs one release candidate and uses those same reviewed tarball bytes for clean-consumer smoke tests and publication.
|
|
@@ -5,7 +5,7 @@ import { type CollabManagerResponsibilityOfferState, type CollabManagerResponsib
|
|
|
5
5
|
import type { CollabIsoTimestamp, CollabMemberId, CollabProjectId } from './types';
|
|
6
6
|
export declare const COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE: "pre-production-replaceable";
|
|
7
7
|
export declare const COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION: 3;
|
|
8
|
-
export declare const COLLAB_PROJECT_BACKUP_RECORD_KINDS: readonly ["project", "member", "request", "request-comment", "ticket", "ticket-comment", "ticket-relation", "ticket-mention", "cloud-event", "cloud-event-cursor", "idempotency-result", "principal-binding", "repository-placement", "lifecycle-journal", "authority-transfer-recovery", "transferred-membership-claim", "transfer-receipt-key", "transfer-claim-batch-receipt", "transfer-redemption-receipt", "terminal-principal", "terminal-responder-replay", "leave-former-principal-replay", "project-invitation", "protected-invitation-envelope", "transferred-membership-claim-override", "protected-claim-override-envelope", "manager-responsibility-offer", "project-membership-recovery", "secret-replay-tombstone", "terminal-responder", "protected-claim-envelope", "tombstone", "schema-catalog", "server-compatibility", "authority-volume-pair"];
|
|
8
|
+
export declare const COLLAB_PROJECT_BACKUP_RECORD_KINDS: readonly ["project", "member", "request", "request-comment", "ticket", "ticket-comment", "ticket-relation", "ticket-mention", "cloud-event", "cloud-event-cursor", "idempotency-result", "principal-binding", "repository-placement", "lifecycle-journal", "authority-transfer-recovery", "transferred-membership-claim", "transfer-receipt-key", "transfer-claim-batch-receipt", "transfer-redemption-receipt", "terminal-principal", "terminal-responder-replay", "leave-former-principal-replay", "project-invitation", "protected-invitation-envelope", "transferred-membership-claim-override", "protected-claim-override-envelope", "manager-responsibility-offer", "membership-idempotency-tombstone", "project-membership-recovery", "secret-replay-tombstone", "terminal-responder", "protected-claim-envelope", "tombstone", "schema-catalog", "server-compatibility", "authority-volume-pair"];
|
|
9
9
|
export type CollabProjectBackupRecordKind = typeof COLLAB_PROJECT_BACKUP_RECORD_KINDS[number];
|
|
10
10
|
interface BackupRecordBase<Kind extends string, Value> {
|
|
11
11
|
readonly kind: Kind;
|
|
@@ -208,6 +208,15 @@ export type CollabProjectBackupManagerResponsibilityOfferRecord = BackupRecordBa
|
|
|
208
208
|
readonly targetMembershipRevisionAtOffer: number;
|
|
209
209
|
readonly terminalAt: CollabIsoTimestamp | null;
|
|
210
210
|
}>;
|
|
211
|
+
type CollabProjectBackupCompactedMembershipOperation = Extract<CollabProjectMembershipOperation, 'createManagerResponsibilityOffer' | 'acknowledgeManagerResponsibility' | 'declineManagerResponsibility' | 'cancelManagerResponsibilityOffer' | 'promoteManager'>;
|
|
212
|
+
export type CollabProjectBackupMembershipIdempotencyTombstoneRecord = BackupRecordBase<'membership-idempotency-tombstone', {
|
|
213
|
+
readonly actorMemberId: CollabMemberId;
|
|
214
|
+
readonly compactedAt: CollabIsoTimestamp;
|
|
215
|
+
readonly idempotencyKey: string;
|
|
216
|
+
readonly operation: CollabProjectBackupCompactedMembershipOperation;
|
|
217
|
+
readonly projectId: CollabProjectId;
|
|
218
|
+
readonly requestFingerprint: string;
|
|
219
|
+
}>;
|
|
211
220
|
export type CollabProjectBackupMembershipRecoveryRecord = BackupRecordBase<'project-membership-recovery', {
|
|
212
221
|
readonly expectedMainOid: string;
|
|
213
222
|
readonly expectedPersonalRefOid: string;
|
|
@@ -229,7 +238,7 @@ export type CollabProjectBackupSecretReplayTombstoneRecord = BackupRecordBase<'s
|
|
|
229
238
|
readonly projectId: CollabProjectId;
|
|
230
239
|
readonly requestFingerprint: string;
|
|
231
240
|
}>;
|
|
232
|
-
export type CollabProjectBackupContinuityRecord = CollabProjectBackupLifecycleJournalRecord | CollabProjectBackupAuthorityTransferRecoveryRecord | CollabProjectBackupTransferredMembershipClaimRecord | CollabProjectBackupTransferReceiptKeyRecord | CollabProjectBackupTransferClaimBatchReceiptRecord | CollabProjectBackupTransferRedemptionReceiptRecord | CollabProjectBackupTerminalPrincipalRecord | CollabProjectBackupTerminalResponderReplayRecord | CollabProjectBackupLeaveFormerPrincipalReplayRecord | CollabProjectBackupInvitationRecord | CollabProjectBackupProtectedInvitationEnvelopeRecord | CollabProjectBackupTransferredMembershipClaimOverrideRecord | CollabProjectBackupProtectedClaimOverrideEnvelopeRecord | CollabProjectBackupManagerResponsibilityOfferRecord | CollabProjectBackupMembershipRecoveryRecord | CollabProjectBackupSecretReplayTombstoneRecord;
|
|
241
|
+
export type CollabProjectBackupContinuityRecord = CollabProjectBackupLifecycleJournalRecord | CollabProjectBackupAuthorityTransferRecoveryRecord | CollabProjectBackupTransferredMembershipClaimRecord | CollabProjectBackupTransferReceiptKeyRecord | CollabProjectBackupTransferClaimBatchReceiptRecord | CollabProjectBackupTransferRedemptionReceiptRecord | CollabProjectBackupTerminalPrincipalRecord | CollabProjectBackupTerminalResponderReplayRecord | CollabProjectBackupLeaveFormerPrincipalReplayRecord | CollabProjectBackupInvitationRecord | CollabProjectBackupProtectedInvitationEnvelopeRecord | CollabProjectBackupTransferredMembershipClaimOverrideRecord | CollabProjectBackupProtectedClaimOverrideEnvelopeRecord | CollabProjectBackupManagerResponsibilityOfferRecord | CollabProjectBackupMembershipIdempotencyTombstoneRecord | CollabProjectBackupMembershipRecoveryRecord | CollabProjectBackupSecretReplayTombstoneRecord;
|
|
233
242
|
type CollabProjectBackupBaseRecord = Exclude<CollabCheckpointBackupRecord, {
|
|
234
243
|
readonly kind: 'lifecycle-state';
|
|
235
244
|
}>;
|
|
@@ -32,6 +32,7 @@ const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
|
32
32
|
'transferred-membership-claim-override',
|
|
33
33
|
'protected-claim-override-envelope',
|
|
34
34
|
'manager-responsibility-offer',
|
|
35
|
+
'membership-idempotency-tombstone',
|
|
35
36
|
'project-membership-recovery',
|
|
36
37
|
'secret-replay-tombstone',
|
|
37
38
|
]);
|
|
@@ -207,65 +208,19 @@ function absoluteTargetUrl(source) {
|
|
|
207
208
|
function principal(source, field) {
|
|
208
209
|
return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
|
|
209
210
|
}
|
|
210
|
-
function
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (source.kind === 'principal-binding') {
|
|
214
|
-
return {
|
|
215
|
-
...source,
|
|
216
|
-
value: { ...value, principalId: `principal_${String(value.memberId)}` },
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
|
|
220
|
-
return {
|
|
221
|
-
...source,
|
|
222
|
-
value: {
|
|
223
|
-
...value,
|
|
224
|
-
acknowledgements: value.acknowledgements.map(item => ({
|
|
225
|
-
...record(item, 'acknowledgement'),
|
|
226
|
-
principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
|
|
227
|
-
})),
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
return item;
|
|
232
|
-
}
|
|
233
|
-
function restoreBackupBasePrincipals(source, base) {
|
|
234
|
-
if (base.kind === 'principal-binding') {
|
|
235
|
-
const value = exactRecord(source.value, 'value', [
|
|
236
|
-
'boundAt', 'memberId', 'principalId', 'projectId',
|
|
237
|
-
]);
|
|
238
|
-
return {
|
|
239
|
-
...base,
|
|
240
|
-
value: { ...base.value, principalId: principal(value, 'principalId') },
|
|
241
|
-
};
|
|
242
|
-
}
|
|
211
|
+
function validateBackupBasePrincipals(base) {
|
|
212
|
+
if (base.kind === 'principal-binding')
|
|
213
|
+
principal(base.value, 'principalId');
|
|
243
214
|
if (base.kind === 'terminal-responder') {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'
|
|
248
|
-
'operation',
|
|
249
|
-
'operationId',
|
|
250
|
-
'projectId',
|
|
251
|
-
'responseJson',
|
|
252
|
-
]);
|
|
253
|
-
const sourceAcknowledgements = value.acknowledgements;
|
|
254
|
-
if (!Array.isArray(sourceAcknowledgements)
|
|
255
|
-
|| sourceAcknowledgements.length !== base.value.acknowledgements.length)
|
|
256
|
-
throw invalidPayload('acknowledgements');
|
|
257
|
-
const acknowledgements = base.value.acknowledgements.map((item, index) => {
|
|
258
|
-
const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
|
|
259
|
-
'acknowledgedAt', 'memberId', 'principalId',
|
|
260
|
-
]);
|
|
261
|
-
return { ...item, principalId: principal(original, 'principalId') };
|
|
262
|
-
});
|
|
263
|
-
if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
|
|
264
|
-
throw invalidPayload('acknowledgements');
|
|
215
|
+
for (const acknowledgement of base.value.acknowledgements) {
|
|
216
|
+
if (acknowledgement.principalId === undefined)
|
|
217
|
+
throw invalidPayload('acknowledgement');
|
|
218
|
+
principal(acknowledgement, 'principalId');
|
|
265
219
|
}
|
|
266
|
-
|
|
220
|
+
if (new Set(base.value.acknowledgements.map(item => item.principalId)).size
|
|
221
|
+
!== base.value.acknowledgements.length)
|
|
222
|
+
throw invalidPayload('acknowledgements');
|
|
267
223
|
}
|
|
268
|
-
return base;
|
|
269
224
|
}
|
|
270
225
|
function repositoryPublicationRef(value) {
|
|
271
226
|
const source = exactRecord(value, 'ref', ['name', 'oid']);
|
|
@@ -1183,6 +1138,41 @@ function membershipRecoveryRecord(source, recordId, revision) {
|
|
|
1183
1138
|
},
|
|
1184
1139
|
};
|
|
1185
1140
|
}
|
|
1141
|
+
function membershipIdempotencyTombstoneRecord(source, recordId, revision) {
|
|
1142
|
+
const value = exactRecord(source.value, 'value', [
|
|
1143
|
+
'actorMemberId',
|
|
1144
|
+
'compactedAt',
|
|
1145
|
+
'idempotencyKey',
|
|
1146
|
+
'operation',
|
|
1147
|
+
'projectId',
|
|
1148
|
+
'requestFingerprint',
|
|
1149
|
+
]);
|
|
1150
|
+
const actorMemberId = token(value, 'actorMemberId', CollabValidation_1.isCollabMemberId);
|
|
1151
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1152
|
+
const operation = literal(value, 'operation', [
|
|
1153
|
+
'createManagerResponsibilityOffer',
|
|
1154
|
+
'acknowledgeManagerResponsibility',
|
|
1155
|
+
'declineManagerResponsibility',
|
|
1156
|
+
'cancelManagerResponsibilityOffer',
|
|
1157
|
+
'promoteManager',
|
|
1158
|
+
]);
|
|
1159
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1160
|
+
throw invalidPayload('recordId');
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
kind: 'membership-idempotency-tombstone',
|
|
1164
|
+
recordId,
|
|
1165
|
+
revision,
|
|
1166
|
+
value: {
|
|
1167
|
+
actorMemberId,
|
|
1168
|
+
compactedAt: timestamp(value, 'compactedAt'),
|
|
1169
|
+
idempotencyKey,
|
|
1170
|
+
operation,
|
|
1171
|
+
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
1172
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1173
|
+
},
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1186
1176
|
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1187
1177
|
const value = exactRecord(source.value, 'value', [
|
|
1188
1178
|
'actorMemberId',
|
|
@@ -1240,6 +1230,8 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
1240
1230
|
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1241
1231
|
case 'manager-responsibility-offer':
|
|
1242
1232
|
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1233
|
+
case 'membership-idempotency-tombstone':
|
|
1234
|
+
return membershipIdempotencyTombstoneRecord(source, recordId, revision);
|
|
1243
1235
|
case 'project-membership-recovery':
|
|
1244
1236
|
return membershipRecoveryRecord(source, recordId, revision);
|
|
1245
1237
|
case 'secret-replay-tombstone':
|
|
@@ -1409,6 +1401,7 @@ function validateContinuity(records) {
|
|
|
1409
1401
|
item,
|
|
1410
1402
|
]));
|
|
1411
1403
|
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1404
|
+
const membershipIdempotencyTombstones = records.filter((item) => (item.kind === 'membership-idempotency-tombstone'));
|
|
1412
1405
|
const membershipRecoveries = new Map(records
|
|
1413
1406
|
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1414
1407
|
.map(item => [item.value.operationId, item]));
|
|
@@ -1792,6 +1785,18 @@ function validateContinuity(records) {
|
|
|
1792
1785
|
throw invalidPayload('records');
|
|
1793
1786
|
}
|
|
1794
1787
|
}
|
|
1788
|
+
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1789
|
+
const exactResult = records.some(item => (item.kind === 'idempotency-result'
|
|
1790
|
+
&& item.value.memberId === tombstone.value.actorMemberId
|
|
1791
|
+
&& item.value.operation === tombstone.value.operation
|
|
1792
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1793
|
+
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1794
|
+
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1795
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1796
|
+
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1797
|
+
throw invalidPayload('records');
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1795
1800
|
for (const item of records) {
|
|
1796
1801
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1797
1802
|
throw invalidPayload('records');
|
|
@@ -1959,55 +1964,18 @@ function validateContinuity(records) {
|
|
|
1959
1964
|
}
|
|
1960
1965
|
}
|
|
1961
1966
|
}
|
|
1962
|
-
function
|
|
1967
|
+
function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
1963
1968
|
const source = record(value, 'manifest');
|
|
1964
1969
|
if (source.profile !== 'backup'
|
|
1965
1970
|
|| source.coordinationFormatVersion !== exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
|
|
1966
1971
|
throw invalidPayload('manifest');
|
|
1967
|
-
return (0, CollabProjectCheckpoint_1.
|
|
1968
|
-
...source,
|
|
1969
|
-
coordinationFormatVersion: CollabProjectCheckpoint_1.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
1970
|
-
});
|
|
1971
|
-
}
|
|
1972
|
-
function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
1973
|
-
const decoded = manifestWithFormatOne(value);
|
|
1974
|
-
return {
|
|
1975
|
-
artifacts: decoded.artifacts,
|
|
1976
|
-
coordinationFormatVersion: exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
|
|
1977
|
-
createdAt: decoded.createdAt,
|
|
1978
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
1979
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
1980
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
1981
|
-
manifestSha256: decoded.manifestSha256,
|
|
1982
|
-
operationId: decoded.operationId,
|
|
1983
|
-
profile: 'backup',
|
|
1984
|
-
projectId: decoded.projectId,
|
|
1985
|
-
protocolVersion: decoded.protocolVersion,
|
|
1986
|
-
refs: decoded.refs,
|
|
1987
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
1988
|
-
targetAuthority: decoded.targetAuthority,
|
|
1989
|
-
};
|
|
1972
|
+
return (0, CollabProjectCheckpoint_1.decodeCheckpointManifestFields)(source, exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION);
|
|
1990
1973
|
}
|
|
1991
1974
|
function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
|
|
1992
1975
|
return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
1993
1976
|
}
|
|
1994
1977
|
function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
|
|
1995
|
-
|
|
1996
|
-
return JSON.stringify({
|
|
1997
|
-
artifacts: decoded.artifacts,
|
|
1998
|
-
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
1999
|
-
createdAt: decoded.createdAt,
|
|
2000
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
2001
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
2002
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
2003
|
-
operationId: decoded.operationId,
|
|
2004
|
-
profile: decoded.profile,
|
|
2005
|
-
projectId: decoded.projectId,
|
|
2006
|
-
protocolVersion: decoded.protocolVersion,
|
|
2007
|
-
refs: decoded.refs,
|
|
2008
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
2009
|
-
targetAuthority: decoded.targetAuthority,
|
|
2010
|
-
});
|
|
1978
|
+
return (0, CollabProjectCheckpoint_1.encodeCheckpointManifestDigestInput)(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
2011
1979
|
}
|
|
2012
1980
|
function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
2013
1981
|
if (typeof value !== 'string'
|
|
@@ -2027,22 +1995,43 @@ function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
|
2027
1995
|
}
|
|
2028
1996
|
if (JSON.stringify(item) !== line)
|
|
2029
1997
|
throw invalidPayload('coordination');
|
|
2030
|
-
return {
|
|
1998
|
+
return {
|
|
1999
|
+
envelope: recordEnvelope(item),
|
|
2000
|
+
item,
|
|
2001
|
+
base: undefined,
|
|
2002
|
+
};
|
|
2031
2003
|
});
|
|
2032
2004
|
const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2005
|
+
for (const { envelope } of baseItems) {
|
|
2006
|
+
const value = record(envelope.source.value, 'value');
|
|
2007
|
+
if (envelope.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
|
|
2008
|
+
for (const acknowledgement of value.acknowledgements)
|
|
2009
|
+
record(acknowledgement, 'acknowledgement');
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const baseRecords = [];
|
|
2013
|
+
for (const entry of baseItems) {
|
|
2014
|
+
entry.base = (0, CollabProjectCheckpoint_1.decodeCheckpointRecord)(entry.item, 'deferred');
|
|
2015
|
+
if (JSON.stringify(entry.base) !== JSON.stringify(entry.item)
|
|
2016
|
+
|| (entry.base.kind === 'principal-binding' && entry.base.value.principalId === undefined)) {
|
|
2017
|
+
throw invalidPayload('coordination');
|
|
2018
|
+
}
|
|
2019
|
+
baseRecords.push(entry.base);
|
|
2020
|
+
}
|
|
2021
|
+
if (baseRecords.length === 0)
|
|
2022
|
+
throw invalidPayload('coordination');
|
|
2023
|
+
(0, CollabProjectCheckpoint_1.validateCheckpointRecordSequence)(baseRecords, 'backup', 'member');
|
|
2024
|
+
const decoded = parsed.map(({ envelope, base }) => {
|
|
2036
2025
|
if (envelope.kind === 'idempotency-result') {
|
|
2037
2026
|
return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
|
|
2038
2027
|
}
|
|
2039
2028
|
if (CONTINUITY_KIND_SET.has(envelope.kind)) {
|
|
2040
2029
|
return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
|
|
2041
2030
|
}
|
|
2042
|
-
const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
|
|
2043
2031
|
if (base === undefined)
|
|
2044
2032
|
throw invalidPayload('records');
|
|
2045
|
-
|
|
2033
|
+
validateBackupBasePrincipals(base);
|
|
2034
|
+
return base;
|
|
2046
2035
|
});
|
|
2047
2036
|
if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
|
|
2048
2037
|
throw invalidPayload('coordination');
|
|
@@ -2069,6 +2058,9 @@ function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
|
2069
2058
|
if (item.kind === 'secret-replay-tombstone') {
|
|
2070
2059
|
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2071
2060
|
}
|
|
2061
|
+
if (item.kind === 'membership-idempotency-tombstone') {
|
|
2062
|
+
return Date.parse(item.value.compactedAt) > captureTime;
|
|
2063
|
+
}
|
|
2072
2064
|
if (item.kind === 'project-invitation') {
|
|
2073
2065
|
return captureTime < Date.parse(item.value.createdAt)
|
|
2074
2066
|
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
@@ -2103,9 +2095,7 @@ function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
|
2103
2095
|
}))
|
|
2104
2096
|
throw invalidPayload('records');
|
|
2105
2097
|
const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
|
|
2106
|
-
(0, CollabProjectCheckpoint_1.
|
|
2107
|
-
|
|
2108
|
-
coordinationFormatVersion: CollabProjectCheckpoint_1.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
2109
|
-
}), baseRecords);
|
|
2098
|
+
(0, CollabProjectCheckpoint_1.validateCheckpointRecordSequence)(baseRecords, 'backup');
|
|
2099
|
+
(0, CollabProjectCheckpoint_1.validateCheckpointManifestConsistency)(decodedManifest, baseRecords);
|
|
2110
2100
|
return records;
|
|
2111
2101
|
}
|
|
@@ -265,11 +265,36 @@ export type CollabCheckpointAuthorityVolumePairRecord = CollabCheckpointRecordBa
|
|
|
265
265
|
}>;
|
|
266
266
|
export type CollabCheckpointPortableRecord = CollabCheckpointProjectRecord | CollabCheckpointMemberRecord | CollabCheckpointRequestRecord | CollabCheckpointRequestCommentRecord | CollabCheckpointTicketRecord | CollabCheckpointTicketCommentRecord | CollabCheckpointTicketRelationRecord | CollabCheckpointTicketMentionRecord;
|
|
267
267
|
export type CollabCheckpointBackupRecord = CollabCheckpointPortableRecord | CollabCheckpointCloudEventRecord | CollabCheckpointCloudEventCursorRecord | CollabCheckpointIdempotencyResultRecord | CollabCheckpointPrincipalBindingRecord | CollabCheckpointRepositoryPlacementRecord | CollabCheckpointLifecycleStateRecord | CollabCheckpointTerminalResponderRecord | CollabCheckpointProtectedClaimEnvelopeRecord | CollabCheckpointTombstoneRecord | CollabCheckpointSchemaCatalogRecord | CollabCheckpointServerCompatibilityRecord | CollabCheckpointAuthorityVolumePairRecord;
|
|
268
|
+
type ParsedCheckpointPrincipalBindingRecord = CollabCheckpointRecordBase<'principal-binding', Omit<CollabCheckpointPrincipalBindingRecord['value'], 'principalId'> & {
|
|
269
|
+
readonly principalId: unknown;
|
|
270
|
+
}>;
|
|
271
|
+
type ParsedCheckpointTerminalResponderRecord = CollabCheckpointRecordBase<'terminal-responder', Omit<CollabCheckpointTerminalResponderRecord['value'], 'acknowledgements'> & {
|
|
272
|
+
readonly acknowledgements: readonly (Omit<CollabCheckpointTerminalAcknowledgement, 'principalId'> & {
|
|
273
|
+
readonly principalId: unknown;
|
|
274
|
+
})[];
|
|
275
|
+
}>;
|
|
276
|
+
/** @internal Format v3 validates principal fields after the shared base-record phase. */
|
|
277
|
+
export type ParsedCheckpointRecord = Exclude<CollabCheckpointBackupRecord, CollabCheckpointPrincipalBindingRecord | CollabCheckpointTerminalResponderRecord> | ParsedCheckpointPrincipalBindingRecord | ParsedCheckpointTerminalResponderRecord;
|
|
278
|
+
type CheckpointManifestFields<CoordinationVersion extends number> = Omit<CollabProjectCheckpointManifest, 'coordinationFormatVersion'> & {
|
|
279
|
+
readonly coordinationFormatVersion: CoordinationVersion;
|
|
280
|
+
};
|
|
281
|
+
/** @internal Shared field validation; public entry points select their own format. */
|
|
282
|
+
export declare function decodeCheckpointManifestFields<CoordinationVersion extends number>(value: unknown, coordinationFormatVersion: CoordinationVersion): CheckpointManifestFields<CoordinationVersion>;
|
|
268
283
|
export declare function decodeCollabProjectCheckpointManifest(value: unknown): CollabProjectCheckpointManifest;
|
|
269
284
|
export declare function encodeCollabProjectCheckpointManifestCanonicalJson(manifest: CollabProjectCheckpointManifest): string;
|
|
270
285
|
export declare function encodeCollabProjectCheckpointManifestDigestInput(manifest: CollabProjectCheckpointManifest): string;
|
|
286
|
+
/** @internal Encode already validated fields without changing their format. */
|
|
287
|
+
export declare function encodeCheckpointManifestDigestInput(decoded: CheckpointManifestFields<number>): string;
|
|
271
288
|
export declare function encodeCollabProtectedClaimAssociatedData(associatedData: CollabProtectedClaimAssociatedData): string;
|
|
289
|
+
/** @internal Public v1 decoding validates principals in each record. */
|
|
290
|
+
export declare function decodeCheckpointRecord(value: unknown): CollabCheckpointBackupRecord;
|
|
291
|
+
/** @internal Offline v3 decoding validates principals after the base-record sequence. */
|
|
292
|
+
export declare function decodeCheckpointRecord(value: unknown, principalPhase: 'deferred'): ParsedCheckpointRecord;
|
|
293
|
+
/** @internal V3 decoding checks terminal principals separately; consistency binds current principals. */
|
|
294
|
+
export declare function validateCheckpointRecordSequence(records: readonly ParsedCheckpointRecord[], profile: CollabCheckpointProfile, acknowledgementBinding?: 'current-principal' | 'member'): void;
|
|
272
295
|
export declare function decodeCollabProjectCheckpointCoordinationNdjson(value: string, profile: CollabCheckpointProfile): readonly CollabCheckpointBackupRecord[];
|
|
273
296
|
export declare function encodeCollabProjectCheckpointCoordinationNdjson(records: readonly CollabCheckpointBackupRecord[], profile: CollabCheckpointProfile): string;
|
|
274
297
|
export declare function validateCollabProjectCheckpointConsistency(manifest: CollabProjectCheckpointManifest, records: readonly CollabCheckpointBackupRecord[]): readonly CollabCheckpointBackupRecord[];
|
|
298
|
+
/** @internal Cross-artifact checks operate on validated fields, not another format. */
|
|
299
|
+
export declare function validateCheckpointManifestConsistency(decodedManifest: CheckpointManifestFields<number>, records: readonly CollabCheckpointBackupRecord[]): readonly CollabCheckpointBackupRecord[];
|
|
275
300
|
export {};
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.COLLAB_CHECKPOINT_BACKUP_RECORD_KINDS = exports.COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS = exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS = exports.COLLAB_PROJECT_CHECKPOINT_ARTIFACTS = exports.COLLAB_CHECKPOINT_PROFILES = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_LIMITS = exports.COLLAB_PROTECTED_CLAIM_ENVELOPE_VERSION = exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION = exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.decodeCheckpointManifestFields = decodeCheckpointManifestFields;
|
|
4
5
|
exports.decodeCollabProjectCheckpointManifest = decodeCollabProjectCheckpointManifest;
|
|
5
6
|
exports.encodeCollabProjectCheckpointManifestCanonicalJson = encodeCollabProjectCheckpointManifestCanonicalJson;
|
|
6
7
|
exports.encodeCollabProjectCheckpointManifestDigestInput = encodeCollabProjectCheckpointManifestDigestInput;
|
|
8
|
+
exports.encodeCheckpointManifestDigestInput = encodeCheckpointManifestDigestInput;
|
|
7
9
|
exports.encodeCollabProtectedClaimAssociatedData = encodeCollabProtectedClaimAssociatedData;
|
|
10
|
+
exports.decodeCheckpointRecord = decodeCheckpointRecord;
|
|
11
|
+
exports.validateCheckpointRecordSequence = validateCheckpointRecordSequence;
|
|
8
12
|
exports.decodeCollabProjectCheckpointCoordinationNdjson = decodeCollabProjectCheckpointCoordinationNdjson;
|
|
9
13
|
exports.encodeCollabProjectCheckpointCoordinationNdjson = encodeCollabProjectCheckpointCoordinationNdjson;
|
|
10
14
|
exports.validateCollabProjectCheckpointConsistency = validateCollabProjectCheckpointConsistency;
|
|
15
|
+
exports.validateCheckpointManifestConsistency = validateCheckpointManifestConsistency;
|
|
11
16
|
const CollabAuthorityTransfer_1 = require("./CollabAuthorityTransfer");
|
|
12
17
|
const CollabCloudProjectEvent_1 = require("./CollabCloudProjectEvent");
|
|
13
18
|
const CollabConstants_1 = require("./CollabConstants");
|
|
@@ -237,7 +242,7 @@ function gitRefs(value, expectedMainOid) {
|
|
|
237
242
|
throw invalidPayload('refs');
|
|
238
243
|
return Object.freeze(decoded);
|
|
239
244
|
}
|
|
240
|
-
function manifestObject(source,
|
|
245
|
+
function manifestObject(source, coordinationFormatVersion) {
|
|
241
246
|
const profile = literal(source, 'profile', exports.COLLAB_CHECKPOINT_PROFILES);
|
|
242
247
|
const expectedMainOid = token(source, 'expectedMainOid', CollabValidation_1.isCollabGitOid);
|
|
243
248
|
const gitObjectFormat = literal(source, 'gitObjectFormat', ['sha1', 'sha256']);
|
|
@@ -273,15 +278,13 @@ function manifestObject(source, includeManifestSha256) {
|
|
|
273
278
|
sourceAuthority,
|
|
274
279
|
targetAuthority,
|
|
275
280
|
};
|
|
276
|
-
if (common.coordinationFormatVersion !==
|
|
281
|
+
if (common.coordinationFormatVersion !== coordinationFormatVersion
|
|
277
282
|
|| common.manifestSchemaVersion !== exports.COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION
|
|
278
283
|
|| common.protocolVersion !== CollabConstants_1.COLLAB_PROTOCOL_VERSION)
|
|
279
284
|
throw invalidPayload('manifest');
|
|
280
|
-
if (!includeManifestSha256)
|
|
281
|
-
return common;
|
|
282
285
|
return {
|
|
283
286
|
artifacts: common.artifacts,
|
|
284
|
-
coordinationFormatVersion
|
|
287
|
+
coordinationFormatVersion,
|
|
285
288
|
createdAt: common.createdAt,
|
|
286
289
|
expectedMainOid: common.expectedMainOid,
|
|
287
290
|
gitObjectFormat: common.gitObjectFormat,
|
|
@@ -296,7 +299,8 @@ function manifestObject(source, includeManifestSha256) {
|
|
|
296
299
|
targetAuthority: common.targetAuthority,
|
|
297
300
|
};
|
|
298
301
|
}
|
|
299
|
-
|
|
302
|
+
/** @internal Shared field validation; public entry points select their own format. */
|
|
303
|
+
function decodeCheckpointManifestFields(value, coordinationFormatVersion) {
|
|
300
304
|
const source = exactRecord(value, 'manifest', [
|
|
301
305
|
'artifacts',
|
|
302
306
|
'coordinationFormatVersion',
|
|
@@ -313,18 +317,24 @@ function decodeCollabProjectCheckpointManifest(value) {
|
|
|
313
317
|
'sourceAuthority',
|
|
314
318
|
'targetAuthority',
|
|
315
319
|
]);
|
|
316
|
-
const decoded = manifestObject(source,
|
|
320
|
+
const decoded = manifestObject(source, coordinationFormatVersion);
|
|
317
321
|
const encoded = JSON.stringify(decoded);
|
|
318
322
|
if (!(0, CollabValidation_1.hasUtf8ByteLengthAtMost)(encoded, exports.COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes)) {
|
|
319
323
|
throw invalidPayload('manifest');
|
|
320
324
|
}
|
|
321
325
|
return decoded;
|
|
322
326
|
}
|
|
327
|
+
function decodeCollabProjectCheckpointManifest(value) {
|
|
328
|
+
return decodeCheckpointManifestFields(value, exports.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION);
|
|
329
|
+
}
|
|
323
330
|
function encodeCollabProjectCheckpointManifestCanonicalJson(manifest) {
|
|
324
331
|
return JSON.stringify(decodeCollabProjectCheckpointManifest(manifest));
|
|
325
332
|
}
|
|
326
333
|
function encodeCollabProjectCheckpointManifestDigestInput(manifest) {
|
|
327
|
-
|
|
334
|
+
return encodeCheckpointManifestDigestInput(decodeCollabProjectCheckpointManifest(manifest));
|
|
335
|
+
}
|
|
336
|
+
/** @internal Encode already validated fields without changing their format. */
|
|
337
|
+
function encodeCheckpointManifestDigestInput(decoded) {
|
|
328
338
|
const source = {
|
|
329
339
|
artifacts: decoded.artifacts,
|
|
330
340
|
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
@@ -707,13 +717,13 @@ function idempotencyResultRecord(source, recordId, revision) {
|
|
|
707
717
|
},
|
|
708
718
|
};
|
|
709
719
|
}
|
|
710
|
-
function principalBindingRecord(source, recordId, revision) {
|
|
711
|
-
const value =
|
|
720
|
+
function principalBindingRecord(source, recordId, revision, deferPrincipals) {
|
|
721
|
+
const value = principalRecord(source.value, 'value', [
|
|
712
722
|
'boundAt',
|
|
713
723
|
'memberId',
|
|
714
724
|
'principalId',
|
|
715
725
|
'projectId',
|
|
716
|
-
]);
|
|
726
|
+
], deferPrincipals);
|
|
717
727
|
const memberId = token(value, 'memberId', CollabValidation_1.isCollabMemberId);
|
|
718
728
|
if (recordId !== memberId)
|
|
719
729
|
throw invalidPayload('recordId');
|
|
@@ -724,7 +734,7 @@ function principalBindingRecord(source, recordId, revision) {
|
|
|
724
734
|
value: {
|
|
725
735
|
boundAt: timestamp(value, 'boundAt'),
|
|
726
736
|
memberId,
|
|
727
|
-
principalId: token(value, 'principalId'),
|
|
737
|
+
principalId: deferPrincipals ? value.principalId : token(value, 'principalId'),
|
|
728
738
|
projectId: token(value, 'projectId', CollabValidation_1.isCollabProjectId),
|
|
729
739
|
},
|
|
730
740
|
};
|
|
@@ -858,7 +868,13 @@ function lifecycleStateRecord(source, recordId, revision) {
|
|
|
858
868
|
value: internalState,
|
|
859
869
|
};
|
|
860
870
|
}
|
|
861
|
-
function
|
|
871
|
+
function principalRecord(value, field, keys, deferPrincipals) {
|
|
872
|
+
const source = record(value, field);
|
|
873
|
+
return exactRecord(source, field, deferPrincipals && !Object.hasOwn(source, 'principalId')
|
|
874
|
+
? keys.filter(key => key !== 'principalId')
|
|
875
|
+
: keys);
|
|
876
|
+
}
|
|
877
|
+
function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
|
|
862
878
|
const value = exactRecord(source.value, 'value', [
|
|
863
879
|
'acknowledgements',
|
|
864
880
|
'eligibleMemberIds',
|
|
@@ -910,22 +926,24 @@ function terminalResponderRecord(source, recordId, revision) {
|
|
|
910
926
|
if (!Array.isArray(value.acknowledgements))
|
|
911
927
|
throw invalidPayload('acknowledgements');
|
|
912
928
|
const acknowledgements = value.acknowledgements.map((item) => {
|
|
913
|
-
const acknowledgement =
|
|
929
|
+
const acknowledgement = principalRecord(item, 'acknowledgement', [
|
|
914
930
|
'acknowledgedAt',
|
|
915
931
|
'memberId',
|
|
916
932
|
'principalId',
|
|
917
|
-
]);
|
|
933
|
+
], deferPrincipals);
|
|
918
934
|
return {
|
|
919
935
|
acknowledgedAt: timestamp(acknowledgement, 'acknowledgedAt'),
|
|
920
936
|
memberId: token(acknowledgement, 'memberId', CollabValidation_1.isCollabMemberId),
|
|
921
|
-
principalId:
|
|
937
|
+
principalId: deferPrincipals
|
|
938
|
+
? acknowledgement.principalId
|
|
939
|
+
: token(acknowledgement, 'principalId'),
|
|
922
940
|
};
|
|
923
941
|
});
|
|
924
942
|
const acknowledgementPrincipals = new Set();
|
|
925
943
|
acknowledgements.forEach((item, index) => {
|
|
926
944
|
if (!eligibleMemberIds.includes(item.memberId)
|
|
927
945
|
|| (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
|
|
928
|
-
|| acknowledgementPrincipals.has(item.principalId))
|
|
946
|
+
|| (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
|
|
929
947
|
throw invalidPayload('acknowledgements');
|
|
930
948
|
acknowledgementPrincipals.add(item.principalId);
|
|
931
949
|
});
|
|
@@ -934,7 +952,7 @@ function terminalResponderRecord(source, recordId, revision) {
|
|
|
934
952
|
recordId,
|
|
935
953
|
revision,
|
|
936
954
|
value: {
|
|
937
|
-
acknowledgements
|
|
955
|
+
acknowledgements,
|
|
938
956
|
eligibleMemberIds: Object.freeze(eligibleMemberIds),
|
|
939
957
|
expiresAt,
|
|
940
958
|
operation,
|
|
@@ -1107,8 +1125,9 @@ function authorityVolumePairRecord(source, recordId, revision) {
|
|
|
1107
1125
|
},
|
|
1108
1126
|
};
|
|
1109
1127
|
}
|
|
1110
|
-
function decodeCheckpointRecord(value) {
|
|
1128
|
+
function decodeCheckpointRecord(value, principalPhase) {
|
|
1111
1129
|
const { kind, recordId, revision, source } = recordEnvelope(value);
|
|
1130
|
+
const deferPrincipals = principalPhase === 'deferred';
|
|
1112
1131
|
switch (kind) {
|
|
1113
1132
|
case 'project': return projectRecord(source, recordId, revision);
|
|
1114
1133
|
case 'member': return memberRecord(source, recordId, revision);
|
|
@@ -1121,10 +1140,12 @@ function decodeCheckpointRecord(value) {
|
|
|
1121
1140
|
case 'cloud-event': return cloudEventRecord(source, recordId, revision);
|
|
1122
1141
|
case 'cloud-event-cursor': return cloudEventCursorRecord(source, recordId, revision);
|
|
1123
1142
|
case 'idempotency-result': return idempotencyResultRecord(source, recordId, revision);
|
|
1124
|
-
case 'principal-binding':
|
|
1143
|
+
case 'principal-binding':
|
|
1144
|
+
return principalBindingRecord(source, recordId, revision, deferPrincipals);
|
|
1125
1145
|
case 'repository-placement': return repositoryPlacementRecord(source, recordId, revision);
|
|
1126
1146
|
case 'lifecycle-state': return lifecycleStateRecord(source, recordId, revision);
|
|
1127
|
-
case 'terminal-responder':
|
|
1147
|
+
case 'terminal-responder':
|
|
1148
|
+
return terminalResponderRecord(source, recordId, revision, deferPrincipals);
|
|
1128
1149
|
case 'protected-claim-envelope':
|
|
1129
1150
|
return protectedClaimEnvelopeRecord(source, recordId, revision);
|
|
1130
1151
|
case 'tombstone': return tombstoneRecord(source, recordId, revision);
|
|
@@ -1140,7 +1161,8 @@ function compareRecords(left, right) {
|
|
|
1140
1161
|
const kindOrder = recordKindOrder(left.kind) - recordKindOrder(right.kind);
|
|
1141
1162
|
return kindOrder !== 0 ? kindOrder : left.recordId.localeCompare(right.recordId, 'en-US');
|
|
1142
1163
|
}
|
|
1143
|
-
|
|
1164
|
+
/** @internal V3 decoding checks terminal principals separately; consistency binds current principals. */
|
|
1165
|
+
function validateCheckpointRecordSequence(records, profile, acknowledgementBinding = 'current-principal') {
|
|
1144
1166
|
if (records.length === 0 || records[0].kind !== 'project')
|
|
1145
1167
|
throw invalidPayload('records');
|
|
1146
1168
|
const projectId = records[0].recordId;
|
|
@@ -1243,7 +1265,9 @@ function validateRecordSequence(records, profile) {
|
|
|
1243
1265
|
throw invalidPayload('records');
|
|
1244
1266
|
}
|
|
1245
1267
|
if (item.kind === 'terminal-responder' && (item.value.eligibleMemberIds.some(memberId => !members.has(memberId))
|
|
1246
|
-
|| item.value.acknowledgements.some(acknowledgement => (
|
|
1268
|
+
|| item.value.acknowledgements.some(acknowledgement => (acknowledgementBinding === 'current-principal'
|
|
1269
|
+
? principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId
|
|
1270
|
+
: !principalBindings.has(acknowledgement.memberId)))))
|
|
1247
1271
|
throw invalidPayload('records');
|
|
1248
1272
|
}
|
|
1249
1273
|
if (profile === 'backup') {
|
|
@@ -1270,11 +1294,13 @@ function decodeCollabProjectCheckpointCoordinationNdjson(value, profile) {
|
|
|
1270
1294
|
throw invalidPayload('coordination');
|
|
1271
1295
|
}
|
|
1272
1296
|
const result = decodeCheckpointRecord(parsed);
|
|
1297
|
+
if (result.kind === 'terminal-responder')
|
|
1298
|
+
Object.freeze(result.value.acknowledgements);
|
|
1273
1299
|
if (JSON.stringify(result) !== line)
|
|
1274
1300
|
throw invalidPayload('coordination');
|
|
1275
1301
|
return result;
|
|
1276
1302
|
});
|
|
1277
|
-
|
|
1303
|
+
validateCheckpointRecordSequence(decoded, profile);
|
|
1278
1304
|
return Object.freeze(decoded);
|
|
1279
1305
|
}
|
|
1280
1306
|
function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
|
|
@@ -1306,7 +1332,11 @@ function checkpointRecordGitOids(checkpointRecord) {
|
|
|
1306
1332
|
}
|
|
1307
1333
|
function validateCollabProjectCheckpointConsistency(manifest, records) {
|
|
1308
1334
|
const decodedManifest = decodeCollabProjectCheckpointManifest(manifest);
|
|
1309
|
-
|
|
1335
|
+
validateCheckpointRecordSequence(records, decodedManifest.profile);
|
|
1336
|
+
return validateCheckpointManifestConsistency(decodedManifest, records);
|
|
1337
|
+
}
|
|
1338
|
+
/** @internal Cross-artifact checks operate on validated fields, not another format. */
|
|
1339
|
+
function validateCheckpointManifestConsistency(decodedManifest, records) {
|
|
1310
1340
|
const project = records[0];
|
|
1311
1341
|
if (project.kind !== 'project'
|
|
1312
1342
|
|| project.value.projectId !== decodedManifest.projectId
|
|
@@ -2,7 +2,7 @@ import { COLLAB_AUTHORITY_TRANSFER_CANCELLATION_PHASES, COLLAB_CLOUD_TO_LAN_TRAN
|
|
|
2
2
|
import { COLLAB_MAIN_REF, COLLAB_MEMBER_REF_PREFIX, } from './CollabConstants.mjs';
|
|
3
3
|
import { collabControlOperationCodec, } from './CollabControlOperationCodecs.mjs';
|
|
4
4
|
import { CollabError } from './CollabError.mjs';
|
|
5
|
-
import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS,
|
|
5
|
+
import { COLLAB_CHECKPOINT_ARTIFACT_LIMITS, COLLAB_CHECKPOINT_PORTABLE_RECORD_KINDS, decodeCheckpointManifestFields, decodeCheckpointRecord, encodeCheckpointManifestDigestInput, validateCheckpointManifestConsistency, validateCheckpointRecordSequence, } from './CollabProjectCheckpoint.mjs';
|
|
6
6
|
import { COLLAB_PROJECT_MEMBERSHIP_LIMITS, } from './CollabProjectMembership.mjs';
|
|
7
7
|
import { hasUtf8ByteLengthAtMost, isCollabGitOid, isCollabMemberId, isCollabOpaqueId, isCollabProjectId, } from './CollabValidation.mjs';
|
|
8
8
|
export const COLLAB_PROJECT_BACKUP_COMPATIBILITY_STAGE = 'pre-production-replaceable';
|
|
@@ -22,6 +22,7 @@ const BACKUP_CONTINUITY_RECORD_KINDS = Object.freeze([
|
|
|
22
22
|
'transferred-membership-claim-override',
|
|
23
23
|
'protected-claim-override-envelope',
|
|
24
24
|
'manager-responsibility-offer',
|
|
25
|
+
'membership-idempotency-tombstone',
|
|
25
26
|
'project-membership-recovery',
|
|
26
27
|
'secret-replay-tombstone',
|
|
27
28
|
]);
|
|
@@ -197,65 +198,19 @@ function absoluteTargetUrl(source) {
|
|
|
197
198
|
function principal(source, field) {
|
|
198
199
|
return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
|
|
199
200
|
}
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (source.kind === 'principal-binding') {
|
|
204
|
-
return {
|
|
205
|
-
...source,
|
|
206
|
-
value: { ...value, principalId: `principal_${String(value.memberId)}` },
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
|
|
210
|
-
return {
|
|
211
|
-
...source,
|
|
212
|
-
value: {
|
|
213
|
-
...value,
|
|
214
|
-
acknowledgements: value.acknowledgements.map(item => ({
|
|
215
|
-
...record(item, 'acknowledgement'),
|
|
216
|
-
principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
|
|
217
|
-
})),
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return item;
|
|
222
|
-
}
|
|
223
|
-
function restoreBackupBasePrincipals(source, base) {
|
|
224
|
-
if (base.kind === 'principal-binding') {
|
|
225
|
-
const value = exactRecord(source.value, 'value', [
|
|
226
|
-
'boundAt', 'memberId', 'principalId', 'projectId',
|
|
227
|
-
]);
|
|
228
|
-
return {
|
|
229
|
-
...base,
|
|
230
|
-
value: { ...base.value, principalId: principal(value, 'principalId') },
|
|
231
|
-
};
|
|
232
|
-
}
|
|
201
|
+
function validateBackupBasePrincipals(base) {
|
|
202
|
+
if (base.kind === 'principal-binding')
|
|
203
|
+
principal(base.value, 'principalId');
|
|
233
204
|
if (base.kind === 'terminal-responder') {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
'
|
|
238
|
-
'operation',
|
|
239
|
-
'operationId',
|
|
240
|
-
'projectId',
|
|
241
|
-
'responseJson',
|
|
242
|
-
]);
|
|
243
|
-
const sourceAcknowledgements = value.acknowledgements;
|
|
244
|
-
if (!Array.isArray(sourceAcknowledgements)
|
|
245
|
-
|| sourceAcknowledgements.length !== base.value.acknowledgements.length)
|
|
246
|
-
throw invalidPayload('acknowledgements');
|
|
247
|
-
const acknowledgements = base.value.acknowledgements.map((item, index) => {
|
|
248
|
-
const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
|
|
249
|
-
'acknowledgedAt', 'memberId', 'principalId',
|
|
250
|
-
]);
|
|
251
|
-
return { ...item, principalId: principal(original, 'principalId') };
|
|
252
|
-
});
|
|
253
|
-
if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
|
|
254
|
-
throw invalidPayload('acknowledgements');
|
|
205
|
+
for (const acknowledgement of base.value.acknowledgements) {
|
|
206
|
+
if (acknowledgement.principalId === undefined)
|
|
207
|
+
throw invalidPayload('acknowledgement');
|
|
208
|
+
principal(acknowledgement, 'principalId');
|
|
255
209
|
}
|
|
256
|
-
|
|
210
|
+
if (new Set(base.value.acknowledgements.map(item => item.principalId)).size
|
|
211
|
+
!== base.value.acknowledgements.length)
|
|
212
|
+
throw invalidPayload('acknowledgements');
|
|
257
213
|
}
|
|
258
|
-
return base;
|
|
259
214
|
}
|
|
260
215
|
function repositoryPublicationRef(value) {
|
|
261
216
|
const source = exactRecord(value, 'ref', ['name', 'oid']);
|
|
@@ -1173,6 +1128,41 @@ function membershipRecoveryRecord(source, recordId, revision) {
|
|
|
1173
1128
|
},
|
|
1174
1129
|
};
|
|
1175
1130
|
}
|
|
1131
|
+
function membershipIdempotencyTombstoneRecord(source, recordId, revision) {
|
|
1132
|
+
const value = exactRecord(source.value, 'value', [
|
|
1133
|
+
'actorMemberId',
|
|
1134
|
+
'compactedAt',
|
|
1135
|
+
'idempotencyKey',
|
|
1136
|
+
'operation',
|
|
1137
|
+
'projectId',
|
|
1138
|
+
'requestFingerprint',
|
|
1139
|
+
]);
|
|
1140
|
+
const actorMemberId = token(value, 'actorMemberId', isCollabMemberId);
|
|
1141
|
+
const idempotencyKey = token(value, 'idempotencyKey');
|
|
1142
|
+
const operation = literal(value, 'operation', [
|
|
1143
|
+
'createManagerResponsibilityOffer',
|
|
1144
|
+
'acknowledgeManagerResponsibility',
|
|
1145
|
+
'declineManagerResponsibility',
|
|
1146
|
+
'cancelManagerResponsibilityOffer',
|
|
1147
|
+
'promoteManager',
|
|
1148
|
+
]);
|
|
1149
|
+
if (recordId !== `${operation}:${actorMemberId}:${idempotencyKey}`) {
|
|
1150
|
+
throw invalidPayload('recordId');
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
kind: 'membership-idempotency-tombstone',
|
|
1154
|
+
recordId,
|
|
1155
|
+
revision,
|
|
1156
|
+
value: {
|
|
1157
|
+
actorMemberId,
|
|
1158
|
+
compactedAt: timestamp(value, 'compactedAt'),
|
|
1159
|
+
idempotencyKey,
|
|
1160
|
+
operation,
|
|
1161
|
+
projectId: token(value, 'projectId', isCollabProjectId),
|
|
1162
|
+
requestFingerprint: sha256(value, 'requestFingerprint'),
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1176
1166
|
function secretReplayTombstoneRecord(source, recordId, revision) {
|
|
1177
1167
|
const value = exactRecord(source.value, 'value', [
|
|
1178
1168
|
'actorMemberId',
|
|
@@ -1230,6 +1220,8 @@ function decodeContinuityRecord(kind, source, recordId, revision) {
|
|
|
1230
1220
|
return protectedClaimOverrideEnvelopeRecord(source, recordId, revision);
|
|
1231
1221
|
case 'manager-responsibility-offer':
|
|
1232
1222
|
return managerResponsibilityOfferRecord(source, recordId, revision);
|
|
1223
|
+
case 'membership-idempotency-tombstone':
|
|
1224
|
+
return membershipIdempotencyTombstoneRecord(source, recordId, revision);
|
|
1233
1225
|
case 'project-membership-recovery':
|
|
1234
1226
|
return membershipRecoveryRecord(source, recordId, revision);
|
|
1235
1227
|
case 'secret-replay-tombstone':
|
|
@@ -1399,6 +1391,7 @@ function validateContinuity(records) {
|
|
|
1399
1391
|
item,
|
|
1400
1392
|
]));
|
|
1401
1393
|
const managerOffers = records.filter((item) => (item.kind === 'manager-responsibility-offer'));
|
|
1394
|
+
const membershipIdempotencyTombstones = records.filter((item) => (item.kind === 'membership-idempotency-tombstone'));
|
|
1402
1395
|
const membershipRecoveries = new Map(records
|
|
1403
1396
|
.filter((item) => (item.kind === 'project-membership-recovery'))
|
|
1404
1397
|
.map(item => [item.value.operationId, item]));
|
|
@@ -1782,6 +1775,18 @@ function validateContinuity(records) {
|
|
|
1782
1775
|
throw invalidPayload('records');
|
|
1783
1776
|
}
|
|
1784
1777
|
}
|
|
1778
|
+
for (const tombstone of membershipIdempotencyTombstones) {
|
|
1779
|
+
const exactResult = records.some(item => (item.kind === 'idempotency-result'
|
|
1780
|
+
&& item.value.memberId === tombstone.value.actorMemberId
|
|
1781
|
+
&& item.value.operation === tombstone.value.operation
|
|
1782
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1783
|
+
const exactOffer = tombstone.value.operation === 'createManagerResponsibilityOffer'
|
|
1784
|
+
&& managerOffers.some(item => (item.value.sourceManagerMemberId === tombstone.value.actorMemberId
|
|
1785
|
+
&& item.value.idempotencyKey === tombstone.value.idempotencyKey));
|
|
1786
|
+
if (!members.has(tombstone.value.actorMemberId) || exactResult || exactOffer) {
|
|
1787
|
+
throw invalidPayload('records');
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1785
1790
|
for (const item of records) {
|
|
1786
1791
|
if (item.kind === 'idempotency-result' && !members.has(item.value.memberId)) {
|
|
1787
1792
|
throw invalidPayload('records');
|
|
@@ -1949,55 +1954,18 @@ function validateContinuity(records) {
|
|
|
1949
1954
|
}
|
|
1950
1955
|
}
|
|
1951
1956
|
}
|
|
1952
|
-
function
|
|
1957
|
+
export function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
1953
1958
|
const source = record(value, 'manifest');
|
|
1954
1959
|
if (source.profile !== 'backup'
|
|
1955
1960
|
|| source.coordinationFormatVersion !== COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
|
|
1956
1961
|
throw invalidPayload('manifest');
|
|
1957
|
-
return
|
|
1958
|
-
...source,
|
|
1959
|
-
coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
1960
|
-
});
|
|
1961
|
-
}
|
|
1962
|
-
export function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
1963
|
-
const decoded = manifestWithFormatOne(value);
|
|
1964
|
-
return {
|
|
1965
|
-
artifacts: decoded.artifacts,
|
|
1966
|
-
coordinationFormatVersion: COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
|
|
1967
|
-
createdAt: decoded.createdAt,
|
|
1968
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
1969
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
1970
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
1971
|
-
manifestSha256: decoded.manifestSha256,
|
|
1972
|
-
operationId: decoded.operationId,
|
|
1973
|
-
profile: 'backup',
|
|
1974
|
-
projectId: decoded.projectId,
|
|
1975
|
-
protocolVersion: decoded.protocolVersion,
|
|
1976
|
-
refs: decoded.refs,
|
|
1977
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
1978
|
-
targetAuthority: decoded.targetAuthority,
|
|
1979
|
-
};
|
|
1962
|
+
return decodeCheckpointManifestFields(source, COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION);
|
|
1980
1963
|
}
|
|
1981
1964
|
export function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
|
|
1982
1965
|
return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
1983
1966
|
}
|
|
1984
1967
|
export function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
|
|
1985
|
-
|
|
1986
|
-
return JSON.stringify({
|
|
1987
|
-
artifacts: decoded.artifacts,
|
|
1988
|
-
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
1989
|
-
createdAt: decoded.createdAt,
|
|
1990
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
1991
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
1992
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
1993
|
-
operationId: decoded.operationId,
|
|
1994
|
-
profile: decoded.profile,
|
|
1995
|
-
projectId: decoded.projectId,
|
|
1996
|
-
protocolVersion: decoded.protocolVersion,
|
|
1997
|
-
refs: decoded.refs,
|
|
1998
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
1999
|
-
targetAuthority: decoded.targetAuthority,
|
|
2000
|
-
});
|
|
1968
|
+
return encodeCheckpointManifestDigestInput(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
2001
1969
|
}
|
|
2002
1970
|
export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
2003
1971
|
if (typeof value !== 'string'
|
|
@@ -2017,22 +1985,43 @@ export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
|
2017
1985
|
}
|
|
2018
1986
|
if (JSON.stringify(item) !== line)
|
|
2019
1987
|
throw invalidPayload('coordination');
|
|
2020
|
-
return {
|
|
1988
|
+
return {
|
|
1989
|
+
envelope: recordEnvelope(item),
|
|
1990
|
+
item,
|
|
1991
|
+
base: undefined,
|
|
1992
|
+
};
|
|
2021
1993
|
});
|
|
2022
1994
|
const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
|
|
2023
|
-
const
|
|
2024
|
-
|
|
2025
|
-
|
|
1995
|
+
for (const { envelope } of baseItems) {
|
|
1996
|
+
const value = record(envelope.source.value, 'value');
|
|
1997
|
+
if (envelope.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
|
|
1998
|
+
for (const acknowledgement of value.acknowledgements)
|
|
1999
|
+
record(acknowledgement, 'acknowledgement');
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const baseRecords = [];
|
|
2003
|
+
for (const entry of baseItems) {
|
|
2004
|
+
entry.base = decodeCheckpointRecord(entry.item, 'deferred');
|
|
2005
|
+
if (JSON.stringify(entry.base) !== JSON.stringify(entry.item)
|
|
2006
|
+
|| (entry.base.kind === 'principal-binding' && entry.base.value.principalId === undefined)) {
|
|
2007
|
+
throw invalidPayload('coordination');
|
|
2008
|
+
}
|
|
2009
|
+
baseRecords.push(entry.base);
|
|
2010
|
+
}
|
|
2011
|
+
if (baseRecords.length === 0)
|
|
2012
|
+
throw invalidPayload('coordination');
|
|
2013
|
+
validateCheckpointRecordSequence(baseRecords, 'backup', 'member');
|
|
2014
|
+
const decoded = parsed.map(({ envelope, base }) => {
|
|
2026
2015
|
if (envelope.kind === 'idempotency-result') {
|
|
2027
2016
|
return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
|
|
2028
2017
|
}
|
|
2029
2018
|
if (CONTINUITY_KIND_SET.has(envelope.kind)) {
|
|
2030
2019
|
return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
|
|
2031
2020
|
}
|
|
2032
|
-
const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
|
|
2033
2021
|
if (base === undefined)
|
|
2034
2022
|
throw invalidPayload('records');
|
|
2035
|
-
|
|
2023
|
+
validateBackupBasePrincipals(base);
|
|
2024
|
+
return base;
|
|
2036
2025
|
});
|
|
2037
2026
|
if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
|
|
2038
2027
|
throw invalidPayload('coordination');
|
|
@@ -2059,6 +2048,9 @@ export function validateCollabProjectBackupCheckpointConsistency(manifest, recor
|
|
|
2059
2048
|
if (item.kind === 'secret-replay-tombstone') {
|
|
2060
2049
|
return Date.parse(item.value.expiredAt) > captureTime;
|
|
2061
2050
|
}
|
|
2051
|
+
if (item.kind === 'membership-idempotency-tombstone') {
|
|
2052
|
+
return Date.parse(item.value.compactedAt) > captureTime;
|
|
2053
|
+
}
|
|
2062
2054
|
if (item.kind === 'project-invitation') {
|
|
2063
2055
|
return captureTime < Date.parse(item.value.createdAt)
|
|
2064
2056
|
|| (captureTime < Date.parse(item.value.secretReplayExpiresAt))
|
|
@@ -2093,9 +2085,7 @@ export function validateCollabProjectBackupCheckpointConsistency(manifest, recor
|
|
|
2093
2085
|
}))
|
|
2094
2086
|
throw invalidPayload('records');
|
|
2095
2087
|
const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
2099
|
-
}), baseRecords);
|
|
2088
|
+
validateCheckpointRecordSequence(baseRecords, 'backup');
|
|
2089
|
+
validateCheckpointManifestConsistency(decodedManifest, baseRecords);
|
|
2100
2090
|
return records;
|
|
2101
2091
|
}
|
|
@@ -227,7 +227,7 @@ function gitRefs(value, expectedMainOid) {
|
|
|
227
227
|
throw invalidPayload('refs');
|
|
228
228
|
return Object.freeze(decoded);
|
|
229
229
|
}
|
|
230
|
-
function manifestObject(source,
|
|
230
|
+
function manifestObject(source, coordinationFormatVersion) {
|
|
231
231
|
const profile = literal(source, 'profile', COLLAB_CHECKPOINT_PROFILES);
|
|
232
232
|
const expectedMainOid = token(source, 'expectedMainOid', isCollabGitOid);
|
|
233
233
|
const gitObjectFormat = literal(source, 'gitObjectFormat', ['sha1', 'sha256']);
|
|
@@ -263,15 +263,13 @@ function manifestObject(source, includeManifestSha256) {
|
|
|
263
263
|
sourceAuthority,
|
|
264
264
|
targetAuthority,
|
|
265
265
|
};
|
|
266
|
-
if (common.coordinationFormatVersion !==
|
|
266
|
+
if (common.coordinationFormatVersion !== coordinationFormatVersion
|
|
267
267
|
|| common.manifestSchemaVersion !== COLLAB_PROJECT_CHECKPOINT_MANIFEST_SCHEMA_VERSION
|
|
268
268
|
|| common.protocolVersion !== COLLAB_PROTOCOL_VERSION)
|
|
269
269
|
throw invalidPayload('manifest');
|
|
270
|
-
if (!includeManifestSha256)
|
|
271
|
-
return common;
|
|
272
270
|
return {
|
|
273
271
|
artifacts: common.artifacts,
|
|
274
|
-
coordinationFormatVersion
|
|
272
|
+
coordinationFormatVersion,
|
|
275
273
|
createdAt: common.createdAt,
|
|
276
274
|
expectedMainOid: common.expectedMainOid,
|
|
277
275
|
gitObjectFormat: common.gitObjectFormat,
|
|
@@ -286,7 +284,8 @@ function manifestObject(source, includeManifestSha256) {
|
|
|
286
284
|
targetAuthority: common.targetAuthority,
|
|
287
285
|
};
|
|
288
286
|
}
|
|
289
|
-
|
|
287
|
+
/** @internal Shared field validation; public entry points select their own format. */
|
|
288
|
+
export function decodeCheckpointManifestFields(value, coordinationFormatVersion) {
|
|
290
289
|
const source = exactRecord(value, 'manifest', [
|
|
291
290
|
'artifacts',
|
|
292
291
|
'coordinationFormatVersion',
|
|
@@ -303,18 +302,24 @@ export function decodeCollabProjectCheckpointManifest(value) {
|
|
|
303
302
|
'sourceAuthority',
|
|
304
303
|
'targetAuthority',
|
|
305
304
|
]);
|
|
306
|
-
const decoded = manifestObject(source,
|
|
305
|
+
const decoded = manifestObject(source, coordinationFormatVersion);
|
|
307
306
|
const encoded = JSON.stringify(decoded);
|
|
308
307
|
if (!hasUtf8ByteLengthAtMost(encoded, COLLAB_CHECKPOINT_ARTIFACT_LIMITS.maxManifestBytes)) {
|
|
309
308
|
throw invalidPayload('manifest');
|
|
310
309
|
}
|
|
311
310
|
return decoded;
|
|
312
311
|
}
|
|
312
|
+
export function decodeCollabProjectCheckpointManifest(value) {
|
|
313
|
+
return decodeCheckpointManifestFields(value, COLLAB_PROJECT_COORDINATION_FORMAT_VERSION);
|
|
314
|
+
}
|
|
313
315
|
export function encodeCollabProjectCheckpointManifestCanonicalJson(manifest) {
|
|
314
316
|
return JSON.stringify(decodeCollabProjectCheckpointManifest(manifest));
|
|
315
317
|
}
|
|
316
318
|
export function encodeCollabProjectCheckpointManifestDigestInput(manifest) {
|
|
317
|
-
|
|
319
|
+
return encodeCheckpointManifestDigestInput(decodeCollabProjectCheckpointManifest(manifest));
|
|
320
|
+
}
|
|
321
|
+
/** @internal Encode already validated fields without changing their format. */
|
|
322
|
+
export function encodeCheckpointManifestDigestInput(decoded) {
|
|
318
323
|
const source = {
|
|
319
324
|
artifacts: decoded.artifacts,
|
|
320
325
|
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
@@ -697,13 +702,13 @@ function idempotencyResultRecord(source, recordId, revision) {
|
|
|
697
702
|
},
|
|
698
703
|
};
|
|
699
704
|
}
|
|
700
|
-
function principalBindingRecord(source, recordId, revision) {
|
|
701
|
-
const value =
|
|
705
|
+
function principalBindingRecord(source, recordId, revision, deferPrincipals) {
|
|
706
|
+
const value = principalRecord(source.value, 'value', [
|
|
702
707
|
'boundAt',
|
|
703
708
|
'memberId',
|
|
704
709
|
'principalId',
|
|
705
710
|
'projectId',
|
|
706
|
-
]);
|
|
711
|
+
], deferPrincipals);
|
|
707
712
|
const memberId = token(value, 'memberId', isCollabMemberId);
|
|
708
713
|
if (recordId !== memberId)
|
|
709
714
|
throw invalidPayload('recordId');
|
|
@@ -714,7 +719,7 @@ function principalBindingRecord(source, recordId, revision) {
|
|
|
714
719
|
value: {
|
|
715
720
|
boundAt: timestamp(value, 'boundAt'),
|
|
716
721
|
memberId,
|
|
717
|
-
principalId: token(value, 'principalId'),
|
|
722
|
+
principalId: deferPrincipals ? value.principalId : token(value, 'principalId'),
|
|
718
723
|
projectId: token(value, 'projectId', isCollabProjectId),
|
|
719
724
|
},
|
|
720
725
|
};
|
|
@@ -848,7 +853,13 @@ function lifecycleStateRecord(source, recordId, revision) {
|
|
|
848
853
|
value: internalState,
|
|
849
854
|
};
|
|
850
855
|
}
|
|
851
|
-
function
|
|
856
|
+
function principalRecord(value, field, keys, deferPrincipals) {
|
|
857
|
+
const source = record(value, field);
|
|
858
|
+
return exactRecord(source, field, deferPrincipals && !Object.hasOwn(source, 'principalId')
|
|
859
|
+
? keys.filter(key => key !== 'principalId')
|
|
860
|
+
: keys);
|
|
861
|
+
}
|
|
862
|
+
function terminalResponderRecord(source, recordId, revision, deferPrincipals) {
|
|
852
863
|
const value = exactRecord(source.value, 'value', [
|
|
853
864
|
'acknowledgements',
|
|
854
865
|
'eligibleMemberIds',
|
|
@@ -900,22 +911,24 @@ function terminalResponderRecord(source, recordId, revision) {
|
|
|
900
911
|
if (!Array.isArray(value.acknowledgements))
|
|
901
912
|
throw invalidPayload('acknowledgements');
|
|
902
913
|
const acknowledgements = value.acknowledgements.map((item) => {
|
|
903
|
-
const acknowledgement =
|
|
914
|
+
const acknowledgement = principalRecord(item, 'acknowledgement', [
|
|
904
915
|
'acknowledgedAt',
|
|
905
916
|
'memberId',
|
|
906
917
|
'principalId',
|
|
907
|
-
]);
|
|
918
|
+
], deferPrincipals);
|
|
908
919
|
return {
|
|
909
920
|
acknowledgedAt: timestamp(acknowledgement, 'acknowledgedAt'),
|
|
910
921
|
memberId: token(acknowledgement, 'memberId', isCollabMemberId),
|
|
911
|
-
principalId:
|
|
922
|
+
principalId: deferPrincipals
|
|
923
|
+
? acknowledgement.principalId
|
|
924
|
+
: token(acknowledgement, 'principalId'),
|
|
912
925
|
};
|
|
913
926
|
});
|
|
914
927
|
const acknowledgementPrincipals = new Set();
|
|
915
928
|
acknowledgements.forEach((item, index) => {
|
|
916
929
|
if (!eligibleMemberIds.includes(item.memberId)
|
|
917
930
|
|| (index > 0 && acknowledgements[index - 1].memberId.localeCompare(item.memberId, 'en-US') >= 0)
|
|
918
|
-
|| acknowledgementPrincipals.has(item.principalId))
|
|
931
|
+
|| (!deferPrincipals && acknowledgementPrincipals.has(item.principalId)))
|
|
919
932
|
throw invalidPayload('acknowledgements');
|
|
920
933
|
acknowledgementPrincipals.add(item.principalId);
|
|
921
934
|
});
|
|
@@ -924,7 +937,7 @@ function terminalResponderRecord(source, recordId, revision) {
|
|
|
924
937
|
recordId,
|
|
925
938
|
revision,
|
|
926
939
|
value: {
|
|
927
|
-
acknowledgements
|
|
940
|
+
acknowledgements,
|
|
928
941
|
eligibleMemberIds: Object.freeze(eligibleMemberIds),
|
|
929
942
|
expiresAt,
|
|
930
943
|
operation,
|
|
@@ -1097,8 +1110,9 @@ function authorityVolumePairRecord(source, recordId, revision) {
|
|
|
1097
1110
|
},
|
|
1098
1111
|
};
|
|
1099
1112
|
}
|
|
1100
|
-
function decodeCheckpointRecord(value) {
|
|
1113
|
+
export function decodeCheckpointRecord(value, principalPhase) {
|
|
1101
1114
|
const { kind, recordId, revision, source } = recordEnvelope(value);
|
|
1115
|
+
const deferPrincipals = principalPhase === 'deferred';
|
|
1102
1116
|
switch (kind) {
|
|
1103
1117
|
case 'project': return projectRecord(source, recordId, revision);
|
|
1104
1118
|
case 'member': return memberRecord(source, recordId, revision);
|
|
@@ -1111,10 +1125,12 @@ function decodeCheckpointRecord(value) {
|
|
|
1111
1125
|
case 'cloud-event': return cloudEventRecord(source, recordId, revision);
|
|
1112
1126
|
case 'cloud-event-cursor': return cloudEventCursorRecord(source, recordId, revision);
|
|
1113
1127
|
case 'idempotency-result': return idempotencyResultRecord(source, recordId, revision);
|
|
1114
|
-
case 'principal-binding':
|
|
1128
|
+
case 'principal-binding':
|
|
1129
|
+
return principalBindingRecord(source, recordId, revision, deferPrincipals);
|
|
1115
1130
|
case 'repository-placement': return repositoryPlacementRecord(source, recordId, revision);
|
|
1116
1131
|
case 'lifecycle-state': return lifecycleStateRecord(source, recordId, revision);
|
|
1117
|
-
case 'terminal-responder':
|
|
1132
|
+
case 'terminal-responder':
|
|
1133
|
+
return terminalResponderRecord(source, recordId, revision, deferPrincipals);
|
|
1118
1134
|
case 'protected-claim-envelope':
|
|
1119
1135
|
return protectedClaimEnvelopeRecord(source, recordId, revision);
|
|
1120
1136
|
case 'tombstone': return tombstoneRecord(source, recordId, revision);
|
|
@@ -1130,7 +1146,8 @@ function compareRecords(left, right) {
|
|
|
1130
1146
|
const kindOrder = recordKindOrder(left.kind) - recordKindOrder(right.kind);
|
|
1131
1147
|
return kindOrder !== 0 ? kindOrder : left.recordId.localeCompare(right.recordId, 'en-US');
|
|
1132
1148
|
}
|
|
1133
|
-
|
|
1149
|
+
/** @internal V3 decoding checks terminal principals separately; consistency binds current principals. */
|
|
1150
|
+
export function validateCheckpointRecordSequence(records, profile, acknowledgementBinding = 'current-principal') {
|
|
1134
1151
|
if (records.length === 0 || records[0].kind !== 'project')
|
|
1135
1152
|
throw invalidPayload('records');
|
|
1136
1153
|
const projectId = records[0].recordId;
|
|
@@ -1233,7 +1250,9 @@ function validateRecordSequence(records, profile) {
|
|
|
1233
1250
|
throw invalidPayload('records');
|
|
1234
1251
|
}
|
|
1235
1252
|
if (item.kind === 'terminal-responder' && (item.value.eligibleMemberIds.some(memberId => !members.has(memberId))
|
|
1236
|
-
|| item.value.acknowledgements.some(acknowledgement => (
|
|
1253
|
+
|| item.value.acknowledgements.some(acknowledgement => (acknowledgementBinding === 'current-principal'
|
|
1254
|
+
? principalBindings.get(acknowledgement.memberId) !== acknowledgement.principalId
|
|
1255
|
+
: !principalBindings.has(acknowledgement.memberId)))))
|
|
1237
1256
|
throw invalidPayload('records');
|
|
1238
1257
|
}
|
|
1239
1258
|
if (profile === 'backup') {
|
|
@@ -1260,11 +1279,13 @@ export function decodeCollabProjectCheckpointCoordinationNdjson(value, profile)
|
|
|
1260
1279
|
throw invalidPayload('coordination');
|
|
1261
1280
|
}
|
|
1262
1281
|
const result = decodeCheckpointRecord(parsed);
|
|
1282
|
+
if (result.kind === 'terminal-responder')
|
|
1283
|
+
Object.freeze(result.value.acknowledgements);
|
|
1263
1284
|
if (JSON.stringify(result) !== line)
|
|
1264
1285
|
throw invalidPayload('coordination');
|
|
1265
1286
|
return result;
|
|
1266
1287
|
});
|
|
1267
|
-
|
|
1288
|
+
validateCheckpointRecordSequence(decoded, profile);
|
|
1268
1289
|
return Object.freeze(decoded);
|
|
1269
1290
|
}
|
|
1270
1291
|
export function encodeCollabProjectCheckpointCoordinationNdjson(records, profile) {
|
|
@@ -1296,7 +1317,11 @@ function checkpointRecordGitOids(checkpointRecord) {
|
|
|
1296
1317
|
}
|
|
1297
1318
|
export function validateCollabProjectCheckpointConsistency(manifest, records) {
|
|
1298
1319
|
const decodedManifest = decodeCollabProjectCheckpointManifest(manifest);
|
|
1299
|
-
|
|
1320
|
+
validateCheckpointRecordSequence(records, decodedManifest.profile);
|
|
1321
|
+
return validateCheckpointManifestConsistency(decodedManifest, records);
|
|
1322
|
+
}
|
|
1323
|
+
/** @internal Cross-artifact checks operate on validated fields, not another format. */
|
|
1324
|
+
export function validateCheckpointManifestConsistency(decodedManifest, records) {
|
|
1300
1325
|
const project = records[0];
|
|
1301
1326
|
if (project.kind !== 'project'
|
|
1302
1327
|
|| project.value.projectId !== decodedManifest.projectId
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudian-collab/protocol",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"description": "Canonical Collab wire and Cloud binding contract for Claudian clients and servers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"release:candidate": "node scripts/release-candidate.mjs --output .context/release",
|
|
47
47
|
"test": "jest && node --test tests/*.test.mjs",
|
|
48
48
|
"typecheck": "tsc --noEmit -p tsconfig.jest.json",
|
|
49
|
-
"verify": "npm run lint && npm run typecheck && npm run
|
|
49
|
+
"verify": "npm run lint && npm run typecheck && npm run release:candidate && npm run test && npm run check:compatibility && npm run check:migration-provenance && npm run verify:pack",
|
|
50
50
|
"verify:pack": "node scripts/verify-pack.mjs"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|