@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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/CollabCloudBinding.d.ts +106 -0
- package/dist/CollabCloudBinding.js +431 -0
- package/dist/CollabCloudProjectEvent.d.ts +41 -0
- package/dist/CollabCloudProjectEvent.js +133 -0
- package/dist/CollabCloudProjectSnapshot.d.ts +42 -0
- package/dist/CollabCloudProjectSnapshot.js +281 -0
- package/dist/CollabConstants.d.ts +32 -0
- package/dist/CollabConstants.js +34 -0
- package/dist/CollabControlOperationCodecs.d.ts +28 -0
- package/dist/CollabControlOperationCodecs.js +63 -0
- package/dist/CollabError.d.ts +27 -0
- package/dist/CollabError.js +165 -0
- package/dist/CollabMarkdownProse.d.ts +1 -0
- package/dist/CollabMarkdownProse.js +57 -0
- package/dist/CollabMemberMentionParser.d.ts +6 -0
- package/dist/CollabMemberMentionParser.js +51 -0
- package/dist/CollabProtocol.d.ts +144 -0
- package/dist/CollabProtocol.js +67 -0
- package/dist/CollabRequestTicketRequestCodecs.d.ts +5 -0
- package/dist/CollabRequestTicketRequestCodecs.js +266 -0
- package/dist/CollabRequestTicketResponseCodecs.d.ts +15 -0
- package/dist/CollabRequestTicketResponseCodecs.js +368 -0
- package/dist/CollabTicketReferenceParser.d.ts +24 -0
- package/dist/CollabTicketReferenceParser.js +56 -0
- package/dist/CollabValidation.d.ts +5 -0
- package/dist/CollabValidation.js +26 -0
- package/dist/DevelopmentBootstrap.d.ts +199 -0
- package/dist/DevelopmentBootstrap.js +544 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +67 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.js +11 -0
- 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
|
+
}
|