@company-semantics/contracts 39.1.0 → 39.3.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/package.json +15 -3
- package/src/api/README.md +2 -2
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +295 -0
- package/src/generated/openapi-routes.ts +4 -0
- package/src/guards/config.ts +22 -0
- package/src/index.ts +32 -0
- package/src/org/README.md +24 -0
- package/src/org/__tests__/company-md-collab.test.ts +290 -0
- package/src/org/company-md-collab.ts +337 -0
- package/src/org/company-md.ts +12 -0
- package/src/org/index.ts +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "39.
|
|
3
|
+
"version": "39.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
"generate:spec-hash": "tsx scripts/generate-spec-hash.ts",
|
|
109
109
|
"generate:spec-hash:check": "tsx scripts/generate-spec-hash.ts --check",
|
|
110
110
|
"generate:api": "pnpm generate:api-types && pnpm generate:spec-hash && pnpm generate:openapi-routes",
|
|
111
|
+
"generate:api:check": "pnpm generate:api-types:check && pnpm generate:spec-hash:check && pnpm generate:openapi-routes:check",
|
|
111
112
|
"generate:api-types": "openapi-typescript openapi/backend.yaml -o src/api/generated.ts",
|
|
112
113
|
"generate:openapi-routes": "tsx scripts/generate-openapi-routes.ts",
|
|
113
114
|
"generate:openapi-routes:check": "tsx scripts/generate-openapi-routes.ts --check",
|
|
@@ -138,17 +139,28 @@
|
|
|
138
139
|
"prettier": "^3.9.6",
|
|
139
140
|
"tsx": "^4.23.1",
|
|
140
141
|
"typescript": "^5.8.3",
|
|
142
|
+
"vite": "^7.3.6",
|
|
141
143
|
"vitest": "^4.1.10",
|
|
142
144
|
"yaml": "^2.9.0"
|
|
143
145
|
},
|
|
144
146
|
"pnpm": {
|
|
145
147
|
"overrides": {
|
|
146
|
-
"
|
|
148
|
+
"brace-expansion@<5.0.8": ">=5.0.8",
|
|
149
|
+
"minimatch@<10.2.3": ">=10.2.3",
|
|
150
|
+
"rollup@<4.59.0": ">=4.59.0",
|
|
151
|
+
"picomatch@<4.0.4": ">=4.0.4",
|
|
152
|
+
"esbuild@<0.28.1": ">=0.28.1",
|
|
153
|
+
"linkify-it@<5.0.2": ">=5.0.2",
|
|
154
|
+
"postcss@<8.5.18": ">=8.5.18",
|
|
155
|
+
"markdown-it@<14.2.0": ">=14.2.0",
|
|
156
|
+
"js-yaml@>=4.0.0 <4.3.0": ">=4.3.0 <5.0.0",
|
|
157
|
+
"js-yaml@>=5.0.0 <5.2.2": ">=5.2.2"
|
|
147
158
|
}
|
|
148
159
|
},
|
|
149
160
|
"lint-staged": {
|
|
150
161
|
"*.ts": "bash -c 'tsc -b --noEmit'",
|
|
151
162
|
"*.md": "markdownlint-cli2",
|
|
152
163
|
"package.json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\""
|
|
153
|
-
}
|
|
164
|
+
},
|
|
165
|
+
"securityRequirementsVersion": "e88ddf247895bafc36dc5d878bb1045dc522a2375683fe08e1b59a10f99e8766"
|
|
154
166
|
}
|
package/src/api/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Provides reusable functions that encode API response shapes shared between the c
|
|
|
12
12
|
- Response shapes must match the types defined in `src/mcp/` (e.g., `ToolDiscoveryResponse`)
|
|
13
13
|
- Capability graph inclusion is opt-in via query parameter convention (`?include=graph`)
|
|
14
14
|
- `generated.ts` contains only type exports — zero runtime imports, zero const/let/var/function declarations
|
|
15
|
-
- `generated.ts` must stay in sync with `openapi/backend.yaml` — run `pnpm generate:api
|
|
15
|
+
- `generated.ts` must stay in sync with `openapi/backend.yaml` — run `pnpm generate:api:check` to verify (it checks all three spec-derived artifacts, not just this one)
|
|
16
16
|
|
|
17
17
|
## Generated Types (`generated.ts`)
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ Auto-generated by `openapi-typescript` from `openapi/backend.yaml`. Exports thre
|
|
|
22
22
|
- `components` — All schema types (MeResponse, WorkspaceOverview, etc.)
|
|
23
23
|
- `operations` — Operation-level types keyed by operationId
|
|
24
24
|
|
|
25
|
-
Regenerate with `pnpm generate:api-types
|
|
25
|
+
Regenerate with `pnpm generate:api` — never `pnpm generate:api-types` alone. Three artifacts derive from `openapi/backend.yaml` (this file, `generated-spec-hash.ts`, and `src/generated/openapi-routes.ts`); regenerating one leaves the others stale. Check for staleness with `pnpm generate:api:check`.
|
|
26
26
|
|
|
27
27
|
The file is committed to the repo (not .gitignored) so consumers can import types without running the generator, and type changes are visible in code review diffs.
|
|
28
28
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '578211b8b148' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '578211b8b1489b1d26938629c14b09a94ee3bf636085a5f058f1b573747f8fce' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -1457,6 +1457,77 @@ export interface paths {
|
|
|
1457
1457
|
patch?: never;
|
|
1458
1458
|
trace?: never;
|
|
1459
1459
|
};
|
|
1460
|
+
"/api/company-md/docs/{id}/collab/sync": {
|
|
1461
|
+
parameters: {
|
|
1462
|
+
query?: never;
|
|
1463
|
+
header?: never;
|
|
1464
|
+
path?: never;
|
|
1465
|
+
cookie?: never;
|
|
1466
|
+
};
|
|
1467
|
+
/** Get merged collaboration state for a company.md document body */
|
|
1468
|
+
get: operations["getCompanyMdDocCollabSync"];
|
|
1469
|
+
put?: never;
|
|
1470
|
+
post?: never;
|
|
1471
|
+
delete?: never;
|
|
1472
|
+
options?: never;
|
|
1473
|
+
head?: never;
|
|
1474
|
+
patch?: never;
|
|
1475
|
+
trace?: never;
|
|
1476
|
+
};
|
|
1477
|
+
"/api/company-md/docs/{id}/collab/updates": {
|
|
1478
|
+
parameters: {
|
|
1479
|
+
query?: never;
|
|
1480
|
+
header?: never;
|
|
1481
|
+
path?: never;
|
|
1482
|
+
cookie?: never;
|
|
1483
|
+
};
|
|
1484
|
+
get?: never;
|
|
1485
|
+
put?: never;
|
|
1486
|
+
/** Append one Yjs update to a company.md document’s collaboration log */
|
|
1487
|
+
post: operations["appendCompanyMdDocCollabUpdate"];
|
|
1488
|
+
delete?: never;
|
|
1489
|
+
options?: never;
|
|
1490
|
+
head?: never;
|
|
1491
|
+
patch?: never;
|
|
1492
|
+
trace?: never;
|
|
1493
|
+
};
|
|
1494
|
+
"/api/company-md/docs/{id}/collab/presence": {
|
|
1495
|
+
parameters: {
|
|
1496
|
+
query?: never;
|
|
1497
|
+
header?: never;
|
|
1498
|
+
path?: never;
|
|
1499
|
+
cookie?: never;
|
|
1500
|
+
};
|
|
1501
|
+
get?: never;
|
|
1502
|
+
put?: never;
|
|
1503
|
+
/** Signal presence (cursor position, status, departure) on a company.md document */
|
|
1504
|
+
post: operations["postCompanyMdDocCollabPresence"];
|
|
1505
|
+
delete?: never;
|
|
1506
|
+
options?: never;
|
|
1507
|
+
head?: never;
|
|
1508
|
+
patch?: never;
|
|
1509
|
+
trace?: never;
|
|
1510
|
+
};
|
|
1511
|
+
"/api/company-md/docs/{id}/collab/stream": {
|
|
1512
|
+
parameters: {
|
|
1513
|
+
query?: never;
|
|
1514
|
+
header?: never;
|
|
1515
|
+
path?: never;
|
|
1516
|
+
cookie?: never;
|
|
1517
|
+
};
|
|
1518
|
+
/**
|
|
1519
|
+
* Stream company.md collaboration updates via SSE
|
|
1520
|
+
* @description Server-Sent Events stream of collaboration updates for a company.md document body. x-streaming: true. Durable frames are `event: collab-update` with `id: <seq>` (decimal string); resume via `?after=<seq>&epoch=<n>` from /collab/sync, or the Last-Event-ID header on reconnect. An unusable cursor gets `event: collab-reset` and the client refetches /collab/sync. See ADR-BE-439.
|
|
1521
|
+
*/
|
|
1522
|
+
get: operations["streamCompanyMdDocCollab"];
|
|
1523
|
+
put?: never;
|
|
1524
|
+
post?: never;
|
|
1525
|
+
delete?: never;
|
|
1526
|
+
options?: never;
|
|
1527
|
+
head?: never;
|
|
1528
|
+
patch?: never;
|
|
1529
|
+
trace?: never;
|
|
1530
|
+
};
|
|
1460
1531
|
"/api/company-md/extract": {
|
|
1461
1532
|
parameters: {
|
|
1462
1533
|
query?: never;
|
|
@@ -4578,6 +4649,48 @@ export interface components {
|
|
|
4578
4649
|
UpdateCompanyMdTitleRequest: {
|
|
4579
4650
|
title: string;
|
|
4580
4651
|
};
|
|
4652
|
+
/** @description Merged collaboration state plus the cursor the stream resumes from. */
|
|
4653
|
+
CollabSyncResponse: {
|
|
4654
|
+
epoch: number;
|
|
4655
|
+
seq: string;
|
|
4656
|
+
update: string;
|
|
4657
|
+
reset: boolean;
|
|
4658
|
+
editable: boolean;
|
|
4659
|
+
limits: components["schemas"]["CollabSyncLimits"];
|
|
4660
|
+
};
|
|
4661
|
+
/** @description The position the update was assigned, and whether it was a replayed retry. */
|
|
4662
|
+
CollabAppendUpdateResponse: {
|
|
4663
|
+
seq: string;
|
|
4664
|
+
duplicate: boolean;
|
|
4665
|
+
};
|
|
4666
|
+
/** @description One Yjs update plus the generation and idempotency pair it belongs to. */
|
|
4667
|
+
CollabAppendUpdateRequest: {
|
|
4668
|
+
epoch: number;
|
|
4669
|
+
/** Format: uuid */
|
|
4670
|
+
clientKey: string;
|
|
4671
|
+
/** Format: uuid */
|
|
4672
|
+
clientUpdateId: string;
|
|
4673
|
+
update: string;
|
|
4674
|
+
};
|
|
4675
|
+
/** @description One presence signal: position blobs and a status for the caller’s own editor instance. Identity is stamped server-side. */
|
|
4676
|
+
CollabPresenceRequest: {
|
|
4677
|
+
/** Format: uuid */
|
|
4678
|
+
clientKey: string;
|
|
4679
|
+
/** @enum {string} */
|
|
4680
|
+
status: "editing" | "viewing";
|
|
4681
|
+
anchor?: string;
|
|
4682
|
+
head?: string;
|
|
4683
|
+
/** @constant */
|
|
4684
|
+
gone?: true;
|
|
4685
|
+
};
|
|
4686
|
+
/** @description One durable collaboration update, as streamed over SSE. */
|
|
4687
|
+
CollabStreamUpdateFrame: {
|
|
4688
|
+
/** @constant */
|
|
4689
|
+
v: 1;
|
|
4690
|
+
epoch: number;
|
|
4691
|
+
seq: string;
|
|
4692
|
+
update: string;
|
|
4693
|
+
};
|
|
4581
4694
|
CompanyMdContextBankResponse: {
|
|
4582
4695
|
items: {
|
|
4583
4696
|
id: string;
|
|
@@ -5882,6 +5995,11 @@ export interface components {
|
|
|
5882
5995
|
userAgent: string;
|
|
5883
5996
|
} | null;
|
|
5884
5997
|
};
|
|
5998
|
+
/** @description Server-enforced collaboration size limits, published so clients can pre-check. */
|
|
5999
|
+
CollabSyncLimits: {
|
|
6000
|
+
maxUpdateBytes: number;
|
|
6001
|
+
maxDocTextBytes: number;
|
|
6002
|
+
};
|
|
5885
6003
|
/** @description An ACL-admitted context-doc discovery hit. */
|
|
5886
6004
|
ContextDocDiscoveryHit: {
|
|
5887
6005
|
id: string;
|
|
@@ -8117,6 +8235,183 @@ export interface operations {
|
|
|
8117
8235
|
};
|
|
8118
8236
|
};
|
|
8119
8237
|
};
|
|
8238
|
+
getCompanyMdDocCollabSync: {
|
|
8239
|
+
parameters: {
|
|
8240
|
+
query?: {
|
|
8241
|
+
since?: string;
|
|
8242
|
+
epoch?: number;
|
|
8243
|
+
};
|
|
8244
|
+
header?: never;
|
|
8245
|
+
path: {
|
|
8246
|
+
id: string;
|
|
8247
|
+
};
|
|
8248
|
+
cookie?: never;
|
|
8249
|
+
};
|
|
8250
|
+
requestBody?: never;
|
|
8251
|
+
responses: {
|
|
8252
|
+
/** @description Merged collaboration state plus the cursor the stream resumes from */
|
|
8253
|
+
200: {
|
|
8254
|
+
headers: {
|
|
8255
|
+
[name: string]: unknown;
|
|
8256
|
+
};
|
|
8257
|
+
content: {
|
|
8258
|
+
"application/json": components["schemas"]["CollabSyncResponse"];
|
|
8259
|
+
};
|
|
8260
|
+
};
|
|
8261
|
+
/** @description Document not found, or the caller may not read its body */
|
|
8262
|
+
404: {
|
|
8263
|
+
headers: {
|
|
8264
|
+
[name: string]: unknown;
|
|
8265
|
+
};
|
|
8266
|
+
content?: never;
|
|
8267
|
+
};
|
|
8268
|
+
/** @description The supplied cursor is ahead of this document’s log; resync */
|
|
8269
|
+
409: {
|
|
8270
|
+
headers: {
|
|
8271
|
+
[name: string]: unknown;
|
|
8272
|
+
};
|
|
8273
|
+
content?: never;
|
|
8274
|
+
};
|
|
8275
|
+
};
|
|
8276
|
+
};
|
|
8277
|
+
appendCompanyMdDocCollabUpdate: {
|
|
8278
|
+
parameters: {
|
|
8279
|
+
query?: never;
|
|
8280
|
+
header?: never;
|
|
8281
|
+
path: {
|
|
8282
|
+
id: string;
|
|
8283
|
+
};
|
|
8284
|
+
cookie?: never;
|
|
8285
|
+
};
|
|
8286
|
+
requestBody: {
|
|
8287
|
+
content: {
|
|
8288
|
+
"application/json": components["schemas"]["CollabAppendUpdateRequest"];
|
|
8289
|
+
};
|
|
8290
|
+
};
|
|
8291
|
+
responses: {
|
|
8292
|
+
/** @description The position the update was assigned, and whether it was a replayed retry */
|
|
8293
|
+
200: {
|
|
8294
|
+
headers: {
|
|
8295
|
+
[name: string]: unknown;
|
|
8296
|
+
};
|
|
8297
|
+
content: {
|
|
8298
|
+
"application/json": components["schemas"]["CollabAppendUpdateResponse"];
|
|
8299
|
+
};
|
|
8300
|
+
};
|
|
8301
|
+
/** @description Malformed body, or a payload that does not decode as a Yjs update */
|
|
8302
|
+
400: {
|
|
8303
|
+
headers: {
|
|
8304
|
+
[name: string]: unknown;
|
|
8305
|
+
};
|
|
8306
|
+
content?: never;
|
|
8307
|
+
};
|
|
8308
|
+
/** @description Document not found, or the caller may not edit it */
|
|
8309
|
+
404: {
|
|
8310
|
+
headers: {
|
|
8311
|
+
[name: string]: unknown;
|
|
8312
|
+
};
|
|
8313
|
+
content?: never;
|
|
8314
|
+
};
|
|
8315
|
+
/** @description Epoch mismatch, or the document was never activated for collaboration */
|
|
8316
|
+
409: {
|
|
8317
|
+
headers: {
|
|
8318
|
+
[name: string]: unknown;
|
|
8319
|
+
};
|
|
8320
|
+
content?: never;
|
|
8321
|
+
};
|
|
8322
|
+
/** @description Applying the update would exceed the document text cap; not logged */
|
|
8323
|
+
413: {
|
|
8324
|
+
headers: {
|
|
8325
|
+
[name: string]: unknown;
|
|
8326
|
+
};
|
|
8327
|
+
content?: never;
|
|
8328
|
+
};
|
|
8329
|
+
/** @description Too many uncompacted updates pending; retry after backoff */
|
|
8330
|
+
429: {
|
|
8331
|
+
headers: {
|
|
8332
|
+
[name: string]: unknown;
|
|
8333
|
+
};
|
|
8334
|
+
content?: never;
|
|
8335
|
+
};
|
|
8336
|
+
};
|
|
8337
|
+
};
|
|
8338
|
+
postCompanyMdDocCollabPresence: {
|
|
8339
|
+
parameters: {
|
|
8340
|
+
query?: never;
|
|
8341
|
+
header?: never;
|
|
8342
|
+
path: {
|
|
8343
|
+
id: string;
|
|
8344
|
+
};
|
|
8345
|
+
cookie?: never;
|
|
8346
|
+
};
|
|
8347
|
+
requestBody: {
|
|
8348
|
+
content: {
|
|
8349
|
+
"application/json": components["schemas"]["CollabPresenceRequest"];
|
|
8350
|
+
};
|
|
8351
|
+
};
|
|
8352
|
+
responses: {
|
|
8353
|
+
/** @description Presence signal accepted (ephemeral; nothing durable written) */
|
|
8354
|
+
204: {
|
|
8355
|
+
headers: {
|
|
8356
|
+
[name: string]: unknown;
|
|
8357
|
+
};
|
|
8358
|
+
content?: never;
|
|
8359
|
+
};
|
|
8360
|
+
/** @description Malformed body (bad clientKey, status, or oversized position blob) */
|
|
8361
|
+
400: {
|
|
8362
|
+
headers: {
|
|
8363
|
+
[name: string]: unknown;
|
|
8364
|
+
};
|
|
8365
|
+
content?: never;
|
|
8366
|
+
};
|
|
8367
|
+
/** @description The clientKey is bound to a different user on this document */
|
|
8368
|
+
403: {
|
|
8369
|
+
headers: {
|
|
8370
|
+
[name: string]: unknown;
|
|
8371
|
+
};
|
|
8372
|
+
content?: never;
|
|
8373
|
+
};
|
|
8374
|
+
/** @description Document not found, or the caller may not read its body */
|
|
8375
|
+
404: {
|
|
8376
|
+
headers: {
|
|
8377
|
+
[name: string]: unknown;
|
|
8378
|
+
};
|
|
8379
|
+
content?: never;
|
|
8380
|
+
};
|
|
8381
|
+
};
|
|
8382
|
+
};
|
|
8383
|
+
streamCompanyMdDocCollab: {
|
|
8384
|
+
parameters: {
|
|
8385
|
+
query?: {
|
|
8386
|
+
after?: string;
|
|
8387
|
+
epoch?: number;
|
|
8388
|
+
};
|
|
8389
|
+
header?: never;
|
|
8390
|
+
path: {
|
|
8391
|
+
id: string;
|
|
8392
|
+
};
|
|
8393
|
+
cookie?: never;
|
|
8394
|
+
};
|
|
8395
|
+
requestBody?: never;
|
|
8396
|
+
responses: {
|
|
8397
|
+
/** @description SSE event stream (text/event-stream). Durable frame payload: CollabStreamUpdateFrame. */
|
|
8398
|
+
200: {
|
|
8399
|
+
headers: {
|
|
8400
|
+
[name: string]: unknown;
|
|
8401
|
+
};
|
|
8402
|
+
content: {
|
|
8403
|
+
"text/event-stream": components["schemas"]["CollabStreamUpdateFrame"];
|
|
8404
|
+
};
|
|
8405
|
+
};
|
|
8406
|
+
/** @description Document not found, or the caller may not read its body */
|
|
8407
|
+
404: {
|
|
8408
|
+
headers: {
|
|
8409
|
+
[name: string]: unknown;
|
|
8410
|
+
};
|
|
8411
|
+
content?: never;
|
|
8412
|
+
};
|
|
8413
|
+
};
|
|
8414
|
+
};
|
|
8120
8415
|
extractCompanyMd: {
|
|
8121
8416
|
parameters: {
|
|
8122
8417
|
query?: never;
|
|
@@ -30,6 +30,10 @@ export const openApiRoutes = {
|
|
|
30
30
|
'/api/company-md/docs/:id/context-bank/suggestions': ['GET'],
|
|
31
31
|
'/api/company-md/docs/{id}': ['GET'],
|
|
32
32
|
'/api/company-md/docs/{id}/access-requests': ['GET', 'POST'],
|
|
33
|
+
'/api/company-md/docs/{id}/collab/presence': ['POST'],
|
|
34
|
+
'/api/company-md/docs/{id}/collab/stream': ['GET'],
|
|
35
|
+
'/api/company-md/docs/{id}/collab/sync': ['GET'],
|
|
36
|
+
'/api/company-md/docs/{id}/collab/updates': ['POST'],
|
|
33
37
|
'/api/company-md/docs/{id}/content': ['PUT'],
|
|
34
38
|
'/api/company-md/docs/{id}/context-bank': ['GET'],
|
|
35
39
|
'/api/company-md/docs/{id}/context-bank/associate': ['POST'],
|
package/src/guards/config.ts
CHANGED
|
@@ -238,6 +238,21 @@ export interface ContractsConsumerBaseline {
|
|
|
238
238
|
packageJsonPath: string;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Security-overrides-sync guard baseline configuration.
|
|
243
|
+
* CI injects checkOverridesSync based on this config.
|
|
244
|
+
*
|
|
245
|
+
* Verifies the repo never RESOLVES a package below the ci security-version
|
|
246
|
+
* policy (company-semantics-ci/dependency-policy/security-requirements.ts). The
|
|
247
|
+
* requirement set is central in ci — a repo declares only where its manifest is.
|
|
248
|
+
*
|
|
249
|
+
* @see ADR-CI-122 (policy + resolver + guard) and ADR-CTRL-273 (sync driver)
|
|
250
|
+
*/
|
|
251
|
+
export interface SecurityOverridesBaseline {
|
|
252
|
+
/** Path to package.json relative to repo root */
|
|
253
|
+
packageJsonPath: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
/**
|
|
242
257
|
* Structural guard baselines.
|
|
243
258
|
* Product repos provide DATA only; CI orchestrator owns guard implementations.
|
|
@@ -264,6 +279,13 @@ export interface StructuralBaselines {
|
|
|
264
279
|
* Enforces: consumers never advance ahead of npm.
|
|
265
280
|
*/
|
|
266
281
|
contractsConsumer?: ContractsConsumerBaseline;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Security-overrides-sync check configuration.
|
|
285
|
+
* CI injects checkOverridesSync based on this config.
|
|
286
|
+
* Enforces: no package resolves below the ci security-version policy.
|
|
287
|
+
*/
|
|
288
|
+
securityOverrides?: SecurityOverridesBaseline;
|
|
267
289
|
}
|
|
268
290
|
|
|
269
291
|
/**
|
package/src/index.ts
CHANGED
|
@@ -418,6 +418,38 @@ export type {
|
|
|
418
418
|
export { SourceAuthoritySchema } from "./org/index";
|
|
419
419
|
export type { SourceAuthority } from "./org/index";
|
|
420
420
|
|
|
421
|
+
// Company.md real-time collaboration wire contract: sync/update-accepted
|
|
422
|
+
// responses, the SSE frame union, the Y.Doc shape constant, and the published
|
|
423
|
+
// size limits. Sequences are ALWAYS decimal strings. (ADR-CONT-102)
|
|
424
|
+
export {
|
|
425
|
+
COMPANY_MD_COLLAB_TEXT_KEY,
|
|
426
|
+
COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS,
|
|
427
|
+
COMPANY_MD_COLLAB_MAX_DOC_TEXT_BYTES,
|
|
428
|
+
COMPANY_MD_COLLAB_MAX_PRESENCE_POSITION_CHARS,
|
|
429
|
+
CompanyMdCollabSyncLimitsSchema,
|
|
430
|
+
CompanyMdCollabSyncResponseSchema,
|
|
431
|
+
CompanyMdCollabUpdateAcceptedSchema,
|
|
432
|
+
CompanyMdCollabUpdateEventSchema,
|
|
433
|
+
CompanyMdCollabPresenceEventSchema,
|
|
434
|
+
CompanyMdCollabResetEventSchema,
|
|
435
|
+
CompanyMdCollabAccessRevokedEventSchema,
|
|
436
|
+
CompanyMdCollabConnectedEventSchema,
|
|
437
|
+
CompanyMdCollabServerDrainEventSchema,
|
|
438
|
+
CompanyMdCollabSseEventSchema,
|
|
439
|
+
} from "./org/index";
|
|
440
|
+
export type {
|
|
441
|
+
CompanyMdCollabSyncLimits,
|
|
442
|
+
CompanyMdCollabSyncResponse,
|
|
443
|
+
CompanyMdCollabUpdateAccepted,
|
|
444
|
+
CompanyMdCollabUpdateEvent,
|
|
445
|
+
CompanyMdCollabPresenceEvent,
|
|
446
|
+
CompanyMdCollabResetEvent,
|
|
447
|
+
CompanyMdCollabAccessRevokedEvent,
|
|
448
|
+
CompanyMdCollabConnectedEvent,
|
|
449
|
+
CompanyMdCollabServerDrainEvent,
|
|
450
|
+
CompanyMdCollabSseEvent,
|
|
451
|
+
} from "./org/index";
|
|
452
|
+
|
|
421
453
|
// Sync run summary: the outcome record of one adapter sync run (counts +
|
|
422
454
|
// cursor watermark). A flat report shape, not a structure fact. (ADR-CONT-085)
|
|
423
455
|
export { SyncRunSummarySchema } from "./org/index";
|
package/src/org/README.md
CHANGED
|
@@ -37,6 +37,10 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
37
37
|
- `AuthoritySource` _(type)_
|
|
38
38
|
- `AuthoritySourceSchema` — Engine-internal authority source.
|
|
39
39
|
- `AuthorizableView` _(type)_ — Type for views that can be checked against VIEWSCOPEMAP.
|
|
40
|
+
- `COMPANY_MD_COLLAB_MAX_DOC_TEXT_BYTES` — Cap on the MATERIALIZED document text, in UTF-8 bytes of the expanded result.
|
|
41
|
+
- `COMPANY_MD_COLLAB_MAX_PRESENCE_POSITION_CHARS` — Cap on ONE presence position blob (`anchor` or `head`), in base64 chars.
|
|
42
|
+
- `COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS` — Cap on ONE encoded update, measured on the base64 payload as it is sent.
|
|
43
|
+
- `COMPANY_MD_COLLAB_TEXT_KEY` — The Y.Doc root key holding the document body — a CONTRACT, not a detail.
|
|
40
44
|
- `CanonicalFacts` _(type)_
|
|
41
45
|
- `CanonicalFactsSchema` — The full normalized batch an external-source adapter emits in one pass: every person, org unit, position…
|
|
42
46
|
- `CanonicalOccupancy` _(type)_
|
|
@@ -55,6 +59,26 @@ Shared type vocabulary for organization ownership, type classification, and tran
|
|
|
55
59
|
- `ChangeMemberRoleResponse` _(type)_
|
|
56
60
|
- `ChangeMemberRoleResponseSchema`
|
|
57
61
|
- `CompanyMdClassification` _(type)_ — Information classification for a Company.md node — what the document is, for governance, NOT permissions.
|
|
62
|
+
- `CompanyMdCollabAccessRevokedEvent` _(type)_
|
|
63
|
+
- `CompanyMdCollabAccessRevokedEventSchema` — `event: access-revoked` — the actor's read grant went away mid-stream.
|
|
64
|
+
- `CompanyMdCollabConnectedEvent` _(type)_
|
|
65
|
+
- `CompanyMdCollabConnectedEventSchema` — `event: connected` — the stream is open.
|
|
66
|
+
- `CompanyMdCollabPresenceEvent` _(type)_
|
|
67
|
+
- `CompanyMdCollabPresenceEventSchema` — `event: collab-presence` — one participant's presence signal, either live or replayed from the roster right…
|
|
68
|
+
- `CompanyMdCollabResetEvent` _(type)_
|
|
69
|
+
- `CompanyMdCollabResetEventSchema` — `event: collab-reset` — the client's cursor was unusable (a superseded generation, older than what compaction…
|
|
70
|
+
- `CompanyMdCollabServerDrainEvent` _(type)_
|
|
71
|
+
- `CompanyMdCollabServerDrainEventSchema` — `event: server_drain` — the server is shutting the stream down deliberately (a rolling restart, not a…
|
|
72
|
+
- `CompanyMdCollabSseEvent` _(type)_
|
|
73
|
+
- `CompanyMdCollabSseEventSchema` — Every frame the collaboration stream emits, discriminated on the `type` the client stamps from the SSE…
|
|
74
|
+
- `CompanyMdCollabSyncLimits` _(type)_
|
|
75
|
+
- `CompanyMdCollabSyncLimitsSchema` — Protocol limits the client needs BEFORE it sends anything.
|
|
76
|
+
- `CompanyMdCollabSyncResponse` _(type)_
|
|
77
|
+
- `CompanyMdCollabSyncResponseSchema` — `GET /collab/sync` response body — merged collaboration state plus the cursor the stream resumes from.
|
|
78
|
+
- `CompanyMdCollabUpdateAccepted` _(type)_
|
|
79
|
+
- `CompanyMdCollabUpdateAcceptedSchema` — `POST /collab/updates` response body.
|
|
80
|
+
- `CompanyMdCollabUpdateEvent` _(type)_
|
|
81
|
+
- `CompanyMdCollabUpdateEventSchema` — `event: collab-update` — one durable collaboration update.
|
|
58
82
|
- `CompanyMdContextBankItem` _(type)_ — A context bank item — a company.md doc associated with one or more parent nodes.
|
|
59
83
|
- `CompanyMdDependency` _(type)_
|
|
60
84
|
- `CompanyMdDiscoverabilityPolicy` _(type)_ — Discoverability policy for a Company.md node — who knows it exists, orthogonal to who may read it (authority…
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
COMPANY_MD_COLLAB_TEXT_KEY,
|
|
5
|
+
COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS,
|
|
6
|
+
COMPANY_MD_COLLAB_MAX_DOC_TEXT_BYTES,
|
|
7
|
+
COMPANY_MD_COLLAB_MAX_PRESENCE_POSITION_CHARS,
|
|
8
|
+
CompanyMdCollabSyncResponseSchema,
|
|
9
|
+
CompanyMdCollabUpdateAcceptedSchema,
|
|
10
|
+
CompanyMdCollabSseEventSchema,
|
|
11
|
+
} from "../company-md-collab.js";
|
|
12
|
+
|
|
13
|
+
const DOC_ID = "9f1c7b62-2b3e-4d21-9a11-6a0c0b5e7c31";
|
|
14
|
+
const USER_ID = "3d0a2f18-5c44-4a90-8b7a-1d2e3f4a5b6c";
|
|
15
|
+
const CLIENT_KEY = "7c9e6679-7425-40de-944b-e07fc1f90ae7";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A sequence past 2^53. This is the whole reason seq is a string: as a JSON
|
|
19
|
+
* number it would round, and a rounded cursor resumes at the wrong position
|
|
20
|
+
* without failing.
|
|
21
|
+
*/
|
|
22
|
+
const BIG_SEQ = "9007199254740993";
|
|
23
|
+
|
|
24
|
+
describe("CompanyMdCollabSyncResponseSchema", () => {
|
|
25
|
+
const base = {
|
|
26
|
+
epoch: 3,
|
|
27
|
+
seq: "42",
|
|
28
|
+
update: "AQIDBA==",
|
|
29
|
+
editable: true,
|
|
30
|
+
reset: false,
|
|
31
|
+
limits: { maxUpdateBytes: 262144, maxDocTextBytes: 1000000 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
it("accepts the sync response the backend emits", () => {
|
|
35
|
+
expect(CompanyMdCollabSyncResponseSchema.parse(base)).toEqual(base);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("accepts a seq beyond the JS safe-integer range", () => {
|
|
39
|
+
const parsed = CompanyMdCollabSyncResponseSchema.parse({
|
|
40
|
+
...base,
|
|
41
|
+
seq: BIG_SEQ,
|
|
42
|
+
});
|
|
43
|
+
expect(parsed.seq).toBe(BIG_SEQ);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("accepts a response without reset (the field is additive-optional)", () => {
|
|
47
|
+
const { reset: _reset, ...withoutReset } = base;
|
|
48
|
+
expect(() =>
|
|
49
|
+
CompanyMdCollabSyncResponseSchema.parse(withoutReset),
|
|
50
|
+
).not.toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("rejects a numeric seq", () => {
|
|
54
|
+
expect(() =>
|
|
55
|
+
CompanyMdCollabSyncResponseSchema.parse({ ...base, seq: 42 }),
|
|
56
|
+
).toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects a non-digit seq", () => {
|
|
60
|
+
expect(() =>
|
|
61
|
+
CompanyMdCollabSyncResponseSchema.parse({ ...base, seq: "42n" }),
|
|
62
|
+
).toThrow();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("rejects a zero epoch", () => {
|
|
66
|
+
expect(() =>
|
|
67
|
+
CompanyMdCollabSyncResponseSchema.parse({ ...base, epoch: 0 }),
|
|
68
|
+
).toThrow();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("rejects a response missing limits", () => {
|
|
72
|
+
const { limits: _limits, ...withoutLimits } = base;
|
|
73
|
+
expect(() =>
|
|
74
|
+
CompanyMdCollabSyncResponseSchema.parse(withoutLimits),
|
|
75
|
+
).toThrow();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("CompanyMdCollabUpdateAcceptedSchema", () => {
|
|
80
|
+
it("accepts the append response the backend emits", () => {
|
|
81
|
+
const frame = { seq: "1024", duplicate: false };
|
|
82
|
+
expect(CompanyMdCollabUpdateAcceptedSchema.parse(frame)).toEqual(frame);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("rejects a numeric seq", () => {
|
|
86
|
+
expect(() =>
|
|
87
|
+
CompanyMdCollabUpdateAcceptedSchema.parse({
|
|
88
|
+
seq: 1024,
|
|
89
|
+
duplicate: false,
|
|
90
|
+
}),
|
|
91
|
+
).toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects a missing duplicate flag", () => {
|
|
95
|
+
expect(() =>
|
|
96
|
+
CompanyMdCollabUpdateAcceptedSchema.parse({ seq: "1024" }),
|
|
97
|
+
).toThrow();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The frames below are the exact `data:` bodies the backend writes, with `type`
|
|
103
|
+
* stamped from the SSE `event:` name — which is how a client feeds the union.
|
|
104
|
+
* If one of these stops parsing, the published contract has drifted from the
|
|
105
|
+
* stream.
|
|
106
|
+
*/
|
|
107
|
+
describe("CompanyMdCollabSseEventSchema — raw backend frames", () => {
|
|
108
|
+
it("parses a collab-update frame", () => {
|
|
109
|
+
const parsed = CompanyMdCollabSseEventSchema.parse({
|
|
110
|
+
type: "collab-update",
|
|
111
|
+
v: 1,
|
|
112
|
+
epoch: 3,
|
|
113
|
+
seq: BIG_SEQ,
|
|
114
|
+
update: "AQIDBA==",
|
|
115
|
+
});
|
|
116
|
+
expect(parsed.type).toBe("collab-update");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("parses a collab-presence frame with selection blobs", () => {
|
|
120
|
+
const parsed = CompanyMdCollabSseEventSchema.parse({
|
|
121
|
+
type: "collab-presence",
|
|
122
|
+
v: 1,
|
|
123
|
+
userId: USER_ID,
|
|
124
|
+
clientKey: CLIENT_KEY,
|
|
125
|
+
status: "editing",
|
|
126
|
+
anchor: "AQAB",
|
|
127
|
+
head: "AQAC",
|
|
128
|
+
});
|
|
129
|
+
expect(parsed.type).toBe("collab-presence");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("parses a collab-presence departure frame", () => {
|
|
133
|
+
expect(() =>
|
|
134
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
135
|
+
type: "collab-presence",
|
|
136
|
+
v: 1,
|
|
137
|
+
userId: USER_ID,
|
|
138
|
+
clientKey: CLIENT_KEY,
|
|
139
|
+
status: "viewing",
|
|
140
|
+
gone: true,
|
|
141
|
+
}),
|
|
142
|
+
).not.toThrow();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("parses a collab-reset frame for each reason the backend sends", () => {
|
|
146
|
+
for (const reason of ["epoch-mismatch", "compacted", "cursor-ahead"]) {
|
|
147
|
+
expect(() =>
|
|
148
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
149
|
+
type: "collab-reset",
|
|
150
|
+
v: 1,
|
|
151
|
+
reason,
|
|
152
|
+
epoch: 4,
|
|
153
|
+
resumeSeq: "77",
|
|
154
|
+
}),
|
|
155
|
+
).not.toThrow();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("parses an access-revoked frame with its empty body", () => {
|
|
160
|
+
const parsed = CompanyMdCollabSseEventSchema.parse({
|
|
161
|
+
type: "access-revoked",
|
|
162
|
+
});
|
|
163
|
+
expect(parsed.type).toBe("access-revoked");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("parses a connected frame with its empty body", () => {
|
|
167
|
+
expect(() =>
|
|
168
|
+
CompanyMdCollabSseEventSchema.parse({ type: "connected" }),
|
|
169
|
+
).not.toThrow();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("parses a server_drain frame", () => {
|
|
173
|
+
expect(() =>
|
|
174
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
175
|
+
type: "server_drain",
|
|
176
|
+
reason: "shutting_down",
|
|
177
|
+
}),
|
|
178
|
+
).not.toThrow();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("CompanyMdCollabSseEventSchema — optional attribution fields", () => {
|
|
183
|
+
it("accepts a collab-update carrying docId, author and origin", () => {
|
|
184
|
+
expect(() =>
|
|
185
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
186
|
+
type: "collab-update",
|
|
187
|
+
v: 1,
|
|
188
|
+
epoch: 3,
|
|
189
|
+
seq: "9",
|
|
190
|
+
update: "AQIDBA==",
|
|
191
|
+
docId: DOC_ID,
|
|
192
|
+
authorUserId: USER_ID,
|
|
193
|
+
origin: "client",
|
|
194
|
+
}),
|
|
195
|
+
).not.toThrow();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("accepts a server-originated collab-update with a null author", () => {
|
|
199
|
+
expect(() =>
|
|
200
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
201
|
+
type: "collab-update",
|
|
202
|
+
v: 1,
|
|
203
|
+
epoch: 3,
|
|
204
|
+
seq: "9",
|
|
205
|
+
update: "AQIDBA==",
|
|
206
|
+
authorUserId: null,
|
|
207
|
+
origin: "bridge",
|
|
208
|
+
}),
|
|
209
|
+
).not.toThrow();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("CompanyMdCollabSseEventSchema — rejections", () => {
|
|
214
|
+
it("rejects an unknown event type", () => {
|
|
215
|
+
expect(() =>
|
|
216
|
+
CompanyMdCollabSseEventSchema.parse({ type: "collab-yolo", v: 1 }),
|
|
217
|
+
).toThrow();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("rejects a frame with no type stamped on it", () => {
|
|
221
|
+
expect(() =>
|
|
222
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
223
|
+
v: 1,
|
|
224
|
+
epoch: 3,
|
|
225
|
+
seq: "9",
|
|
226
|
+
update: "AQIDBA==",
|
|
227
|
+
}),
|
|
228
|
+
).toThrow();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("rejects a numeric seq on collab-update", () => {
|
|
232
|
+
expect(() =>
|
|
233
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
234
|
+
type: "collab-update",
|
|
235
|
+
v: 1,
|
|
236
|
+
epoch: 3,
|
|
237
|
+
seq: 9,
|
|
238
|
+
update: "AQIDBA==",
|
|
239
|
+
}),
|
|
240
|
+
).toThrow();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("rejects a numeric resumeSeq on collab-reset", () => {
|
|
244
|
+
expect(() =>
|
|
245
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
246
|
+
type: "collab-reset",
|
|
247
|
+
v: 1,
|
|
248
|
+
reason: "compacted",
|
|
249
|
+
epoch: 4,
|
|
250
|
+
resumeSeq: 77,
|
|
251
|
+
}),
|
|
252
|
+
).toThrow();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("rejects an unknown presence status", () => {
|
|
256
|
+
expect(() =>
|
|
257
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
258
|
+
type: "collab-presence",
|
|
259
|
+
v: 1,
|
|
260
|
+
userId: USER_ID,
|
|
261
|
+
clientKey: CLIENT_KEY,
|
|
262
|
+
status: "idle",
|
|
263
|
+
}),
|
|
264
|
+
).toThrow();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("rejects a future frame version", () => {
|
|
268
|
+
expect(() =>
|
|
269
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
270
|
+
type: "collab-update",
|
|
271
|
+
v: 2,
|
|
272
|
+
epoch: 3,
|
|
273
|
+
seq: "9",
|
|
274
|
+
update: "AQIDBA==",
|
|
275
|
+
}),
|
|
276
|
+
).toThrow();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("collaboration protocol constants", () => {
|
|
281
|
+
it("names the Y.Doc root key the backend materializes text from", () => {
|
|
282
|
+
expect(COMPANY_MD_COLLAB_TEXT_KEY).toBe("content");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("publishes the size limits the server enforces", () => {
|
|
286
|
+
expect(COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS).toBe(256 * 1024);
|
|
287
|
+
expect(COMPANY_MD_COLLAB_MAX_DOC_TEXT_BYTES).toBe(1_000_000);
|
|
288
|
+
expect(COMPANY_MD_COLLAB_MAX_PRESENCE_POSITION_CHARS).toBe(512);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* company.md real-time collaboration — the published wire contract.
|
|
3
|
+
*
|
|
4
|
+
* The response and event vocabulary of the four `/api/company-md/docs/{id}/collab/*`
|
|
5
|
+
* routes. Promoted here because both the app (the editor and its transport) and
|
|
6
|
+
* the backend (the routes and the Y.Doc bridge) must agree on these shapes, and
|
|
7
|
+
* a client that guesses one of them fails silently rather than loudly. Request
|
|
8
|
+
* bodies deliberately stay backend-side per ADR-CONT-029 — only responses and
|
|
9
|
+
* broadcast events are the published promise.
|
|
10
|
+
*
|
|
11
|
+
* THE ONE RULE — every sequence value is a DECIMAL STRING, never a JSON number.
|
|
12
|
+
* Sequences are Postgres bigints; past 2^53 `JSON.parse` rounds them, and a
|
|
13
|
+
* rounded cursor does not fail, it silently resumes at the wrong position. So
|
|
14
|
+
* `seq` and `resumeSeq` are `z.string().regex(/^[0-9]+$/)` and a numeric value
|
|
15
|
+
* is rejected at the boundary.
|
|
16
|
+
*
|
|
17
|
+
* ON THE SSE UNION'S DISCRIMINATOR — the stream's frames do NOT carry a `type`
|
|
18
|
+
* field. The discriminator is the SSE `event:` NAME, and the `data:` body is the
|
|
19
|
+
* bare payload. A client therefore stamps `type` from the frame's event name
|
|
20
|
+
* before parsing:
|
|
21
|
+
*
|
|
22
|
+
* CompanyMdCollabSseEventSchema.parse({ type: ev.type, ...JSON.parse(ev.data) })
|
|
23
|
+
*
|
|
24
|
+
* The union is modeled on `type` anyway (the ChatSseEvent precedent) because
|
|
25
|
+
* that is the shape a consumer actually switches on. Fields the server does not
|
|
26
|
+
* put on the wire today are optional, so a stamped raw frame parses as-is.
|
|
27
|
+
*
|
|
28
|
+
* @see ADR-CONT-102 for what is promoted here and what deliberately is not.
|
|
29
|
+
* @see the backend protocol ADR (slug `company-md-collab-protocol`) for the
|
|
30
|
+
* authoritative protocol semantics these shapes describe.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Protocol constants
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Y.Doc root key holding the document body — a CONTRACT, not a detail.
|
|
41
|
+
*
|
|
42
|
+
* The server materializes text by reading this key and the bridge mutates the
|
|
43
|
+
* same type. A client that writes its body under a different root key gets its
|
|
44
|
+
* updates durably logged and permanently ignored: the log is correct, the
|
|
45
|
+
* stored `content` stays empty, and nothing errors anywhere. Title is
|
|
46
|
+
* deliberately NOT in the CRDT document.
|
|
47
|
+
*/
|
|
48
|
+
export const COMPANY_MD_COLLAB_TEXT_KEY = "content" as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Cap on ONE encoded update, measured on the base64 payload as it is sent.
|
|
52
|
+
* Published so an editor can refuse an oversized paste locally instead of
|
|
53
|
+
* discovering the cap on a rejected POST. Prefer the `limits` block on a live
|
|
54
|
+
* {@link CompanyMdCollabSyncResponseSchema} when one is in hand — this constant
|
|
55
|
+
* is the compile-time default for a client that has not synced yet.
|
|
56
|
+
*/
|
|
57
|
+
export const COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS = 262144 as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Cap on the MATERIALIZED document text, in UTF-8 bytes of the expanded result.
|
|
61
|
+
*
|
|
62
|
+
* Not redundant with {@link COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS}: a CRDT
|
|
63
|
+
* update is a DELTA, so a small delta can expand the document arbitrarily and a
|
|
64
|
+
* stream of individually-tiny updates can grow state without any single request
|
|
65
|
+
* looking suspicious. Only a measurement on the expanded text catches that.
|
|
66
|
+
*/
|
|
67
|
+
export const COMPANY_MD_COLLAB_MAX_DOC_TEXT_BYTES = 1_000_000 as const;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Cap on ONE presence position blob (`anchor` or `head`), in base64 chars.
|
|
71
|
+
* These are encoded Y.RelativePosition values — opaque POSITION data, a few
|
|
72
|
+
* dozen bytes in practice; the cap keeps a full presence envelope well under
|
|
73
|
+
* the transport's payload ceiling so an envelope is never truncated.
|
|
74
|
+
*/
|
|
75
|
+
export const COMPANY_MD_COLLAB_MAX_PRESENCE_POSITION_CHARS = 512 as const;
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Sequences
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A sequence number on the wire: decimal digits only, arbitrary precision.
|
|
83
|
+
* Module-private on purpose — consumers reach it through the field that uses
|
|
84
|
+
* it, so there is one place the rule can change.
|
|
85
|
+
*/
|
|
86
|
+
const CollabSeqString = z
|
|
87
|
+
.string()
|
|
88
|
+
.regex(
|
|
89
|
+
/^[0-9]+$/,
|
|
90
|
+
"seq must be a decimal string (bigint values are never JSON numbers)",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Response shapes
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Protocol limits the client needs BEFORE it sends anything. Published from the
|
|
99
|
+
* same constants the server enforces — a published limit that disagrees with
|
|
100
|
+
* the enforced one is worse than none, because the editor lets the user type
|
|
101
|
+
* and the server then rejects the result.
|
|
102
|
+
*/
|
|
103
|
+
export const CompanyMdCollabSyncLimitsSchema = z.object({
|
|
104
|
+
/** Max size of ONE base64-encoded update the server will accept, in bytes. */
|
|
105
|
+
maxUpdateBytes: z.number().int(),
|
|
106
|
+
/** Max size of the MATERIALIZED document text, in UTF-8 bytes. */
|
|
107
|
+
maxDocTextBytes: z.number().int(),
|
|
108
|
+
});
|
|
109
|
+
export type CompanyMdCollabSyncLimits = z.infer<
|
|
110
|
+
typeof CompanyMdCollabSyncLimitsSchema
|
|
111
|
+
>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `GET /collab/sync` response body — merged collaboration state plus the cursor
|
|
115
|
+
* the stream resumes from.
|
|
116
|
+
*
|
|
117
|
+
* `seq` is handed straight back to the stream as `?after=`, which is what closes
|
|
118
|
+
* the race between "fetched state" and "receiving live updates": the server
|
|
119
|
+
* serves everything strictly after that seq, so an update committed inside the
|
|
120
|
+
* window is re-sent rather than lost.
|
|
121
|
+
*/
|
|
122
|
+
export const CompanyMdCollabSyncResponseSchema = z.object({
|
|
123
|
+
/** The document's current collaboration generation. */
|
|
124
|
+
epoch: z.number().int().positive(),
|
|
125
|
+
/** Cursor this payload advances the client to — pass verbatim as `after=`. */
|
|
126
|
+
seq: CollabSeqString,
|
|
127
|
+
/** base64 Yjs bytes: a full snapshot when `reset`, otherwise a delta. */
|
|
128
|
+
update: z.string(),
|
|
129
|
+
/**
|
|
130
|
+
* Whether this actor may edit the body — surfaced so a client can mount a
|
|
131
|
+
* read-only editor instead of walking into a doomed first write. Signal only;
|
|
132
|
+
* it never affects admittance to the stream.
|
|
133
|
+
*/
|
|
134
|
+
editable: z.boolean(),
|
|
135
|
+
/**
|
|
136
|
+
* True when `update` is a full snapshot because the client's cursor was
|
|
137
|
+
* unusable (absent, from another generation, or older than what compaction
|
|
138
|
+
* still holds). Applying it is identical either way — Yjs merges losslessly —
|
|
139
|
+
* but anything the client queued against the old cursor is void.
|
|
140
|
+
*
|
|
141
|
+
* Optional on the published contract though the server always sends it: a
|
|
142
|
+
* reader that treats an absent value as `false` behaves correctly against
|
|
143
|
+
* every server that omits it, and this keeps the field addable to sibling
|
|
144
|
+
* responses later without a major.
|
|
145
|
+
*/
|
|
146
|
+
reset: z.boolean().optional(),
|
|
147
|
+
limits: CompanyMdCollabSyncLimitsSchema,
|
|
148
|
+
});
|
|
149
|
+
export type CompanyMdCollabSyncResponse = z.infer<
|
|
150
|
+
typeof CompanyMdCollabSyncResponseSchema
|
|
151
|
+
>;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* `POST /collab/updates` response body.
|
|
155
|
+
*
|
|
156
|
+
* `duplicate` is not decoration: on a retry the server returns the ORIGINAL seq
|
|
157
|
+
* and writes nothing, so a client that treats the response as proof of a fresh
|
|
158
|
+
* append would double-count its own edit.
|
|
159
|
+
*/
|
|
160
|
+
export const CompanyMdCollabUpdateAcceptedSchema = z.object({
|
|
161
|
+
/** The position this update occupies in the doc's log. */
|
|
162
|
+
seq: CollabSeqString,
|
|
163
|
+
/** True when this exact `(clientKey, clientUpdateId)` was already appended. */
|
|
164
|
+
duplicate: z.boolean(),
|
|
165
|
+
});
|
|
166
|
+
export type CompanyMdCollabUpdateAccepted = z.infer<
|
|
167
|
+
typeof CompanyMdCollabUpdateAcceptedSchema
|
|
168
|
+
>;
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// SSE frames
|
|
172
|
+
//
|
|
173
|
+
// Each member's `type` is the SSE `event:` name, stamped by the client onto the
|
|
174
|
+
// parsed `data:` body (see the module note). Fields the server does not
|
|
175
|
+
// currently emit are optional so a stamped raw frame parses unchanged.
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `event: collab-update` — one durable collaboration update.
|
|
180
|
+
*
|
|
181
|
+
* The only frame that carries an SSE `id:` line (equal to `seq`), which is what
|
|
182
|
+
* makes a browser's `Last-Event-ID` resume land on a durable cursor. Every other
|
|
183
|
+
* frame is deliberately id-less so it can never advance that cursor.
|
|
184
|
+
*/
|
|
185
|
+
export const CompanyMdCollabUpdateEventSchema = z.object({
|
|
186
|
+
type: z.literal("collab-update"),
|
|
187
|
+
/** Frame format version. */
|
|
188
|
+
v: z.literal(1),
|
|
189
|
+
/** The generation this update belongs to. */
|
|
190
|
+
epoch: z.number().int(),
|
|
191
|
+
/** The update's position in the doc's log — mirrors the frame's `id:`. */
|
|
192
|
+
seq: CollabSeqString,
|
|
193
|
+
/** base64 Yjs update bytes. Opaque; the server never interprets them. */
|
|
194
|
+
update: z.string(),
|
|
195
|
+
/** The document. Absent on a single-doc stream, where the URL already names it. */
|
|
196
|
+
docId: z.string().uuid().optional(),
|
|
197
|
+
/** Who produced the update; `null` for server-originated writes. Absent when not attributed. */
|
|
198
|
+
authorUserId: z.string().uuid().nullable().optional(),
|
|
199
|
+
/** Where the update came from. Absent when the server does not attribute origin. */
|
|
200
|
+
origin: z.enum(["client", "bridge", "bootstrap"]).optional(),
|
|
201
|
+
});
|
|
202
|
+
export type CompanyMdCollabUpdateEvent = z.infer<
|
|
203
|
+
typeof CompanyMdCollabUpdateEventSchema
|
|
204
|
+
>;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* `event: collab-presence` — one participant's presence signal, either live or
|
|
208
|
+
* replayed from the roster right after connect (so a joining client's avatar
|
|
209
|
+
* stack fills instantly).
|
|
210
|
+
*
|
|
211
|
+
* `userId` is trustworthy: the server stamps it from the posting session and
|
|
212
|
+
* never takes it from a client body. That is why no client-supplied identity
|
|
213
|
+
* type is published — receiving clients resolve name/avatar/colour from
|
|
214
|
+
* `userId` themselves.
|
|
215
|
+
*/
|
|
216
|
+
export const CompanyMdCollabPresenceEventSchema = z.object({
|
|
217
|
+
type: z.literal("collab-presence"),
|
|
218
|
+
/** Frame format version. */
|
|
219
|
+
v: z.literal(1),
|
|
220
|
+
/** The participant — session-stamped server-side; resolve identity from this. */
|
|
221
|
+
userId: z.string().uuid(),
|
|
222
|
+
/** The participant's editor instance (one user may hold several). */
|
|
223
|
+
clientKey: z.string().uuid(),
|
|
224
|
+
/** What that editor instance is doing. */
|
|
225
|
+
status: z.enum(["editing", "viewing"]),
|
|
226
|
+
/** Selection anchor — an encoded Y.RelativePosition blob. */
|
|
227
|
+
anchor: z.string().optional(),
|
|
228
|
+
/** Selection head — an encoded Y.RelativePosition blob. */
|
|
229
|
+
head: z.string().optional(),
|
|
230
|
+
/** Present (`true`) when this clientKey left — explicitly or by TTL expiry. */
|
|
231
|
+
gone: z.literal(true).optional(),
|
|
232
|
+
/** The document. Absent on a single-doc stream, where the URL already names it. */
|
|
233
|
+
docId: z.string().uuid().optional(),
|
|
234
|
+
/** The generation. Absent because presence is not a log position. */
|
|
235
|
+
epoch: z.number().int().optional(),
|
|
236
|
+
});
|
|
237
|
+
export type CompanyMdCollabPresenceEvent = z.infer<
|
|
238
|
+
typeof CompanyMdCollabPresenceEventSchema
|
|
239
|
+
>;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `event: collab-reset` — the client's cursor was unusable (a superseded
|
|
243
|
+
* generation, older than what compaction still holds, or ahead of anything this
|
|
244
|
+
* generation ever issued). The stream keeps serving from the current tail; the
|
|
245
|
+
* client refetches `/sync` and its local Y.Doc merges the snapshot losslessly,
|
|
246
|
+
* so nothing typed meanwhile is lost.
|
|
247
|
+
*/
|
|
248
|
+
export const CompanyMdCollabResetEventSchema = z.object({
|
|
249
|
+
type: z.literal("collab-reset"),
|
|
250
|
+
/** Frame format version. */
|
|
251
|
+
v: z.literal(1),
|
|
252
|
+
/**
|
|
253
|
+
* Why the cursor could not be honored. Currently one of `epoch-mismatch`,
|
|
254
|
+
* `compacted`, `cursor-ahead` — published as an open string, not an enum, so
|
|
255
|
+
* a server that grows a fourth reason does not turn every existing client's
|
|
256
|
+
* reset into a parse failure. The client's response is the same regardless:
|
|
257
|
+
* refetch `/sync`.
|
|
258
|
+
*/
|
|
259
|
+
reason: z.string(),
|
|
260
|
+
/** The document's CURRENT generation — what `/sync` will answer from. */
|
|
261
|
+
epoch: z.number().int(),
|
|
262
|
+
/** The tail the stream is serving from while the client re-syncs. */
|
|
263
|
+
resumeSeq: CollabSeqString,
|
|
264
|
+
});
|
|
265
|
+
export type CompanyMdCollabResetEvent = z.infer<
|
|
266
|
+
typeof CompanyMdCollabResetEventSchema
|
|
267
|
+
>;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* `event: access-revoked` — the actor's read grant went away mid-stream. The
|
|
271
|
+
* server closes immediately after; the client must tear down its editor rather
|
|
272
|
+
* than reconnect.
|
|
273
|
+
*
|
|
274
|
+
* Carries an EMPTY body on the wire, so `v` is optional here even though every
|
|
275
|
+
* other frame requires it. Requiring `v` would reject the real frame.
|
|
276
|
+
*/
|
|
277
|
+
export const CompanyMdCollabAccessRevokedEventSchema = z.object({
|
|
278
|
+
type: z.literal("access-revoked"),
|
|
279
|
+
/** Frame format version; absent on the wire today. */
|
|
280
|
+
v: z.literal(1).optional(),
|
|
281
|
+
});
|
|
282
|
+
export type CompanyMdCollabAccessRevokedEvent = z.infer<
|
|
283
|
+
typeof CompanyMdCollabAccessRevokedEventSchema
|
|
284
|
+
>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* `event: connected` — the stream is open. Transport-level, with an empty body;
|
|
288
|
+
* modeled so a client that routes EVERY frame through this union does not treat
|
|
289
|
+
* a normal connect as an unknown event.
|
|
290
|
+
*/
|
|
291
|
+
export const CompanyMdCollabConnectedEventSchema = z.object({
|
|
292
|
+
type: z.literal("connected"),
|
|
293
|
+
/** Frame format version; absent on the wire today. */
|
|
294
|
+
v: z.literal(1).optional(),
|
|
295
|
+
});
|
|
296
|
+
export type CompanyMdCollabConnectedEvent = z.infer<
|
|
297
|
+
typeof CompanyMdCollabConnectedEventSchema
|
|
298
|
+
>;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* `event: server_drain` — the server is shutting the stream down deliberately
|
|
302
|
+
* (a rolling restart, not a permission change). The client should reconnect;
|
|
303
|
+
* unlike `access-revoked` this is not terminal.
|
|
304
|
+
*
|
|
305
|
+
* Modeled for the same reason as `connected`: without it, every rolling restart
|
|
306
|
+
* looks like an unknown event to a client that validates all frames. Note the
|
|
307
|
+
* snake_case name — it predates the kebab-case collab frames.
|
|
308
|
+
*/
|
|
309
|
+
export const CompanyMdCollabServerDrainEventSchema = z.object({
|
|
310
|
+
type: z.literal("server_drain"),
|
|
311
|
+
/** Why the server is draining; `shutting_down` today. Open string for forward compatibility. */
|
|
312
|
+
reason: z.string(),
|
|
313
|
+
/** Frame format version; absent on the wire today. */
|
|
314
|
+
v: z.literal(1).optional(),
|
|
315
|
+
});
|
|
316
|
+
export type CompanyMdCollabServerDrainEvent = z.infer<
|
|
317
|
+
typeof CompanyMdCollabServerDrainEventSchema
|
|
318
|
+
>;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Every frame the collaboration stream emits, discriminated on the `type` the
|
|
322
|
+
* client stamps from the SSE `event:` name.
|
|
323
|
+
*
|
|
324
|
+
* Registered as the OpenAPI component `CompanyMdCollabSseEvent`, per the
|
|
325
|
+
* `ChatSseEvent` / `ExecutionSseEvent` precedent.
|
|
326
|
+
*/
|
|
327
|
+
export const CompanyMdCollabSseEventSchema = z.discriminatedUnion("type", [
|
|
328
|
+
CompanyMdCollabUpdateEventSchema,
|
|
329
|
+
CompanyMdCollabPresenceEventSchema,
|
|
330
|
+
CompanyMdCollabResetEventSchema,
|
|
331
|
+
CompanyMdCollabAccessRevokedEventSchema,
|
|
332
|
+
CompanyMdCollabConnectedEventSchema,
|
|
333
|
+
CompanyMdCollabServerDrainEventSchema,
|
|
334
|
+
]);
|
|
335
|
+
export type CompanyMdCollabSseEvent = z.infer<
|
|
336
|
+
typeof CompanyMdCollabSseEventSchema
|
|
337
|
+
>;
|
package/src/org/company-md.ts
CHANGED
|
@@ -218,6 +218,18 @@ export interface CompanyMdDocCore extends CompanyMdNodeIdentity {
|
|
|
218
218
|
* recipient to an `owners` node may still be allowed to request).
|
|
219
219
|
*/
|
|
220
220
|
readonly canRequestAccess?: boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Whether this doc is served by the real-time collaboration protocol
|
|
223
|
+
* (ADR-CONT-102). When `true` the client may open `/collab/sync` + the collab
|
|
224
|
+
* stream; otherwise it uses the legacy save path.
|
|
225
|
+
*
|
|
226
|
+
* Optional so the app deploy and the contract bump stay order-independent in
|
|
227
|
+
* BOTH directions: an old client reading a new API ignores the field, and a
|
|
228
|
+
* new client reading an old API sees it absent — and an absent value means
|
|
229
|
+
* "not collaborative", so both degrade to the legacy editing path rather than
|
|
230
|
+
* to a broken one.
|
|
231
|
+
*/
|
|
232
|
+
readonly collabEnabled?: boolean;
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
export interface CompanyMdDocCollaborators {
|
package/src/org/index.ts
CHANGED
|
@@ -227,6 +227,38 @@ export type {
|
|
|
227
227
|
CompanyMdContextBankItem,
|
|
228
228
|
} from "./company-md";
|
|
229
229
|
|
|
230
|
+
// Company.md real-time collaboration wire contract: sync/update-accepted
|
|
231
|
+
// responses, the SSE frame union, the Y.Doc shape constant, and the published
|
|
232
|
+
// size limits. Sequences are ALWAYS decimal strings. (ADR-CONT-102)
|
|
233
|
+
export {
|
|
234
|
+
COMPANY_MD_COLLAB_TEXT_KEY,
|
|
235
|
+
COMPANY_MD_COLLAB_MAX_UPDATE_B64_CHARS,
|
|
236
|
+
COMPANY_MD_COLLAB_MAX_DOC_TEXT_BYTES,
|
|
237
|
+
COMPANY_MD_COLLAB_MAX_PRESENCE_POSITION_CHARS,
|
|
238
|
+
CompanyMdCollabSyncLimitsSchema,
|
|
239
|
+
CompanyMdCollabSyncResponseSchema,
|
|
240
|
+
CompanyMdCollabUpdateAcceptedSchema,
|
|
241
|
+
CompanyMdCollabUpdateEventSchema,
|
|
242
|
+
CompanyMdCollabPresenceEventSchema,
|
|
243
|
+
CompanyMdCollabResetEventSchema,
|
|
244
|
+
CompanyMdCollabAccessRevokedEventSchema,
|
|
245
|
+
CompanyMdCollabConnectedEventSchema,
|
|
246
|
+
CompanyMdCollabServerDrainEventSchema,
|
|
247
|
+
CompanyMdCollabSseEventSchema,
|
|
248
|
+
} from "./company-md-collab";
|
|
249
|
+
export type {
|
|
250
|
+
CompanyMdCollabSyncLimits,
|
|
251
|
+
CompanyMdCollabSyncResponse,
|
|
252
|
+
CompanyMdCollabUpdateAccepted,
|
|
253
|
+
CompanyMdCollabUpdateEvent,
|
|
254
|
+
CompanyMdCollabPresenceEvent,
|
|
255
|
+
CompanyMdCollabResetEvent,
|
|
256
|
+
CompanyMdCollabAccessRevokedEvent,
|
|
257
|
+
CompanyMdCollabConnectedEvent,
|
|
258
|
+
CompanyMdCollabServerDrainEvent,
|
|
259
|
+
CompanyMdCollabSseEvent,
|
|
260
|
+
} from "./company-md-collab";
|
|
261
|
+
|
|
230
262
|
// Sharing and ACL types (PRD-00306). The legacy AccessSource / AccessReason /
|
|
231
263
|
// EffectiveAccess / EvaluationStep / AccessExplanation were removed with
|
|
232
264
|
// CompanyMdAccessEvaluator (ADR-BE-392); ShareState.effectiveAccess now carries
|