@claudian-collab/protocol 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/CollabCloudBinding.d.ts +106 -0
  4. package/dist/CollabCloudBinding.js +431 -0
  5. package/dist/CollabCloudProjectEvent.d.ts +41 -0
  6. package/dist/CollabCloudProjectEvent.js +133 -0
  7. package/dist/CollabCloudProjectSnapshot.d.ts +42 -0
  8. package/dist/CollabCloudProjectSnapshot.js +281 -0
  9. package/dist/CollabConstants.d.ts +32 -0
  10. package/dist/CollabConstants.js +34 -0
  11. package/dist/CollabControlOperationCodecs.d.ts +28 -0
  12. package/dist/CollabControlOperationCodecs.js +63 -0
  13. package/dist/CollabError.d.ts +27 -0
  14. package/dist/CollabError.js +165 -0
  15. package/dist/CollabMarkdownProse.d.ts +1 -0
  16. package/dist/CollabMarkdownProse.js +57 -0
  17. package/dist/CollabMemberMentionParser.d.ts +6 -0
  18. package/dist/CollabMemberMentionParser.js +51 -0
  19. package/dist/CollabProtocol.d.ts +144 -0
  20. package/dist/CollabProtocol.js +67 -0
  21. package/dist/CollabRequestTicketRequestCodecs.d.ts +5 -0
  22. package/dist/CollabRequestTicketRequestCodecs.js +266 -0
  23. package/dist/CollabRequestTicketResponseCodecs.d.ts +15 -0
  24. package/dist/CollabRequestTicketResponseCodecs.js +368 -0
  25. package/dist/CollabTicketReferenceParser.d.ts +24 -0
  26. package/dist/CollabTicketReferenceParser.js +56 -0
  27. package/dist/CollabValidation.d.ts +5 -0
  28. package/dist/CollabValidation.js +26 -0
  29. package/dist/DevelopmentBootstrap.d.ts +199 -0
  30. package/dist/DevelopmentBootstrap.js +544 -0
  31. package/dist/index.d.ts +24 -0
  32. package/dist/index.js +67 -0
  33. package/dist/types.d.ts +138 -0
  34. package/dist/types.js +11 -0
  35. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @claudian-collab/protocol
2
+
3
+ The canonical shared Collab wire contract, produced from the
4
+ [claudian-collab-protocol](https://github.com/YishenTu/claudian-collab-protocol) repository. Both Claudian
5
+ (LAN client and Host) and Claudian Cloud Server consume this package; neither
6
+ may maintain copied types, operation registries, codec maps, shared constants,
7
+ or compatibility rules.
8
+
9
+ ## Contents
10
+
11
+ - Opaque Collab ID types (`CollabProjectId`, `CollabMemberId`, `CollabGitOid`, …)
12
+ - Transport-neutral request and response DTOs
13
+ - Executable request/response codecs with decoder-defined compatibility behavior
14
+ - The canonical operation registry (`CollabControlOperationMap`,
15
+ `COLLAB_CONTROL_OPERATION_CODECS`, `collabControlOperationCodec`)
16
+ - Safe public error codes and sanitized error context (`CollabError`,
17
+ `COLLAB_ERROR_CODES`, `sanitizeCollabDiagnosticContext`)
18
+ - Shared Git ref semantics (`COLLAB_MAIN_REF`, `COLLAB_MEMBER_REF_PREFIX`,
19
+ `collabMemberRef`)
20
+ - Shared limits that client and server must agree on (`COLLAB_LIMITS`)
21
+ - Protocol-version negotiation (`COLLAB_PROTOCOL_VERSION`, envelope decoding)
22
+ - Cloud binding v1 route construction/matching, capability negotiation, strict success/error envelopes, private-development bootstrap, Project snapshot, and redacted event contracts
23
+ - Markdown-derived Ticket-reference and Member-mention semantics shared by
24
+ client rendering and authority derivation
25
+
26
+ The package owns the declarative Cloud HTTP/WebSocket/Git binding contract, but no transport runtime, request dispatch, trust policy, authentication, authorization, persistence, Git execution, or client-local recovery. It contains no LAN Host admission, invitation trust, mDNS/TLS/discovery, or Host-transfer transport; no Obsidian, Vault, UI, Agent, provider, SQL, or filesystem behavior; and no `IngressPrincipal` or other deployment-ingress contracts.
27
+
28
+ The current registry contains only the decision-complete, transport-neutral
29
+ request, Ticket, metadata, and Accept operations. Detail DTOs embed the first
30
+ page of their comments and accepted relations; `listRequestComments`,
31
+ `listTicketComments`, and `listTicketAcceptedRelations` page the remainder
32
+ with stable opaque cursors, and producers bound pages by the shared count and
33
+ UTF-8 byte limits. Producers size embedded first pages against the measured
34
+ fixed part of the detail so the whole detail stays within
35
+ `detailMaxUtf8Bytes`, including JSON escaping. JSON adapters accept the shared
36
+ `maxJsonPayloadUtf8Bytes` cap so every valid bounded request, page, and detail
37
+ has one compatible final-serialization limit. Request comments and Ticket
38
+ accepted relations also have shared authority-enforced total limits; summary
39
+ counts let complete consumers reject partial or cross-snapshot assembly.
40
+ Cloud binding v1 adds the authority-neutral Project snapshot, six redacted durable event kinds plus `snapshot.required`, eight capability tokens, the exact ordinary Project/Git route catalog, and six private-development bootstrap bindings. Existing LAN v9 Join, invitation, endpoint, membership lifecycle, Host-transfer, retirement, snapshot, event, and HTTP bindings remain independently application-owned.
41
+
42
+ ## Usage
43
+
44
+ ```ts
45
+ import {
46
+ COLLAB_PROTOCOL_VERSION,
47
+ COLLAB_CONTROL_OPERATION_CODECS,
48
+ CollabError,
49
+ } from '@claudian-collab/protocol';
50
+
51
+ const codec = COLLAB_CONTROL_OPERATION_CODECS.ensureMyRequest;
52
+ const decoded = codec.decodeRequest(rawBody);
53
+ if (decoded.status !== 'ok') {
54
+ // decoded.error is a safe CollabError: no credentials, paths, or internals.
55
+ }
56
+ ```
57
+
58
+ Only the package root (`.`) is a supported import. Subpath imports are not
59
+ part of the public surface.
60
+
61
+ ## Versioning
62
+
63
+ Package SemVer, canonical wire version, and Cloud binding version are independent concepts.
64
+
65
+ - **Package version** (this `package.json`): `1.0.0`. Patch releases preserve
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. The package
70
+ version never signals wire compatibility by itself.
71
+ - **Wire version** (`COLLAB_PROTOCOL_VERSION`): currently `4`. The supported
72
+ range is exactly `[4, 4]`. This is independent from Cloud binding version `1` and the existing application
73
+ LAN control version `9`. Any change to an envelope, DTO, or operation
74
+ payload shape, or to the operation inventory, is wire-breaking and requires
75
+ a new wire-protocol version.
76
+ - **Cloud binding version** (`COLLAB_CLOUD_BINDING_VERSION`): currently `1`. It versions Cloud routes and capabilities independently from package and wire versions.
77
+
78
+ ### Compatibility behavior
79
+
80
+ - Envelope decoders reject unknown fields (`protocol-payload-invalid`).
81
+ Operation request and response compatibility is
82
+ decoder-defined and pinned by package fixtures; decoded DTOs are reconstructed
83
+ rather than retaining unrecognized input properties.
84
+ - An unsupported `protocolVersion` decodes to `unsupported-version` with the
85
+ received and supported versions in safe context.
86
+ - Unknown operation kinds have no codec and fail at registry lookup.
87
+ - Unknown Cloud capability tokens are accepted and ignored by older consumers; unknown document fields, route operations, discriminants, schema versions, binding versions, and wire versions fail closed.
88
+ - `CollabError.safeContext` is sanitized: credential-like keys are redacted
89
+ and filesystem paths are replaced with `[PATH]`.
90
+
91
+ Compatibility is tested with executable fixtures in this package
92
+ (`tests/`), and cross-repository contract fixtures pin client and server to
93
+ the same behavior.
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ npm run build # compile src/ -> dist/ (CommonJS + declarations)
99
+ npm test # package unit tests
100
+ npm run check:compatibility
101
+ npm run verify:pack # pack the artifact and smoke-test a clean consumer
102
+ ```
103
+
104
+ `dist/` is generated output and is never committed. The packed artifact is
105
+ reproducible from source: `npm pack` rebuilds via `prepack`.
@@ -0,0 +1,106 @@
1
+ import { COLLAB_PROTOCOL_VERSION } from './CollabConstants';
2
+ import { CollabError, type CollabDiagnosticContext, type CollabErrorCode, type CollabRecoveryAction } from './CollabError';
3
+ export declare const COLLAB_CLOUD_BINDING_VERSION: 1;
4
+ export declare const COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION: 1;
5
+ export declare const COLLAB_CLOUD_CAPABILITIES: readonly ["accept", "development-bootstrap", "git-receive-pack-personal-ref", "git-upload-pack", "project-events", "project-snapshot", "requests", "tickets"];
6
+ export type CollabCloudCapability = typeof COLLAB_CLOUD_CAPABILITIES[number];
7
+ export declare const COLLAB_CLOUD_JSON_OPERATIONS: readonly ["getProjectSnapshot", ...(keyof import("./CollabProtocol").CollabControlOperationMap)[]];
8
+ export type CollabCloudJsonOperation = typeof COLLAB_CLOUD_JSON_OPERATIONS[number];
9
+ export declare const COLLAB_CLOUD_BINDING_LIMITS: Readonly<{
10
+ readonly bootstrapAttemptTtlMs: number;
11
+ readonly defaultMaxConcurrentBootstrapUploads: 1;
12
+ readonly eventHeartbeatMs: 30000;
13
+ readonly eventMissedHeartbeatLimit: 2;
14
+ readonly maxCloudOpenRequests: 100;
15
+ readonly maxCloudProjectMembers: 100;
16
+ readonly maxCloudSnapshotUtf8Bytes: number;
17
+ readonly maxCloudTicketHighlights: 5;
18
+ readonly maxDevelopmentBootstrapGitBundleBytes: number;
19
+ readonly maxDevelopmentBootstrapManifestUtf8Bytes: number;
20
+ readonly maxDevelopmentBootstrapReportUtf8Bytes: number;
21
+ readonly maxDevelopmentBootstrapRepositoryBytes: number;
22
+ readonly maxDevelopmentBootstrapStagingBytes: number;
23
+ readonly maxEventReplay: 500;
24
+ readonly maxGitReceivePackBytes: number;
25
+ readonly maxRepositoryBytes: number;
26
+ readonly minRetainedEventCount: 10000;
27
+ readonly maxUploadsPerBootstrapAttempt: 1;
28
+ readonly minEventRetentionDays: 30;
29
+ readonly uploadDeadlineMs: number;
30
+ readonly uploadIdleTimeoutMs: 30000;
31
+ }>;
32
+ export interface CollabCloudCapabilityLimits {
33
+ readonly maxDevelopmentBootstrapGitBundleBytes: number;
34
+ readonly maxDevelopmentBootstrapManifestUtf8Bytes: number;
35
+ readonly maxDevelopmentBootstrapReportUtf8Bytes: number;
36
+ readonly maxEventReplay: number;
37
+ readonly maxGitReceivePackBytes: number;
38
+ readonly maxJsonPayloadUtf8Bytes: number;
39
+ readonly maxRepositoryBytes: number;
40
+ }
41
+ export interface CollabCloudCapabilityDocument {
42
+ readonly bindingVersions: readonly number[];
43
+ readonly capabilities: readonly string[];
44
+ readonly limits: CollabCloudCapabilityLimits;
45
+ readonly protocolVersions: readonly number[];
46
+ readonly schemaVersion: typeof COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION;
47
+ }
48
+ export type DevelopmentBootstrapOperation = 'beginDevelopmentBootstrap' | 'submitDevelopmentBootstrapReport' | 'getDevelopmentBootstrap' | 'activateDevelopmentBootstrap' | 'cancelDevelopmentBootstrap' | 'putDevelopmentBootstrapGitBundle';
49
+ export type CollabCloudGitService = 'git-upload-pack' | 'git-receive-pack';
50
+ export type CollabCloudRouteMatch = {
51
+ readonly kind: 'capabilities';
52
+ } | {
53
+ readonly kind: 'project-operation';
54
+ readonly operation: CollabCloudJsonOperation;
55
+ readonly projectId: string;
56
+ } | {
57
+ readonly afterSequence: number;
58
+ readonly kind: 'project-events';
59
+ readonly projectId: string;
60
+ } | {
61
+ readonly kind: 'git-info-refs';
62
+ readonly projectId: string;
63
+ readonly service: CollabCloudGitService;
64
+ } | {
65
+ readonly kind: 'git-upload-pack' | 'git-receive-pack';
66
+ readonly projectId: string;
67
+ } | {
68
+ readonly attemptId?: string;
69
+ readonly kind: 'development-bootstrap';
70
+ readonly operation: DevelopmentBootstrapOperation;
71
+ };
72
+ export interface CollabCloudRoute {
73
+ readonly match: CollabCloudRouteMatch;
74
+ readonly method: 'GET' | 'POST' | 'PUT';
75
+ readonly target: string;
76
+ }
77
+ export interface CollabCloudSuccessEnvelope<T> {
78
+ readonly data: T;
79
+ readonly protocolVersion: typeof COLLAB_PROTOCOL_VERSION;
80
+ readonly requestId: string;
81
+ }
82
+ export interface CollabCloudWireError {
83
+ readonly code: CollabErrorCode;
84
+ readonly recoveryActions: readonly CollabRecoveryAction[];
85
+ readonly safeContext: CollabDiagnosticContext;
86
+ }
87
+ export interface CollabCloudErrorEnvelope {
88
+ readonly error: CollabCloudWireError;
89
+ readonly protocolVersion: typeof COLLAB_PROTOCOL_VERSION;
90
+ readonly requestId: string;
91
+ }
92
+ export declare function collabCloudCapabilitiesRoute(): CollabCloudRoute;
93
+ export declare function collabCloudProjectOperationRoute(projectId: string, operation: CollabCloudJsonOperation): CollabCloudRoute;
94
+ export declare function collabCloudProjectEventsRoute(projectId: string, afterSequence: number): CollabCloudRoute;
95
+ export declare function collabCloudGitRoute(projectId: string, routeKind: 'info-refs', service: CollabCloudGitService): CollabCloudRoute;
96
+ export declare function collabCloudGitRoute(projectId: string, routeKind: CollabCloudGitService): CollabCloudRoute;
97
+ export declare function collabDevelopmentBootstrapRoute(operation: 'beginDevelopmentBootstrap'): CollabCloudRoute;
98
+ export declare function collabDevelopmentBootstrapRoute(operation: Exclude<DevelopmentBootstrapOperation, 'beginDevelopmentBootstrap'>, attemptId: string): CollabCloudRoute;
99
+ export declare function matchCollabCloudRoute(method: string, target: string): CollabCloudRouteMatch | null;
100
+ export declare function decodeCollabCloudCapabilityDocument(value: unknown): CollabCloudCapabilityDocument;
101
+ export declare function collabCloudCapabilityDocument(capabilities: readonly CollabCloudCapability[], limits: CollabCloudCapabilityLimits): CollabCloudCapabilityDocument;
102
+ export declare function collabCloudCapabilitySupported(document: CollabCloudCapabilityDocument, capability: CollabCloudCapability): boolean;
103
+ export declare function collabCloudSuccessEnvelope<T>(requestId: string, data: T): CollabCloudSuccessEnvelope<T>;
104
+ export declare function decodeCollabCloudSuccessEnvelope<T = unknown>(value: unknown): CollabCloudSuccessEnvelope<T>;
105
+ export declare function collabCloudErrorEnvelope(requestId: string, error: CollabError): CollabCloudErrorEnvelope;
106
+ export declare function decodeCollabCloudErrorEnvelope(value: unknown): CollabCloudErrorEnvelope;
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COLLAB_CLOUD_BINDING_LIMITS = exports.COLLAB_CLOUD_JSON_OPERATIONS = exports.COLLAB_CLOUD_CAPABILITIES = exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = exports.COLLAB_CLOUD_BINDING_VERSION = void 0;
4
+ exports.collabCloudCapabilitiesRoute = collabCloudCapabilitiesRoute;
5
+ exports.collabCloudProjectOperationRoute = collabCloudProjectOperationRoute;
6
+ exports.collabCloudProjectEventsRoute = collabCloudProjectEventsRoute;
7
+ exports.collabCloudGitRoute = collabCloudGitRoute;
8
+ exports.collabDevelopmentBootstrapRoute = collabDevelopmentBootstrapRoute;
9
+ exports.matchCollabCloudRoute = matchCollabCloudRoute;
10
+ exports.decodeCollabCloudCapabilityDocument = decodeCollabCloudCapabilityDocument;
11
+ exports.collabCloudCapabilityDocument = collabCloudCapabilityDocument;
12
+ exports.collabCloudCapabilitySupported = collabCloudCapabilitySupported;
13
+ exports.collabCloudSuccessEnvelope = collabCloudSuccessEnvelope;
14
+ exports.decodeCollabCloudSuccessEnvelope = decodeCollabCloudSuccessEnvelope;
15
+ exports.collabCloudErrorEnvelope = collabCloudErrorEnvelope;
16
+ exports.decodeCollabCloudErrorEnvelope = decodeCollabCloudErrorEnvelope;
17
+ const CollabConstants_1 = require("./CollabConstants");
18
+ const CollabControlOperationCodecs_1 = require("./CollabControlOperationCodecs");
19
+ const CollabError_1 = require("./CollabError");
20
+ const CollabValidation_1 = require("./CollabValidation");
21
+ exports.COLLAB_CLOUD_BINDING_VERSION = 1;
22
+ exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION = 1;
23
+ exports.COLLAB_CLOUD_CAPABILITIES = Object.freeze([
24
+ 'accept',
25
+ 'development-bootstrap',
26
+ 'git-receive-pack-personal-ref',
27
+ 'git-upload-pack',
28
+ 'project-events',
29
+ 'project-snapshot',
30
+ 'requests',
31
+ 'tickets',
32
+ ]);
33
+ exports.COLLAB_CLOUD_JSON_OPERATIONS = Object.freeze([
34
+ 'getProjectSnapshot',
35
+ ...Object.keys(CollabControlOperationCodecs_1.COLLAB_CONTROL_OPERATION_CODECS),
36
+ ]);
37
+ exports.COLLAB_CLOUD_BINDING_LIMITS = Object.freeze({
38
+ bootstrapAttemptTtlMs: 24 * 60 * 60 * 1_000,
39
+ defaultMaxConcurrentBootstrapUploads: 1,
40
+ eventHeartbeatMs: 30_000,
41
+ eventMissedHeartbeatLimit: 2,
42
+ maxCloudOpenRequests: 100,
43
+ maxCloudProjectMembers: 100,
44
+ maxCloudSnapshotUtf8Bytes: 448 * 1024,
45
+ maxCloudTicketHighlights: 5,
46
+ maxDevelopmentBootstrapGitBundleBytes: 1024 * 1024 * 1024,
47
+ maxDevelopmentBootstrapManifestUtf8Bytes: 64 * 1024,
48
+ maxDevelopmentBootstrapReportUtf8Bytes: 64 * 1024,
49
+ maxDevelopmentBootstrapRepositoryBytes: 1024 * 1024 * 1024,
50
+ maxDevelopmentBootstrapStagingBytes: 2 * 1024 * 1024 * 1024,
51
+ maxEventReplay: 500,
52
+ maxGitReceivePackBytes: 256 * 1024 * 1024,
53
+ maxRepositoryBytes: 1024 * 1024 * 1024,
54
+ minRetainedEventCount: 10_000,
55
+ maxUploadsPerBootstrapAttempt: 1,
56
+ minEventRetentionDays: 30,
57
+ uploadDeadlineMs: 15 * 60 * 1_000,
58
+ uploadIdleTimeoutMs: 30_000,
59
+ });
60
+ const CLOUD_CAPABILITY_SET = new Set(exports.COLLAB_CLOUD_CAPABILITIES);
61
+ const CLOUD_JSON_OPERATION_SET = new Set(exports.COLLAB_CLOUD_JSON_OPERATIONS);
62
+ const COLLAB_ERROR_CODE_SET = new Set(CollabError_1.COLLAB_ERROR_CODES);
63
+ const RECOVERY_ACTION_SET = new Set([
64
+ 'retry',
65
+ 'review-conflicts',
66
+ 'request-access',
67
+ ]);
68
+ const CAPABILITY_TOKEN_PATTERN = /^[a-z][a-z0-9-]{0,63}$/u;
69
+ function invalidPayload(field) {
70
+ return new CollabError_1.CollabError({
71
+ code: 'protocol-payload-invalid',
72
+ safeContext: { field },
73
+ });
74
+ }
75
+ function invalidRoute() {
76
+ throw new RangeError('Invalid Collab Cloud route input');
77
+ }
78
+ function record(value, field) {
79
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
80
+ throw invalidPayload(field);
81
+ }
82
+ return value;
83
+ }
84
+ function hasExactKeys(value, keys) {
85
+ const expected = new Set(keys);
86
+ return keys.every(key => Object.hasOwn(value, key))
87
+ && Object.keys(value).every(key => expected.has(key));
88
+ }
89
+ function exactRecord(value, field, keys) {
90
+ const source = record(value, field);
91
+ if (!hasExactKeys(source, keys))
92
+ throw invalidPayload(field);
93
+ return source;
94
+ }
95
+ function positiveSafeInteger(value, field, maximum) {
96
+ if (typeof value !== 'number'
97
+ || !Number.isSafeInteger(value)
98
+ || value < 1
99
+ || (maximum !== undefined && value > maximum)) {
100
+ throw invalidPayload(field);
101
+ }
102
+ return value;
103
+ }
104
+ function sortedUniqueIntegers(value, field) {
105
+ if (!Array.isArray(value) || value.length === 0)
106
+ throw invalidPayload(field);
107
+ const decoded = value.map(item => positiveSafeInteger(item, field));
108
+ if (decoded.some((item, index) => index > 0 && decoded[index - 1] >= item)) {
109
+ throw invalidPayload(field);
110
+ }
111
+ return Object.freeze(decoded);
112
+ }
113
+ function sortedUniqueTokens(value, field) {
114
+ if (!Array.isArray(value))
115
+ throw invalidPayload(field);
116
+ const decoded = value.map((item) => {
117
+ if (typeof item !== 'string' || !CAPABILITY_TOKEN_PATTERN.test(item)) {
118
+ throw invalidPayload(field);
119
+ }
120
+ return item;
121
+ });
122
+ if (decoded.some((item, index) => index > 0 && decoded[index - 1].localeCompare(item, 'en-US') >= 0)) {
123
+ throw invalidPayload(field);
124
+ }
125
+ return Object.freeze(decoded);
126
+ }
127
+ function requireSupportedVersion(versions, supportedVersion, kind) {
128
+ if (versions.length === 1 && versions[0] === supportedVersion)
129
+ return;
130
+ throw new CollabError_1.CollabError({
131
+ code: 'protocol-version-unsupported',
132
+ safeContext: {
133
+ kind,
134
+ receivedVersion: versions.find(version => version !== supportedVersion)
135
+ ?? versions[0]
136
+ ?? 0,
137
+ supportedVersion,
138
+ },
139
+ });
140
+ }
141
+ function route(method, target, match) {
142
+ return Object.freeze({ match: Object.freeze(match), method, target });
143
+ }
144
+ function assertProjectId(projectId) {
145
+ if (!(0, CollabValidation_1.isCollabProjectId)(projectId))
146
+ invalidRoute();
147
+ }
148
+ function assertAttemptId(attemptId) {
149
+ if (!(0, CollabValidation_1.isCollabOpaqueId)(attemptId))
150
+ invalidRoute();
151
+ }
152
+ function isCloudJsonOperation(value) {
153
+ return CLOUD_JSON_OPERATION_SET.has(value);
154
+ }
155
+ function collabCloudCapabilitiesRoute() {
156
+ return route('GET', '/collab/capabilities', { kind: 'capabilities' });
157
+ }
158
+ function collabCloudProjectOperationRoute(projectId, operation) {
159
+ assertProjectId(projectId);
160
+ if (!isCloudJsonOperation(operation))
161
+ invalidRoute();
162
+ return route('POST', `/v1/projects/${projectId}/operations/${operation}`, {
163
+ kind: 'project-operation',
164
+ operation,
165
+ projectId,
166
+ });
167
+ }
168
+ function collabCloudProjectEventsRoute(projectId, afterSequence) {
169
+ assertProjectId(projectId);
170
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
171
+ invalidRoute();
172
+ return route('GET', `/v1/projects/${projectId}/events?afterSequence=${afterSequence}`, {
173
+ afterSequence,
174
+ kind: 'project-events',
175
+ projectId,
176
+ });
177
+ }
178
+ function collabCloudGitRoute(projectId, routeKind, service) {
179
+ assertProjectId(projectId);
180
+ if (routeKind === 'info-refs') {
181
+ if (service !== 'git-upload-pack' && service !== 'git-receive-pack')
182
+ invalidRoute();
183
+ return route('GET', `/v1/projects/${projectId}/repository.git/info/refs?service=${service}`, { kind: 'git-info-refs', projectId, service });
184
+ }
185
+ if (service !== undefined)
186
+ invalidRoute();
187
+ return route('POST', `/v1/projects/${projectId}/repository.git/${routeKind}`, {
188
+ kind: routeKind,
189
+ projectId,
190
+ });
191
+ }
192
+ function collabDevelopmentBootstrapRoute(operation, attemptId) {
193
+ const base = '/v1/development/bootstrap/attempts';
194
+ if (operation === 'beginDevelopmentBootstrap') {
195
+ if (attemptId !== undefined)
196
+ invalidRoute();
197
+ return route('POST', base, { kind: 'development-bootstrap', operation });
198
+ }
199
+ if (attemptId === undefined)
200
+ invalidRoute();
201
+ assertAttemptId(attemptId);
202
+ const suffixes = {
203
+ activateDevelopmentBootstrap: ['POST', 'activate'],
204
+ cancelDevelopmentBootstrap: ['POST', 'cancel'],
205
+ getDevelopmentBootstrap: ['GET', ''],
206
+ putDevelopmentBootstrapGitBundle: ['PUT', 'git-bundle'],
207
+ submitDevelopmentBootstrapReport: ['POST', 'reports'],
208
+ };
209
+ const binding = suffixes[operation];
210
+ if (!binding)
211
+ invalidRoute();
212
+ const [method, suffix] = binding;
213
+ const target = suffix ? `${base}/${attemptId}/${suffix}` : `${base}/${attemptId}`;
214
+ return route(method, target, {
215
+ attemptId,
216
+ kind: 'development-bootstrap',
217
+ operation,
218
+ });
219
+ }
220
+ function exactQuery(url, expectedKeys) {
221
+ const keys = [...url.searchParams.keys()];
222
+ return keys.length === expectedKeys.length
223
+ && expectedKeys.every(key => keys.filter(candidate => candidate === key).length === 1)
224
+ && keys.every(key => expectedKeys.includes(key))
225
+ && url.search === (expectedKeys.length === 0 ? '' : `?${url.searchParams.toString()}`);
226
+ }
227
+ function parseCanonicalNonNegativeInteger(value) {
228
+ if (value === null || !/^(?:0|[1-9][0-9]*)$/u.test(value))
229
+ return null;
230
+ const parsed = Number(value);
231
+ return Number.isSafeInteger(parsed) ? parsed : null;
232
+ }
233
+ function matchCollabCloudRoute(method, target) {
234
+ if (!target.startsWith('/') || target.includes('#'))
235
+ return null;
236
+ let url;
237
+ try {
238
+ url = new URL(target, 'http://collab.invalid');
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ const segments = url.pathname.split('/').filter(Boolean);
244
+ if (url.pathname !== `/${segments.join('/')}` || target.endsWith('?'))
245
+ return null;
246
+ if (method === 'GET'
247
+ && url.pathname === '/collab/capabilities'
248
+ && exactQuery(url, []))
249
+ return { kind: 'capabilities' };
250
+ if (segments[0] === 'v1'
251
+ && segments[1] === 'projects'
252
+ && (0, CollabValidation_1.isCollabProjectId)(segments[2])) {
253
+ const projectId = segments[2];
254
+ if (method === 'POST'
255
+ && segments.length === 5
256
+ && segments[3] === 'operations'
257
+ && isCloudJsonOperation(segments[4])
258
+ && exactQuery(url, [])) {
259
+ return { kind: 'project-operation', operation: segments[4], projectId };
260
+ }
261
+ if (method === 'GET'
262
+ && segments.length === 4
263
+ && segments[3] === 'events'
264
+ && exactQuery(url, ['afterSequence'])) {
265
+ const afterSequence = parseCanonicalNonNegativeInteger(url.searchParams.get('afterSequence'));
266
+ return afterSequence === null
267
+ ? null
268
+ : { afterSequence, kind: 'project-events', projectId };
269
+ }
270
+ if (method === 'GET'
271
+ && segments.length === 6
272
+ && segments[3] === 'repository.git'
273
+ && segments[4] === 'info'
274
+ && segments[5] === 'refs'
275
+ && exactQuery(url, ['service'])) {
276
+ const service = url.searchParams.get('service');
277
+ return service === 'git-upload-pack' || service === 'git-receive-pack'
278
+ ? { kind: 'git-info-refs', projectId, service }
279
+ : null;
280
+ }
281
+ if (method === 'POST'
282
+ && segments.length === 5
283
+ && segments[3] === 'repository.git'
284
+ && (segments[4] === 'git-upload-pack' || segments[4] === 'git-receive-pack')
285
+ && exactQuery(url, []))
286
+ return { kind: segments[4], projectId };
287
+ }
288
+ if (segments[0] !== 'v1'
289
+ || segments[1] !== 'development'
290
+ || segments[2] !== 'bootstrap'
291
+ || segments[3] !== 'attempts'
292
+ || !exactQuery(url, []))
293
+ return null;
294
+ if (method === 'POST' && segments.length === 4) {
295
+ return { kind: 'development-bootstrap', operation: 'beginDevelopmentBootstrap' };
296
+ }
297
+ const attemptId = segments[4];
298
+ if (!(0, CollabValidation_1.isCollabOpaqueId)(attemptId))
299
+ return null;
300
+ if (method === 'GET' && segments.length === 5) {
301
+ return { attemptId, kind: 'development-bootstrap', operation: 'getDevelopmentBootstrap' };
302
+ }
303
+ const operation = (segments.length === 6
304
+ ? {
305
+ activate: method === 'POST' ? 'activateDevelopmentBootstrap' : null,
306
+ cancel: method === 'POST' ? 'cancelDevelopmentBootstrap' : null,
307
+ 'git-bundle': method === 'PUT' ? 'putDevelopmentBootstrapGitBundle' : null,
308
+ reports: method === 'POST' ? 'submitDevelopmentBootstrapReport' : null,
309
+ }[segments[5]]
310
+ : null);
311
+ return operation
312
+ ? { attemptId, kind: 'development-bootstrap', operation }
313
+ : null;
314
+ }
315
+ function decodeCollabCloudCapabilityDocument(value) {
316
+ const source = exactRecord(value, 'capabilities', [
317
+ 'bindingVersions',
318
+ 'capabilities',
319
+ 'limits',
320
+ 'protocolVersions',
321
+ 'schemaVersion',
322
+ ]);
323
+ if (source.schemaVersion !== exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION) {
324
+ throw invalidPayload('schemaVersion');
325
+ }
326
+ const bindingVersions = sortedUniqueIntegers(source.bindingVersions, 'bindingVersions');
327
+ const protocolVersions = sortedUniqueIntegers(source.protocolVersions, 'protocolVersions');
328
+ requireSupportedVersion(bindingVersions, exports.COLLAB_CLOUD_BINDING_VERSION, 'cloud-binding');
329
+ requireSupportedVersion(protocolVersions, CollabConstants_1.COLLAB_PROTOCOL_VERSION, 'canonical-wire');
330
+ const capabilities = sortedUniqueTokens(source.capabilities, 'capabilities');
331
+ const limitsSource = exactRecord(source.limits, 'limits', [
332
+ 'maxDevelopmentBootstrapGitBundleBytes',
333
+ 'maxDevelopmentBootstrapManifestUtf8Bytes',
334
+ 'maxDevelopmentBootstrapReportUtf8Bytes',
335
+ 'maxEventReplay',
336
+ 'maxGitReceivePackBytes',
337
+ 'maxJsonPayloadUtf8Bytes',
338
+ 'maxRepositoryBytes',
339
+ ]);
340
+ const limits = Object.freeze({
341
+ maxDevelopmentBootstrapGitBundleBytes: positiveSafeInteger(limitsSource.maxDevelopmentBootstrapGitBundleBytes, 'maxDevelopmentBootstrapGitBundleBytes', exports.COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapGitBundleBytes),
342
+ maxDevelopmentBootstrapManifestUtf8Bytes: positiveSafeInteger(limitsSource.maxDevelopmentBootstrapManifestUtf8Bytes, 'maxDevelopmentBootstrapManifestUtf8Bytes', exports.COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapManifestUtf8Bytes),
343
+ maxDevelopmentBootstrapReportUtf8Bytes: positiveSafeInteger(limitsSource.maxDevelopmentBootstrapReportUtf8Bytes, 'maxDevelopmentBootstrapReportUtf8Bytes', exports.COLLAB_CLOUD_BINDING_LIMITS.maxDevelopmentBootstrapReportUtf8Bytes),
344
+ maxEventReplay: positiveSafeInteger(limitsSource.maxEventReplay, 'maxEventReplay', exports.COLLAB_CLOUD_BINDING_LIMITS.maxEventReplay),
345
+ maxGitReceivePackBytes: positiveSafeInteger(limitsSource.maxGitReceivePackBytes, 'maxGitReceivePackBytes', exports.COLLAB_CLOUD_BINDING_LIMITS.maxGitReceivePackBytes),
346
+ maxJsonPayloadUtf8Bytes: positiveSafeInteger(limitsSource.maxJsonPayloadUtf8Bytes, 'maxJsonPayloadUtf8Bytes', CollabConstants_1.COLLAB_LIMITS.maxJsonPayloadUtf8Bytes),
347
+ maxRepositoryBytes: positiveSafeInteger(limitsSource.maxRepositoryBytes, 'maxRepositoryBytes', exports.COLLAB_CLOUD_BINDING_LIMITS.maxRepositoryBytes),
348
+ });
349
+ return Object.freeze({
350
+ bindingVersions,
351
+ capabilities,
352
+ limits,
353
+ protocolVersions,
354
+ schemaVersion: exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION,
355
+ });
356
+ }
357
+ function collabCloudCapabilityDocument(capabilities, limits) {
358
+ if (!capabilities.every(capability => CLOUD_CAPABILITY_SET.has(capability))) {
359
+ throw invalidPayload('capabilities');
360
+ }
361
+ return decodeCollabCloudCapabilityDocument({
362
+ bindingVersions: [exports.COLLAB_CLOUD_BINDING_VERSION],
363
+ capabilities: [...capabilities].sort((left, right) => left.localeCompare(right, 'en-US')),
364
+ limits,
365
+ protocolVersions: [CollabConstants_1.COLLAB_PROTOCOL_VERSION],
366
+ schemaVersion: exports.COLLAB_CLOUD_CAPABILITY_DOCUMENT_SCHEMA_VERSION,
367
+ });
368
+ }
369
+ function collabCloudCapabilitySupported(document, capability) {
370
+ return CLOUD_CAPABILITY_SET.has(capability) && document.capabilities.includes(capability);
371
+ }
372
+ function collabCloudSuccessEnvelope(requestId, data) {
373
+ if (!(0, CollabValidation_1.isCollabOpaqueId)(requestId))
374
+ throw invalidPayload('requestId');
375
+ return Object.freeze({ data, protocolVersion: CollabConstants_1.COLLAB_PROTOCOL_VERSION, requestId });
376
+ }
377
+ function decodeCollabCloudSuccessEnvelope(value) {
378
+ const source = exactRecord(value, 'envelope', ['data', 'protocolVersion', 'requestId']);
379
+ if (source.protocolVersion !== CollabConstants_1.COLLAB_PROTOCOL_VERSION || !(0, CollabValidation_1.isCollabOpaqueId)(source.requestId)) {
380
+ throw invalidPayload('envelope');
381
+ }
382
+ return Object.freeze({
383
+ data: source.data,
384
+ protocolVersion: CollabConstants_1.COLLAB_PROTOCOL_VERSION,
385
+ requestId: source.requestId,
386
+ });
387
+ }
388
+ function collabCloudErrorEnvelope(requestId, error) {
389
+ if (!(0, CollabValidation_1.isCollabOpaqueId)(requestId))
390
+ throw invalidPayload('requestId');
391
+ return Object.freeze({
392
+ error: Object.freeze({
393
+ code: error.code,
394
+ recoveryActions: Object.freeze([...error.recoveryActions]),
395
+ safeContext: error.safeContext,
396
+ }),
397
+ protocolVersion: CollabConstants_1.COLLAB_PROTOCOL_VERSION,
398
+ requestId,
399
+ });
400
+ }
401
+ function decodeCollabCloudErrorEnvelope(value) {
402
+ const source = exactRecord(value, 'envelope', ['error', 'protocolVersion', 'requestId']);
403
+ const wireError = exactRecord(source.error, 'error', [
404
+ 'code',
405
+ 'recoveryActions',
406
+ 'safeContext',
407
+ ]);
408
+ if (source.protocolVersion !== CollabConstants_1.COLLAB_PROTOCOL_VERSION
409
+ || !(0, CollabValidation_1.isCollabOpaqueId)(source.requestId)
410
+ || typeof wireError.code !== 'string'
411
+ || !COLLAB_ERROR_CODE_SET.has(wireError.code)
412
+ || !Array.isArray(wireError.recoveryActions)
413
+ || !wireError.recoveryActions.every(action => (typeof action === 'string' && RECOVERY_ACTION_SET.has(action))))
414
+ throw invalidPayload('error');
415
+ const safeContextSource = record(wireError.safeContext, 'safeContext');
416
+ const safeContext = (0, CollabError_1.sanitizeCollabDiagnosticContext)(safeContextSource);
417
+ if (Object.keys(safeContextSource).length !== Object.keys(safeContext).length
418
+ || Object.entries(safeContextSource).some(([key, item]) => safeContext[key] !== item))
419
+ throw invalidPayload('safeContext');
420
+ return Object.freeze({
421
+ error: Object.freeze({
422
+ code: wireError.code,
423
+ recoveryActions: Object.freeze([
424
+ ...wireError.recoveryActions,
425
+ ]),
426
+ safeContext,
427
+ }),
428
+ protocolVersion: CollabConstants_1.COLLAB_PROTOCOL_VERSION,
429
+ requestId: source.requestId,
430
+ });
431
+ }