@company-semantics/contracts 39.6.1 → 39.8.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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +2 -0
- package/src/index.ts +11 -0
- package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +1 -0
- package/src/notifications/__tests__/fixtures.ts +7 -0
- package/src/notifications/kinds/company-md-access-requested.ts +32 -2
- package/src/notifications/payloads.ts +6 -0
- package/src/permissions/access-request.ts +15 -0
- package/src/security/index.ts +8 -0
- package/src/security/token-policy.ts +113 -0
package/package.json
CHANGED
|
@@ -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 = 'e8cf36a33d43' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'e8cf36a33d43bec5e4383c8edbfbdccd37fe8d081a5ddd02e96c83cb5e7bd21a' as const;
|
package/src/api/generated.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -907,6 +907,17 @@ export type {
|
|
|
907
907
|
DisableSecretRequest,
|
|
908
908
|
} from "./security/index";
|
|
909
909
|
|
|
910
|
+
// Auth token storage/entropy policy (PRD-00901)
|
|
911
|
+
// @see src/security/token-policy.ts — single source for the runtime issuance
|
|
912
|
+
// path and the secure-token-generation CI guard
|
|
913
|
+
export { AUTH_TOKEN_POLICY_REQUIREMENTS } from "./security/index";
|
|
914
|
+
export type {
|
|
915
|
+
TokenStorageFormat,
|
|
916
|
+
TokenPolicyConformance,
|
|
917
|
+
TokenPolicyRequirement,
|
|
918
|
+
AuthTokenPolicyClass,
|
|
919
|
+
} from "./security/index";
|
|
920
|
+
|
|
910
921
|
// Analytics response metadata (shared vocabulary for OLTP/OLAP separation)
|
|
911
922
|
// @see ADR-CTRL-053 for design rationale
|
|
912
923
|
export type {
|
|
@@ -15,6 +15,7 @@ exports[`NOTIFICATION_DEFINITIONS > titles every kind 1`] = `
|
|
|
15
15
|
"companyMd.access_request_denied · No reason": "Your access request was reviewed",
|
|
16
16
|
"companyMd.access_request_denied · With reason": "Your access request was reviewed",
|
|
17
17
|
"companyMd.access_requested · No message": "Someone requested access to a document",
|
|
18
|
+
"companyMd.access_requested · Requested editor": "Someone requested access to a document",
|
|
18
19
|
"companyMd.access_requested · With message": "Someone requested access to a document",
|
|
19
20
|
"org.invite · Admin": "You've been invited to join Acme Corp on Company Semantics",
|
|
20
21
|
"org.invite · Member": "You've been invited to join Acme Corp on Company Semantics",
|
|
@@ -198,6 +198,13 @@ add("companyMd.access_requested", "No message", {
|
|
|
198
198
|
docTitle: "Engineering Handbook",
|
|
199
199
|
reviewUrl: `${APP}/doc/handbook?request=req_456`,
|
|
200
200
|
});
|
|
201
|
+
add("companyMd.access_requested", "Requested editor", {
|
|
202
|
+
requesterName: "Sam Chen",
|
|
203
|
+
docTitle: "Engineering Handbook",
|
|
204
|
+
message: "I maintain this section day to day — may I edit it directly?",
|
|
205
|
+
requestedAccessLevel: "editor",
|
|
206
|
+
reviewUrl: `${APP}/doc/handbook?request=req_789`,
|
|
207
|
+
});
|
|
201
208
|
|
|
202
209
|
add("companyMd.access_request_approved", "Editor", {
|
|
203
210
|
approverName: "Jordan Lee",
|
|
@@ -8,11 +8,28 @@
|
|
|
8
8
|
|
|
9
9
|
import type { NotificationDefinition } from "../definition";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* How each requestable band reads in prose ("requested edit access") and in the
|
|
13
|
+
* key-value table ("Editor"). Absent band ⇒ band-neutral copy, so senders that
|
|
14
|
+
* predate `requestedAccessLevel` (ADR-CONTRACTS-098) render exactly as before.
|
|
15
|
+
*/
|
|
16
|
+
const LEVEL_PHRASE = {
|
|
17
|
+
viewer: "view",
|
|
18
|
+
commenter: "comment",
|
|
19
|
+
editor: "edit",
|
|
20
|
+
} as const;
|
|
21
|
+
const LEVEL_LABEL = {
|
|
22
|
+
viewer: "Viewer",
|
|
23
|
+
commenter: "Commenter",
|
|
24
|
+
editor: "Editor",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
11
27
|
export const accessRequestedDefinition: NotificationDefinition<"companyMd.access_requested"> =
|
|
12
28
|
{
|
|
13
29
|
kind: "companyMd.access_requested",
|
|
14
30
|
compose: (payload, context) => {
|
|
15
|
-
const { requesterName, docTitle, message, reviewUrl } =
|
|
31
|
+
const { requesterName, docTitle, message, requestedAccessLevel, reviewUrl } =
|
|
32
|
+
payload;
|
|
16
33
|
|
|
17
34
|
return {
|
|
18
35
|
metadata: {
|
|
@@ -23,7 +40,12 @@ export const accessRequestedDefinition: NotificationDefinition<"companyMd.access
|
|
|
23
40
|
{
|
|
24
41
|
elements: [
|
|
25
42
|
{ type: "greeting" },
|
|
26
|
-
{
|
|
43
|
+
{
|
|
44
|
+
type: "body",
|
|
45
|
+
text: requestedAccessLevel
|
|
46
|
+
? `Someone requested ${LEVEL_PHRASE[requestedAccessLevel]} access to a document.`
|
|
47
|
+
: "Someone requested document access.",
|
|
48
|
+
},
|
|
27
49
|
{
|
|
28
50
|
type: "chatUnit",
|
|
29
51
|
items: [
|
|
@@ -47,6 +69,14 @@ export const accessRequestedDefinition: NotificationDefinition<"companyMd.access
|
|
|
47
69
|
rows: [
|
|
48
70
|
{ label: "From", value: requesterName },
|
|
49
71
|
{ label: "Document", value: `"${docTitle}"` },
|
|
72
|
+
...(requestedAccessLevel
|
|
73
|
+
? [
|
|
74
|
+
{
|
|
75
|
+
label: "Requested",
|
|
76
|
+
value: LEVEL_LABEL[requestedAccessLevel],
|
|
77
|
+
},
|
|
78
|
+
]
|
|
79
|
+
: []),
|
|
50
80
|
],
|
|
51
81
|
},
|
|
52
82
|
{
|
|
@@ -132,6 +132,12 @@ export interface NotificationPayloads {
|
|
|
132
132
|
docTitle: string;
|
|
133
133
|
/** Optional message from the requester (notification only) */
|
|
134
134
|
message?: string;
|
|
135
|
+
/**
|
|
136
|
+
* The band the requester asked for (ADR-CONTRACTS-098): names the intent
|
|
137
|
+
* in the owner's email ("requested edit access"). Absent = unspecified
|
|
138
|
+
* (older senders / clients) — the copy stays band-neutral.
|
|
139
|
+
*/
|
|
140
|
+
requestedAccessLevel?: "viewer" | "commenter" | "editor";
|
|
135
141
|
/** Deep-link that opens the doc's ShareDialog scrolled to the pending request */
|
|
136
142
|
reviewUrl: string;
|
|
137
143
|
};
|
|
@@ -28,6 +28,15 @@ export type AccessRequestStatus = z.infer<typeof AccessRequestStatusSchema>;
|
|
|
28
28
|
export const AccessRequestCreateSchema = z.object({
|
|
29
29
|
/** Optional context the requester sends to the owner(s). */
|
|
30
30
|
message: z.string().max(2000).optional(),
|
|
31
|
+
/**
|
|
32
|
+
* The band the requester is asking for (ADR-CONTRACTS-098 / ADR-BE-454):
|
|
33
|
+
* `viewer` from the locked preview, `editor` from the view-only Editor tab
|
|
34
|
+
* (elevation, ADR-BE-453). A REQUEST, not a claim — the owner still chooses
|
|
35
|
+
* the granted band on approval; this only seeds their picker. Optional so
|
|
36
|
+
* older clients keep working (absent ⇒ unspecified, owners default to
|
|
37
|
+
* viewer).
|
|
38
|
+
*/
|
|
39
|
+
requestedAccessLevel: GrantableAccessLevelSchema.optional(),
|
|
31
40
|
});
|
|
32
41
|
export type AccessRequestCreate = z.infer<typeof AccessRequestCreateSchema>;
|
|
33
42
|
|
|
@@ -45,6 +54,12 @@ export const AccessRequestSchema = z.object({
|
|
|
45
54
|
docId: z.string(),
|
|
46
55
|
status: AccessRequestStatusSchema,
|
|
47
56
|
message: z.string().nullable(),
|
|
57
|
+
/**
|
|
58
|
+
* The band the requester asked for (ADR-CONTRACTS-098) — seeds the owner
|
|
59
|
+
* inbox's approval picker. Null for requests filed before the field existed
|
|
60
|
+
* or by clients that did not specify one.
|
|
61
|
+
*/
|
|
62
|
+
requestedAccessLevel: GrantableAccessLevelSchema.nullable(),
|
|
48
63
|
createdAt: z.string(),
|
|
49
64
|
resolvedAt: z.string().nullable(),
|
|
50
65
|
});
|
package/src/security/index.ts
CHANGED
|
@@ -19,3 +19,11 @@ export type {
|
|
|
19
19
|
RotateSecretRequest,
|
|
20
20
|
DisableSecretRequest,
|
|
21
21
|
} from "./org-secrets";
|
|
22
|
+
|
|
23
|
+
export { AUTH_TOKEN_POLICY_REQUIREMENTS } from "./token-policy";
|
|
24
|
+
export type {
|
|
25
|
+
TokenStorageFormat,
|
|
26
|
+
TokenPolicyConformance,
|
|
27
|
+
TokenPolicyRequirement,
|
|
28
|
+
AuthTokenPolicyClass,
|
|
29
|
+
} from "./token-policy";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared storage and entropy policy for auth-bearing tokens.
|
|
3
|
+
*
|
|
4
|
+
* This is the single source consumed by BOTH the runtime issuance path and the
|
|
5
|
+
* secure-token-generation CI guard. A declaration with no enforcing consumer is
|
|
6
|
+
* the defect class this module exists to close.
|
|
7
|
+
*
|
|
8
|
+
* Every field states what an implementation MUST satisfy. None of them describe
|
|
9
|
+
* what the code does today — that is `conformance`, and it is mandatory per
|
|
10
|
+
* class. Keeping the two apart is the point: a requirement record that reads as
|
|
11
|
+
* a description silently re-asserts the guarantee it was written to obtain, and
|
|
12
|
+
* is the same declaration-without-enforcement defect wearing a different hat.
|
|
13
|
+
*/
|
|
14
|
+
export type TokenStorageFormat = "hashed" | "hmac-signed" | "encrypted";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether every known issuance site already satisfies the declared requirement.
|
|
18
|
+
*
|
|
19
|
+
* `enforced` — no known site violates it.
|
|
20
|
+
* `unmet` — at least one known site violates it; `gap` names what is wrong.
|
|
21
|
+
*
|
|
22
|
+
* This is NOT a suppression and confers no exemption. The requirement holds
|
|
23
|
+
* either way and the secure-token-generation guard reports the violating sites
|
|
24
|
+
* regardless — the field exists so the canonical record cannot be read as
|
|
25
|
+
* asserting a guarantee the implementation has not yet earned.
|
|
26
|
+
*/
|
|
27
|
+
export type TokenPolicyConformance =
|
|
28
|
+
| { readonly status: "enforced" }
|
|
29
|
+
| { readonly status: "unmet"; readonly gap: string };
|
|
30
|
+
|
|
31
|
+
export interface TokenPolicyRequirement {
|
|
32
|
+
/** Minimum entropy the raw token must carry, in bits. */
|
|
33
|
+
readonly minEntropyBits: number;
|
|
34
|
+
/** Algorithm the stored representation must be derived with. */
|
|
35
|
+
readonly hashAlgorithm: string;
|
|
36
|
+
/**
|
|
37
|
+
* How the token must be persisted. Never 'plaintext' — that is not a member,
|
|
38
|
+
* because plaintext is never an acceptable requirement. A class that IS
|
|
39
|
+
* persisted in plaintext today says so through `conformance`, which is why
|
|
40
|
+
* the union does not need to express it.
|
|
41
|
+
*/
|
|
42
|
+
readonly storageFormat: TokenStorageFormat;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the implementation currently meets the requirement above. Required,
|
|
45
|
+
* not optional: an omitted status would default to an unearned claim, which
|
|
46
|
+
* is exactly the drift this record exists to surface.
|
|
47
|
+
*/
|
|
48
|
+
readonly conformance: TokenPolicyConformance;
|
|
49
|
+
/**
|
|
50
|
+
* Identifier / function-name patterns that bind a call site to this policy
|
|
51
|
+
* class. Heuristic only: the AUTHORITATIVE binding is the explicit policy
|
|
52
|
+
* argument passed at the generation site. These patterns exist so the guard
|
|
53
|
+
* can flag an UNCLASSIFIED site, never to silently classify one.
|
|
54
|
+
*/
|
|
55
|
+
readonly contextPatterns: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const AUTH_TOKEN_POLICY_REQUIREMENTS = {
|
|
59
|
+
SessionToken: {
|
|
60
|
+
minEntropyBits: 256,
|
|
61
|
+
hashAlgorithm: "SHA-256",
|
|
62
|
+
storageFormat: "hashed",
|
|
63
|
+
contextPatterns: ["session"],
|
|
64
|
+
conformance: {
|
|
65
|
+
status: "unmet",
|
|
66
|
+
gap:
|
|
67
|
+
"Minted with randomUUID() (122 bits) and persisted verbatim: sessions.token " +
|
|
68
|
+
"holds the cookie value itself, and lookup is raw equality against it.",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
ChatShareToken: {
|
|
72
|
+
minEntropyBits: 256,
|
|
73
|
+
hashAlgorithm: "SHA-256",
|
|
74
|
+
storageFormat: "hashed",
|
|
75
|
+
contextPatterns: ["share"],
|
|
76
|
+
conformance: {
|
|
77
|
+
status: "unmet",
|
|
78
|
+
gap:
|
|
79
|
+
"Minted with randomBytes(32) and persisted verbatim: chat_shares.token holds " +
|
|
80
|
+
"the share-URL value itself, and lookup is raw equality against it.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
InviteToken: {
|
|
84
|
+
minEntropyBits: 256,
|
|
85
|
+
hashAlgorithm: "SHA-256",
|
|
86
|
+
storageFormat: "hashed",
|
|
87
|
+
contextPatterns: ["invite"],
|
|
88
|
+
conformance: { status: "enforced" },
|
|
89
|
+
},
|
|
90
|
+
OAuthStateToken: {
|
|
91
|
+
minEntropyBits: 128,
|
|
92
|
+
hashAlgorithm: "HMAC-SHA256",
|
|
93
|
+
storageFormat: "hmac-signed",
|
|
94
|
+
contextPatterns: ["state", "nonce"],
|
|
95
|
+
conformance: { status: "enforced" },
|
|
96
|
+
},
|
|
97
|
+
OTPCode: {
|
|
98
|
+
minEntropyBits: 20,
|
|
99
|
+
hashAlgorithm: "bcrypt",
|
|
100
|
+
storageFormat: "hashed",
|
|
101
|
+
contextPatterns: ["otp", "loginCode"],
|
|
102
|
+
conformance: { status: "enforced" },
|
|
103
|
+
},
|
|
104
|
+
OAuthTokenEncryption: {
|
|
105
|
+
minEntropyBits: 256,
|
|
106
|
+
hashAlgorithm: "AES-256-GCM",
|
|
107
|
+
storageFormat: "encrypted",
|
|
108
|
+
contextPatterns: ["tokenEncryption"],
|
|
109
|
+
conformance: { status: "enforced" },
|
|
110
|
+
},
|
|
111
|
+
} as const satisfies Record<string, TokenPolicyRequirement>;
|
|
112
|
+
|
|
113
|
+
export type AuthTokenPolicyClass = keyof typeof AUTH_TOKEN_POLICY_REQUIREMENTS;
|