@claudian-collab/protocol 3.3.1 → 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 -24
- package/dist/CollabProjectBackupCheckpoint.js +42 -106
- package/dist/CollabProjectCheckpoint.d.ts +25 -0
- package/dist/CollabProjectCheckpoint.js +55 -25
- package/dist/esm/CollabProjectBackupCheckpoint.mjs +43 -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,31 +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 pre-production
|
|
78
|
-
`3.3.1` corrective patch adds only the missing backup-v3 tombstone needed to
|
|
79
|
-
compact terminal Manager-responsibility offers and exact responses without
|
|
80
|
-
allowing their idempotency identities to be reused. The package version never
|
|
81
|
-
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.
|
|
82
68
|
- **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `6`. The supported
|
|
83
69
|
range is exactly `[6, 6]`. This is independent from Cloud binding version `2` and the existing application
|
|
84
70
|
LAN control version `9`. Any change to an existing envelope, DTO, operation
|
|
85
|
-
payload shape, or operation definition
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
- **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.
|
|
90
75
|
|
|
91
76
|
### Compatibility behavior
|
|
92
77
|
|
|
@@ -105,14 +90,18 @@ Compatibility is tested with executable fixtures in this package
|
|
|
105
90
|
(`tests/`), and cross-repository contract fixtures pin client and server to
|
|
106
91
|
the same behavior.
|
|
107
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
|
+
|
|
108
95
|
## Development
|
|
109
96
|
|
|
110
97
|
```bash
|
|
111
98
|
npm run build # compile src/ -> dist/ (CommonJS + declarations)
|
|
112
99
|
npm test # package unit tests
|
|
113
100
|
npm run check:compatibility
|
|
114
|
-
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
|
|
115
103
|
```
|
|
116
104
|
|
|
117
105
|
`dist/` is generated output and is never committed. The packed artifact is
|
|
118
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.
|
|
@@ -208,65 +208,19 @@ function absoluteTargetUrl(source) {
|
|
|
208
208
|
function principal(source, field) {
|
|
209
209
|
return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
|
|
210
210
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (source.kind === 'principal-binding') {
|
|
215
|
-
return {
|
|
216
|
-
...source,
|
|
217
|
-
value: { ...value, principalId: `principal_${String(value.memberId)}` },
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
|
|
221
|
-
return {
|
|
222
|
-
...source,
|
|
223
|
-
value: {
|
|
224
|
-
...value,
|
|
225
|
-
acknowledgements: value.acknowledgements.map(item => ({
|
|
226
|
-
...record(item, 'acknowledgement'),
|
|
227
|
-
principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
|
|
228
|
-
})),
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
return item;
|
|
233
|
-
}
|
|
234
|
-
function restoreBackupBasePrincipals(source, base) {
|
|
235
|
-
if (base.kind === 'principal-binding') {
|
|
236
|
-
const value = exactRecord(source.value, 'value', [
|
|
237
|
-
'boundAt', 'memberId', 'principalId', 'projectId',
|
|
238
|
-
]);
|
|
239
|
-
return {
|
|
240
|
-
...base,
|
|
241
|
-
value: { ...base.value, principalId: principal(value, 'principalId') },
|
|
242
|
-
};
|
|
243
|
-
}
|
|
211
|
+
function validateBackupBasePrincipals(base) {
|
|
212
|
+
if (base.kind === 'principal-binding')
|
|
213
|
+
principal(base.value, 'principalId');
|
|
244
214
|
if (base.kind === 'terminal-responder') {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
'
|
|
249
|
-
'operation',
|
|
250
|
-
'operationId',
|
|
251
|
-
'projectId',
|
|
252
|
-
'responseJson',
|
|
253
|
-
]);
|
|
254
|
-
const sourceAcknowledgements = value.acknowledgements;
|
|
255
|
-
if (!Array.isArray(sourceAcknowledgements)
|
|
256
|
-
|| sourceAcknowledgements.length !== base.value.acknowledgements.length)
|
|
257
|
-
throw invalidPayload('acknowledgements');
|
|
258
|
-
const acknowledgements = base.value.acknowledgements.map((item, index) => {
|
|
259
|
-
const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
|
|
260
|
-
'acknowledgedAt', 'memberId', 'principalId',
|
|
261
|
-
]);
|
|
262
|
-
return { ...item, principalId: principal(original, 'principalId') };
|
|
263
|
-
});
|
|
264
|
-
if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
|
|
265
|
-
throw invalidPayload('acknowledgements');
|
|
215
|
+
for (const acknowledgement of base.value.acknowledgements) {
|
|
216
|
+
if (acknowledgement.principalId === undefined)
|
|
217
|
+
throw invalidPayload('acknowledgement');
|
|
218
|
+
principal(acknowledgement, 'principalId');
|
|
266
219
|
}
|
|
267
|
-
|
|
220
|
+
if (new Set(base.value.acknowledgements.map(item => item.principalId)).size
|
|
221
|
+
!== base.value.acknowledgements.length)
|
|
222
|
+
throw invalidPayload('acknowledgements');
|
|
268
223
|
}
|
|
269
|
-
return base;
|
|
270
224
|
}
|
|
271
225
|
function repositoryPublicationRef(value) {
|
|
272
226
|
const source = exactRecord(value, 'ref', ['name', 'oid']);
|
|
@@ -2010,55 +1964,18 @@ function validateContinuity(records) {
|
|
|
2010
1964
|
}
|
|
2011
1965
|
}
|
|
2012
1966
|
}
|
|
2013
|
-
function
|
|
1967
|
+
function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
2014
1968
|
const source = record(value, 'manifest');
|
|
2015
1969
|
if (source.profile !== 'backup'
|
|
2016
1970
|
|| source.coordinationFormatVersion !== exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
|
|
2017
1971
|
throw invalidPayload('manifest');
|
|
2018
|
-
return (0, CollabProjectCheckpoint_1.
|
|
2019
|
-
...source,
|
|
2020
|
-
coordinationFormatVersion: CollabProjectCheckpoint_1.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2023
|
-
function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
2024
|
-
const decoded = manifestWithFormatOne(value);
|
|
2025
|
-
return {
|
|
2026
|
-
artifacts: decoded.artifacts,
|
|
2027
|
-
coordinationFormatVersion: exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
|
|
2028
|
-
createdAt: decoded.createdAt,
|
|
2029
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
2030
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
2031
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
2032
|
-
manifestSha256: decoded.manifestSha256,
|
|
2033
|
-
operationId: decoded.operationId,
|
|
2034
|
-
profile: 'backup',
|
|
2035
|
-
projectId: decoded.projectId,
|
|
2036
|
-
protocolVersion: decoded.protocolVersion,
|
|
2037
|
-
refs: decoded.refs,
|
|
2038
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
2039
|
-
targetAuthority: decoded.targetAuthority,
|
|
2040
|
-
};
|
|
1972
|
+
return (0, CollabProjectCheckpoint_1.decodeCheckpointManifestFields)(source, exports.COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION);
|
|
2041
1973
|
}
|
|
2042
1974
|
function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
|
|
2043
1975
|
return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
2044
1976
|
}
|
|
2045
1977
|
function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
|
|
2046
|
-
|
|
2047
|
-
return JSON.stringify({
|
|
2048
|
-
artifacts: decoded.artifacts,
|
|
2049
|
-
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
2050
|
-
createdAt: decoded.createdAt,
|
|
2051
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
2052
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
2053
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
2054
|
-
operationId: decoded.operationId,
|
|
2055
|
-
profile: decoded.profile,
|
|
2056
|
-
projectId: decoded.projectId,
|
|
2057
|
-
protocolVersion: decoded.protocolVersion,
|
|
2058
|
-
refs: decoded.refs,
|
|
2059
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
2060
|
-
targetAuthority: decoded.targetAuthority,
|
|
2061
|
-
});
|
|
1978
|
+
return (0, CollabProjectCheckpoint_1.encodeCheckpointManifestDigestInput)(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
2062
1979
|
}
|
|
2063
1980
|
function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
2064
1981
|
if (typeof value !== 'string'
|
|
@@ -2078,22 +1995,43 @@ function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
|
2078
1995
|
}
|
|
2079
1996
|
if (JSON.stringify(item) !== line)
|
|
2080
1997
|
throw invalidPayload('coordination');
|
|
2081
|
-
return {
|
|
1998
|
+
return {
|
|
1999
|
+
envelope: recordEnvelope(item),
|
|
2000
|
+
item,
|
|
2001
|
+
base: undefined,
|
|
2002
|
+
};
|
|
2082
2003
|
});
|
|
2083
2004
|
const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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 }) => {
|
|
2087
2025
|
if (envelope.kind === 'idempotency-result') {
|
|
2088
2026
|
return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
|
|
2089
2027
|
}
|
|
2090
2028
|
if (CONTINUITY_KIND_SET.has(envelope.kind)) {
|
|
2091
2029
|
return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
|
|
2092
2030
|
}
|
|
2093
|
-
const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
|
|
2094
2031
|
if (base === undefined)
|
|
2095
2032
|
throw invalidPayload('records');
|
|
2096
|
-
|
|
2033
|
+
validateBackupBasePrincipals(base);
|
|
2034
|
+
return base;
|
|
2097
2035
|
});
|
|
2098
2036
|
if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
|
|
2099
2037
|
throw invalidPayload('coordination');
|
|
@@ -2157,9 +2095,7 @@ function validateCollabProjectBackupCheckpointConsistency(manifest, records) {
|
|
|
2157
2095
|
}))
|
|
2158
2096
|
throw invalidPayload('records');
|
|
2159
2097
|
const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
|
|
2160
|
-
(0, CollabProjectCheckpoint_1.
|
|
2161
|
-
|
|
2162
|
-
coordinationFormatVersion: CollabProjectCheckpoint_1.COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
2163
|
-
}), baseRecords);
|
|
2098
|
+
(0, CollabProjectCheckpoint_1.validateCheckpointRecordSequence)(baseRecords, 'backup');
|
|
2099
|
+
(0, CollabProjectCheckpoint_1.validateCheckpointManifestConsistency)(decodedManifest, baseRecords);
|
|
2164
2100
|
return records;
|
|
2165
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';
|
|
@@ -198,65 +198,19 @@ function absoluteTargetUrl(source) {
|
|
|
198
198
|
function principal(source, field) {
|
|
199
199
|
return token(source, field, value => (typeof value === 'string' && PRINCIPAL_PATTERN.test(value)));
|
|
200
200
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (source.kind === 'principal-binding') {
|
|
205
|
-
return {
|
|
206
|
-
...source,
|
|
207
|
-
value: { ...value, principalId: `principal_${String(value.memberId)}` },
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
if (source.kind === 'terminal-responder' && Array.isArray(value.acknowledgements)) {
|
|
211
|
-
return {
|
|
212
|
-
...source,
|
|
213
|
-
value: {
|
|
214
|
-
...value,
|
|
215
|
-
acknowledgements: value.acknowledgements.map(item => ({
|
|
216
|
-
...record(item, 'acknowledgement'),
|
|
217
|
-
principalId: `principal_${String(record(item, 'acknowledgement').memberId)}`,
|
|
218
|
-
})),
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
return item;
|
|
223
|
-
}
|
|
224
|
-
function restoreBackupBasePrincipals(source, base) {
|
|
225
|
-
if (base.kind === 'principal-binding') {
|
|
226
|
-
const value = exactRecord(source.value, 'value', [
|
|
227
|
-
'boundAt', 'memberId', 'principalId', 'projectId',
|
|
228
|
-
]);
|
|
229
|
-
return {
|
|
230
|
-
...base,
|
|
231
|
-
value: { ...base.value, principalId: principal(value, 'principalId') },
|
|
232
|
-
};
|
|
233
|
-
}
|
|
201
|
+
function validateBackupBasePrincipals(base) {
|
|
202
|
+
if (base.kind === 'principal-binding')
|
|
203
|
+
principal(base.value, 'principalId');
|
|
234
204
|
if (base.kind === 'terminal-responder') {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
'
|
|
239
|
-
'operation',
|
|
240
|
-
'operationId',
|
|
241
|
-
'projectId',
|
|
242
|
-
'responseJson',
|
|
243
|
-
]);
|
|
244
|
-
const sourceAcknowledgements = value.acknowledgements;
|
|
245
|
-
if (!Array.isArray(sourceAcknowledgements)
|
|
246
|
-
|| sourceAcknowledgements.length !== base.value.acknowledgements.length)
|
|
247
|
-
throw invalidPayload('acknowledgements');
|
|
248
|
-
const acknowledgements = base.value.acknowledgements.map((item, index) => {
|
|
249
|
-
const original = exactRecord(sourceAcknowledgements[index], 'acknowledgement', [
|
|
250
|
-
'acknowledgedAt', 'memberId', 'principalId',
|
|
251
|
-
]);
|
|
252
|
-
return { ...item, principalId: principal(original, 'principalId') };
|
|
253
|
-
});
|
|
254
|
-
if (new Set(acknowledgements.map(item => item.principalId)).size !== acknowledgements.length) {
|
|
255
|
-
throw invalidPayload('acknowledgements');
|
|
205
|
+
for (const acknowledgement of base.value.acknowledgements) {
|
|
206
|
+
if (acknowledgement.principalId === undefined)
|
|
207
|
+
throw invalidPayload('acknowledgement');
|
|
208
|
+
principal(acknowledgement, 'principalId');
|
|
256
209
|
}
|
|
257
|
-
|
|
210
|
+
if (new Set(base.value.acknowledgements.map(item => item.principalId)).size
|
|
211
|
+
!== base.value.acknowledgements.length)
|
|
212
|
+
throw invalidPayload('acknowledgements');
|
|
258
213
|
}
|
|
259
|
-
return base;
|
|
260
214
|
}
|
|
261
215
|
function repositoryPublicationRef(value) {
|
|
262
216
|
const source = exactRecord(value, 'ref', ['name', 'oid']);
|
|
@@ -2000,55 +1954,18 @@ function validateContinuity(records) {
|
|
|
2000
1954
|
}
|
|
2001
1955
|
}
|
|
2002
1956
|
}
|
|
2003
|
-
function
|
|
1957
|
+
export function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
2004
1958
|
const source = record(value, 'manifest');
|
|
2005
1959
|
if (source.profile !== 'backup'
|
|
2006
1960
|
|| source.coordinationFormatVersion !== COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION)
|
|
2007
1961
|
throw invalidPayload('manifest');
|
|
2008
|
-
return
|
|
2009
|
-
...source,
|
|
2010
|
-
coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
2011
|
-
});
|
|
2012
|
-
}
|
|
2013
|
-
export function decodeCollabProjectBackupCheckpointManifest(value) {
|
|
2014
|
-
const decoded = manifestWithFormatOne(value);
|
|
2015
|
-
return {
|
|
2016
|
-
artifacts: decoded.artifacts,
|
|
2017
|
-
coordinationFormatVersion: COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION,
|
|
2018
|
-
createdAt: decoded.createdAt,
|
|
2019
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
2020
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
2021
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
2022
|
-
manifestSha256: decoded.manifestSha256,
|
|
2023
|
-
operationId: decoded.operationId,
|
|
2024
|
-
profile: 'backup',
|
|
2025
|
-
projectId: decoded.projectId,
|
|
2026
|
-
protocolVersion: decoded.protocolVersion,
|
|
2027
|
-
refs: decoded.refs,
|
|
2028
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
2029
|
-
targetAuthority: decoded.targetAuthority,
|
|
2030
|
-
};
|
|
1962
|
+
return decodeCheckpointManifestFields(source, COLLAB_PROJECT_BACKUP_COORDINATION_FORMAT_VERSION);
|
|
2031
1963
|
}
|
|
2032
1964
|
export function encodeCollabProjectBackupCheckpointManifestCanonicalJson(manifest) {
|
|
2033
1965
|
return JSON.stringify(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
2034
1966
|
}
|
|
2035
1967
|
export function encodeCollabProjectBackupCheckpointManifestDigestInput(manifest) {
|
|
2036
|
-
|
|
2037
|
-
return JSON.stringify({
|
|
2038
|
-
artifacts: decoded.artifacts,
|
|
2039
|
-
coordinationFormatVersion: decoded.coordinationFormatVersion,
|
|
2040
|
-
createdAt: decoded.createdAt,
|
|
2041
|
-
expectedMainOid: decoded.expectedMainOid,
|
|
2042
|
-
gitObjectFormat: decoded.gitObjectFormat,
|
|
2043
|
-
manifestSchemaVersion: decoded.manifestSchemaVersion,
|
|
2044
|
-
operationId: decoded.operationId,
|
|
2045
|
-
profile: decoded.profile,
|
|
2046
|
-
projectId: decoded.projectId,
|
|
2047
|
-
protocolVersion: decoded.protocolVersion,
|
|
2048
|
-
refs: decoded.refs,
|
|
2049
|
-
sourceAuthority: decoded.sourceAuthority,
|
|
2050
|
-
targetAuthority: decoded.targetAuthority,
|
|
2051
|
-
});
|
|
1968
|
+
return encodeCheckpointManifestDigestInput(decodeCollabProjectBackupCheckpointManifest(manifest));
|
|
2052
1969
|
}
|
|
2053
1970
|
export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
2054
1971
|
if (typeof value !== 'string'
|
|
@@ -2068,22 +1985,43 @@ export function decodeCollabProjectBackupCheckpointCoordinationNdjson(value) {
|
|
|
2068
1985
|
}
|
|
2069
1986
|
if (JSON.stringify(item) !== line)
|
|
2070
1987
|
throw invalidPayload('coordination');
|
|
2071
|
-
return {
|
|
1988
|
+
return {
|
|
1989
|
+
envelope: recordEnvelope(item),
|
|
1990
|
+
item,
|
|
1991
|
+
base: undefined,
|
|
1992
|
+
};
|
|
2072
1993
|
});
|
|
2073
1994
|
const baseItems = parsed.filter(({ envelope }) => (!CONTINUITY_KIND_SET.has(envelope.kind) && envelope.kind !== 'idempotency-result'));
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2076
|
-
|
|
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 }) => {
|
|
2077
2015
|
if (envelope.kind === 'idempotency-result') {
|
|
2078
2016
|
return idempotencyResultRecord(envelope.source, envelope.recordId, envelope.revision);
|
|
2079
2017
|
}
|
|
2080
2018
|
if (CONTINUITY_KIND_SET.has(envelope.kind)) {
|
|
2081
2019
|
return decodeContinuityRecord(envelope.kind, envelope.source, envelope.recordId, envelope.revision);
|
|
2082
2020
|
}
|
|
2083
|
-
const base = baseByIdentity.get(`${envelope.kind}\0${envelope.recordId}`);
|
|
2084
2021
|
if (base === undefined)
|
|
2085
2022
|
throw invalidPayload('records');
|
|
2086
|
-
|
|
2023
|
+
validateBackupBasePrincipals(base);
|
|
2024
|
+
return base;
|
|
2087
2025
|
});
|
|
2088
2026
|
if (decoded.some((item, index) => JSON.stringify(item) !== lines[index])) {
|
|
2089
2027
|
throw invalidPayload('coordination');
|
|
@@ -2147,9 +2085,7 @@ export function validateCollabProjectBackupCheckpointConsistency(manifest, recor
|
|
|
2147
2085
|
}))
|
|
2148
2086
|
throw invalidPayload('records');
|
|
2149
2087
|
const baseRecords = decodedRecords.filter(item => (!CONTINUITY_KIND_SET.has(item.kind) && item.kind !== 'idempotency-result'));
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
coordinationFormatVersion: COLLAB_PROJECT_COORDINATION_FORMAT_VERSION,
|
|
2153
|
-
}), baseRecords);
|
|
2088
|
+
validateCheckpointRecordSequence(baseRecords, 'backup');
|
|
2089
|
+
validateCheckpointManifestConsistency(decodedManifest, baseRecords);
|
|
2154
2090
|
return records;
|
|
2155
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": {
|