@company-semantics/contracts 44.5.0 → 45.1.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 +6 -6
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +2 -2
- package/src/content/schemas.ts +10 -2
- package/src/guards/evolution-config.ts +0 -2
- package/src/guards/soc2-config.ts +0 -2
- package/src/org/__tests__/company-md-collab.test.ts +56 -0
- package/src/org/company-md-collab.ts +20 -1
- package/src/org/company-md.ts +19 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "45.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"generate:current": "tsx ../company-semantics-ci/scripts/generate-current.ts",
|
|
117
117
|
"adr:new": "tsx ../company-semantics-control/scripts/adr/adr-new-cli.ts",
|
|
118
118
|
"repo-map": "tsx ../company-semantics-ci/scripts/generate-repo-map.ts --roots src --name company-semantics-contracts --write",
|
|
119
|
+
"sync:workflow-pins": "tsx ../company-semantics-ci/scripts/sync-workflow-pins.ts --write",
|
|
119
120
|
"repo-map:check": "tsx ../company-semantics-ci/scripts/generate-repo-map.ts --roots src --name company-semantics-contracts --check",
|
|
120
121
|
"readme-api": "tsx ../company-semantics-ci/scripts/generate-readme-api.ts --roots src --write --jsdoc",
|
|
121
122
|
"readme-api:check": "tsx ../company-semantics-ci/scripts/generate-readme-api.ts --roots src --check --jsdoc",
|
|
@@ -130,11 +131,11 @@
|
|
|
130
131
|
"zod": "^4.4.3"
|
|
131
132
|
},
|
|
132
133
|
"devDependencies": {
|
|
133
|
-
"@types/node": "^
|
|
134
|
+
"@types/node": "^22.20.1",
|
|
134
135
|
"amphtml-validator": "^1.0.38",
|
|
135
136
|
"culori": "^4.0.2",
|
|
136
137
|
"husky": "^9.1.7",
|
|
137
|
-
"lint-staged": "^17.
|
|
138
|
+
"lint-staged": "^17.3.0",
|
|
138
139
|
"markdownlint-cli2": "^0.23.2",
|
|
139
140
|
"openapi-typescript": "^7.13.0",
|
|
140
141
|
"prettier": "^3.9.6",
|
|
@@ -146,11 +147,10 @@
|
|
|
146
147
|
},
|
|
147
148
|
"pnpm": {
|
|
148
149
|
"overrides": {
|
|
149
|
-
"brace-expansion@<5.0.
|
|
150
|
+
"brace-expansion@<5.0.9": ">=5.0.9",
|
|
150
151
|
"minimatch@<10.2.3": ">=10.2.3",
|
|
151
152
|
"js-yaml@>=4.0.0 <4.3.0": ">=4.3.0 <5.0.0",
|
|
152
153
|
"js-yaml@>=5.0.0 <5.2.2": ">=5.2.2 <6.0.0",
|
|
153
|
-
"rollup@<4.59.0": ">=4.59.0",
|
|
154
154
|
"picomatch@<4.0.4": ">=4.0.4",
|
|
155
155
|
"esbuild@<0.28.1": ">=0.28.1",
|
|
156
156
|
"linkify-it@<5.0.2": ">=5.0.2",
|
|
@@ -163,5 +163,5 @@
|
|
|
163
163
|
"*.md": "markdownlint-cli2",
|
|
164
164
|
"package.json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\""
|
|
165
165
|
},
|
|
166
|
-
"securityRequirementsVersion": "
|
|
166
|
+
"securityRequirementsVersion": "5227de9b2a68460019c85186c0318d77915e02df1eb8aa8d1516b001e84bc524"
|
|
167
167
|
}
|
|
@@ -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 = 'b10dd357bc4f' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'b10dd357bc4fd990f9d6646954b3b1aa38f9dce4dd64ecfe8f5f83eb8f51a568' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -4736,8 +4736,8 @@ export interface components {
|
|
|
4736
4736
|
extractionStatus: "pending" | "extracting" | "complete" | "failed";
|
|
4737
4737
|
errorClass?: string | null;
|
|
4738
4738
|
errorReason?: string | null;
|
|
4739
|
-
createdAt
|
|
4740
|
-
updatedAt
|
|
4739
|
+
createdAt?: string;
|
|
4740
|
+
updatedAt?: string;
|
|
4741
4741
|
};
|
|
4742
4742
|
UpdateCompanyMdTitleRequest: {
|
|
4743
4743
|
title: string;
|
package/src/content/schemas.ts
CHANGED
|
@@ -199,8 +199,16 @@ export const CompanyMdDocResponseSchema = z.object({
|
|
|
199
199
|
*/
|
|
200
200
|
errorClass: z.string().nullable().optional(),
|
|
201
201
|
errorReason: z.string().nullable().optional(),
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Withheld from non-readers — see `CompanyMdDocRelations` for why, and keep the
|
|
204
|
+
* two in lockstep. Present on every reader path; absent on the
|
|
205
|
+
* `locked_requestable` / `redacted_admin` stubs, where a moving timestamp is
|
|
206
|
+
* the last remaining side channel on a doc whose body, collaborators and
|
|
207
|
+
* structure are already withheld. Absence means "you were not told", not
|
|
208
|
+
* "never modified".
|
|
209
|
+
*/
|
|
210
|
+
createdAt: z.string().optional(),
|
|
211
|
+
updatedAt: z.string().optional(),
|
|
204
212
|
});
|
|
205
213
|
|
|
206
214
|
// ---------------------------------------------------------------------------
|
|
@@ -142,6 +142,62 @@ describe("CompanyMdCollabSseEventSchema — raw backend frames", () => {
|
|
|
142
142
|
).not.toThrow();
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* `canEdit` is permission and `status` is occupancy — the pair below is the
|
|
147
|
+
* case that motivated the field (ADR-CONTRACTS-114). A read-only participant
|
|
148
|
+
* and an edit-capable one who is merely reading BOTH report `viewing`, so a
|
|
149
|
+
* consumer that split the roster on `status` would get the wrong answer for
|
|
150
|
+
* one of them. These three assertions lock that the schema keeps the two
|
|
151
|
+
* axes independent, including the absent case a rollout produces.
|
|
152
|
+
*/
|
|
153
|
+
it("carries canEdit independently of status", () => {
|
|
154
|
+
const base = {
|
|
155
|
+
type: "collab-presence" as const,
|
|
156
|
+
v: 1 as const,
|
|
157
|
+
userId: USER_ID,
|
|
158
|
+
clientKey: CLIENT_KEY,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Edit-capable, but reading. Occupancy says viewing; permission says yes.
|
|
162
|
+
const reader = CompanyMdCollabSseEventSchema.parse({
|
|
163
|
+
...base,
|
|
164
|
+
status: "viewing",
|
|
165
|
+
canEdit: true,
|
|
166
|
+
});
|
|
167
|
+
expect(reader).toMatchObject({ status: "viewing", canEdit: true });
|
|
168
|
+
|
|
169
|
+
// Read-only, and on the editor tab. Occupancy is the same; permission is not.
|
|
170
|
+
const viewer = CompanyMdCollabSseEventSchema.parse({
|
|
171
|
+
...base,
|
|
172
|
+
status: "viewing",
|
|
173
|
+
canEdit: false,
|
|
174
|
+
});
|
|
175
|
+
expect(viewer).toMatchObject({ status: "viewing", canEdit: false });
|
|
176
|
+
|
|
177
|
+
// An older server omits the field. It must parse, and it must not
|
|
178
|
+
// materialize as `true` — absent means no edit rights, so a consumer
|
|
179
|
+
// reading `canEdit === true` fails closed on its own.
|
|
180
|
+
const legacy = CompanyMdCollabSseEventSchema.parse({
|
|
181
|
+
...base,
|
|
182
|
+
status: "editing",
|
|
183
|
+
});
|
|
184
|
+
expect(legacy).toMatchObject({ status: "editing" });
|
|
185
|
+
expect((legacy as { canEdit?: boolean }).canEdit).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("rejects a non-boolean canEdit", () => {
|
|
189
|
+
expect(() =>
|
|
190
|
+
CompanyMdCollabSseEventSchema.parse({
|
|
191
|
+
type: "collab-presence",
|
|
192
|
+
v: 1,
|
|
193
|
+
userId: USER_ID,
|
|
194
|
+
clientKey: CLIENT_KEY,
|
|
195
|
+
status: "editing",
|
|
196
|
+
canEdit: "yes",
|
|
197
|
+
}),
|
|
198
|
+
).toThrow();
|
|
199
|
+
});
|
|
200
|
+
|
|
145
201
|
it("parses a collab-reset frame for each reason the backend sends", () => {
|
|
146
202
|
for (const reason of ["epoch-mismatch", "compacted", "cursor-ahead"]) {
|
|
147
203
|
expect(() =>
|
|
@@ -235,7 +235,9 @@ export type CompanyMdCollabUpdateEvent = z.infer<
|
|
|
235
235
|
* `userId` is trustworthy: the server stamps it from the posting session and
|
|
236
236
|
* never takes it from a client body. That is why no client-supplied identity
|
|
237
237
|
* type is published — receiving clients resolve name/avatar/colour from
|
|
238
|
-
* `userId` themselves.
|
|
238
|
+
* `userId` themselves. `canEdit` is stamped from the same session for the same
|
|
239
|
+
* reason: it is an authority answer, and an authority answer a client could
|
|
240
|
+
* assert about itself is not one.
|
|
239
241
|
*/
|
|
240
242
|
export const CompanyMdCollabPresenceEventSchema = z.object({
|
|
241
243
|
type: z.literal("collab-presence"),
|
|
@@ -247,6 +249,23 @@ export const CompanyMdCollabPresenceEventSchema = z.object({
|
|
|
247
249
|
clientKey: z.string().uuid(),
|
|
248
250
|
/** What that editor instance is doing. */
|
|
249
251
|
status: z.enum(["editing", "viewing"]),
|
|
252
|
+
/**
|
|
253
|
+
* Whether this participant may WRITE the document — server-stamped from the
|
|
254
|
+
* same `CompanyMd.CanEdit` binding the sync handshake reports as `editable`,
|
|
255
|
+
* never taken from a client body.
|
|
256
|
+
*
|
|
257
|
+
* Distinct from `status`, which is occupancy: `status` says what one editor
|
|
258
|
+
* instance is doing right now, this says what the person is ALLOWED to do.
|
|
259
|
+
* The roster admits viewers on purpose (CanView, not CanEdit), so a reader
|
|
260
|
+
* cannot tell the two apart without this — and a client must never re-derive
|
|
261
|
+
* it from an ACL, which would stand up a second access authority in the
|
|
262
|
+
* browser.
|
|
263
|
+
*
|
|
264
|
+
* Optional so an older server stays representable on the wire. ABSENT MEANS
|
|
265
|
+
* NO EDIT RIGHTS: a consumer reads the missing field as `false` rather than
|
|
266
|
+
* guessing, which keeps the surface default-deny through a rollout.
|
|
267
|
+
*/
|
|
268
|
+
canEdit: z.boolean().optional(),
|
|
250
269
|
/** Selection anchor — an encoded Y.RelativePosition blob. */
|
|
251
270
|
anchor: z.string().optional(),
|
|
252
271
|
/** Selection head — an encoded Y.RelativePosition blob. */
|
package/src/org/company-md.ts
CHANGED
|
@@ -275,8 +275,25 @@ export interface CompanyMdDocRelations {
|
|
|
275
275
|
readonly inheritsFrom: string | null;
|
|
276
276
|
readonly sources: readonly CompanyMdSource[];
|
|
277
277
|
readonly dependencies: readonly CompanyMdDependency[];
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
/**
|
|
279
|
+
* WITHHELD FROM NON-READERS, and optional for exactly that reason.
|
|
280
|
+
*
|
|
281
|
+
* A non-reader's doc read is a stub: `content` is `''`, and `sources` /
|
|
282
|
+
* `dependencies` / `members` are empty. These two timestamps were the one
|
|
283
|
+
* thing left on it that still reported activity INSIDE the document — that
|
|
284
|
+
* someone wrote to it, and roughly when. On a doc whose body, collaborators
|
|
285
|
+
* and structure are all withheld, a moving `updatedAt` is a side channel: poll
|
|
286
|
+
* it and you learn how busy a document you cannot open is.
|
|
287
|
+
*
|
|
288
|
+
* So they are absent on the `locked_requestable` / `redacted_admin` branches
|
|
289
|
+
* and present on every reader path. Optional rather than nullable because
|
|
290
|
+
* "withheld" is the absence of an answer, not an answer of `null` — and
|
|
291
|
+
* because an older consumer reading the new API sees a missing field, which is
|
|
292
|
+
* the shape it already tolerates for every other stub-withheld value here.
|
|
293
|
+
* Treat absence as "you were not told", never as "never modified".
|
|
294
|
+
*/
|
|
295
|
+
readonly createdAt?: string;
|
|
296
|
+
readonly updatedAt?: string;
|
|
280
297
|
}
|
|
281
298
|
|
|
282
299
|
export type CompanyMdDoc = CompanyMdDocCore &
|