@opengeni/contracts 0.19.0 → 0.20.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/dist/index.d.ts +664 -12
- package/dist/index.js +2780 -2220
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +181 -0
- package/src/secret-redaction.ts +364 -0
- package/src/workspace-instruction-policies.ts +267 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
const MIN_REDACTABLE_VALUE_LENGTH = 6;
|
|
2
|
+
const REDACTED = "[redacted]";
|
|
3
|
+
const MAX_REDACTION_DEPTH = 64;
|
|
4
|
+
const CYCLE_MARKER = "[OpenGeni omitted cyclic value during secret redaction]";
|
|
5
|
+
const DEPTH_MARKER = "[OpenGeni omitted value beyond secret-redaction depth]";
|
|
6
|
+
|
|
7
|
+
export type SecretForRedaction = {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type PreparedSecret = {
|
|
13
|
+
marker: string;
|
|
14
|
+
value: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SENSITIVE_FIELD_NAMES = new Set([
|
|
18
|
+
"authorization",
|
|
19
|
+
"proxyauthorization",
|
|
20
|
+
"cookie",
|
|
21
|
+
"setcookie",
|
|
22
|
+
"accesstoken",
|
|
23
|
+
"refreshtoken",
|
|
24
|
+
"idtoken",
|
|
25
|
+
"apikey",
|
|
26
|
+
"secret",
|
|
27
|
+
"clientsecret",
|
|
28
|
+
"password",
|
|
29
|
+
"passwd",
|
|
30
|
+
"privatekey",
|
|
31
|
+
"credential",
|
|
32
|
+
"credentials",
|
|
33
|
+
"credentialencrypted",
|
|
34
|
+
"encryptedcredential",
|
|
35
|
+
"headersencrypted",
|
|
36
|
+
"encryptedpkceverifier",
|
|
37
|
+
"codeverifier",
|
|
38
|
+
"signingkey",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const CREDENTIAL_HEADER_PATTERNS = [
|
|
42
|
+
/^(?:proxy-)?authorization$/i,
|
|
43
|
+
/^(?:set-)?cookie$/i,
|
|
44
|
+
/^(?:x[-_])?api[-_]?key$/i,
|
|
45
|
+
/^(?:x[-_])?(?:access|refresh|id)[-_]?token$/i,
|
|
46
|
+
/^(?:x[-_])?(?:auth|session)[-_]?(?:token|key|secret)$/i,
|
|
47
|
+
/^(?:x[-_])?(?:client|app|consumer)[-_]?secret$/i,
|
|
48
|
+
/^x-opengeni-access-key$/i,
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
const SECRET_KEY_SOURCE =
|
|
52
|
+
"(?:proxy[-_ ]?authorization|authorization|set[-_ ]?cookie|cookie|access[-_ ]?token|refresh[-_ ]?token|id[-_ ]?token|api[-_ ]?key|client[-_ ]?secret|secret|password|passwd|private[-_ ]?key|credential(?:s|[-_ ]?encrypted)?|encrypted[-_ ]?credential|encrypted[-_ ]?pkce[-_ ]?verifier|code[-_ ]?verifier|signing[-_ ]?key)";
|
|
53
|
+
const UNQUOTED_SECRET_KEY_SOURCE =
|
|
54
|
+
"(?:access[-_ ]?token|refresh[-_ ]?token|id[-_ ]?token|api[-_ ]?key|client[-_ ]?secret|secret|password|passwd|private[-_ ]?key|credential(?:s|[-_ ]?encrypted)?|encrypted[-_ ]?credential|encrypted[-_ ]?pkce[-_ ]?verifier|code[-_ ]?verifier|signing[-_ ]?key)";
|
|
55
|
+
|
|
56
|
+
const AUTHORIZATION_HEADER_PATTERN =
|
|
57
|
+
/(\b(?:proxy-)?authorization[^\S\r\n]*:[^\S\r\n]*)([^\r\n'"`]+)/gi;
|
|
58
|
+
const COOKIE_HEADER_PATTERN = /(\b(?:set-cookie|cookie)\s*:\s*)([^\r\n'"`]+)/gi;
|
|
59
|
+
const API_KEY_HEADER_PATTERN = /(\b(?:x[-_])?api[-_]?key[^\S\r\n]*:[^\S\r\n]*)([^\r\n'"`]+)/gi;
|
|
60
|
+
const CURL_USER_PATTERN = /((?:^|\s)(?:-u|--user)(?:=|\s+))(?:("[^"]*")|('[^']*')|([^\s]+))/gm;
|
|
61
|
+
const URL_USERINFO_PATTERN = /(https?:\/\/)[^\s/@]+@/gi;
|
|
62
|
+
const SIGNED_QUERY_PATTERN = new RegExp(
|
|
63
|
+
`([?&](?:sig|signature|x-amz-signature|x-amz-credential|x-amz-security-token|x-goog-signature|x-goog-credential|access_token|refresh_token|token)=)([^&#\\s'"<>]+)`,
|
|
64
|
+
"gi",
|
|
65
|
+
);
|
|
66
|
+
const QUOTED_SECRET_ASSIGNMENT_PATTERN = new RegExp(
|
|
67
|
+
`((?:["']${SECRET_KEY_SOURCE}["']|\\b${SECRET_KEY_SOURCE})\\s*[:=]\\s*)(["'])(.*?)\\2`,
|
|
68
|
+
"gi",
|
|
69
|
+
);
|
|
70
|
+
const UNQUOTED_SECRET_ASSIGNMENT_PATTERN = new RegExp(
|
|
71
|
+
`((?:\\b${UNQUOTED_SECRET_KEY_SOURCE})\\s*[:=]\\s*)([^\\s,;}&]+)`,
|
|
72
|
+
"gi",
|
|
73
|
+
);
|
|
74
|
+
const SECRET_ENV_ASSIGNMENT_PATTERN =
|
|
75
|
+
/((?:^|[\s;])(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY|CREDENTIAL|AUTHORIZATION|COOKIE)[A-Za-z0-9_]*\s*=\s*)(?:("[^"]*")|('[^']*')|([^\s;]+))/gim;
|
|
76
|
+
|
|
77
|
+
const PROVIDER_TOKEN_PATTERNS = [
|
|
78
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
79
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
80
|
+
/\bglpat-[A-Za-z0-9_-]{20,}\b/g,
|
|
81
|
+
/\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
82
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
83
|
+
/\bAIza[0-9A-Za-z_-]{30,}\b/g,
|
|
84
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
85
|
+
/\bogd_[A-Za-z0-9._~-]{10,}\b/g,
|
|
86
|
+
/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns true only for fields whose value is itself credential material.
|
|
91
|
+
* Container fields such as `headers` and URL fields are intentionally not
|
|
92
|
+
* included: their nested/value sanitizers retain useful names, hosts, paths,
|
|
93
|
+
* and non-sensitive query parameters.
|
|
94
|
+
*/
|
|
95
|
+
export function isSensitiveFieldName(name: string): boolean {
|
|
96
|
+
return SENSITIVE_FIELD_NAMES.has(normalizeFieldName(name));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Return true only for header names whose values are credential material.
|
|
101
|
+
* Ordinary protocol metadata (`content-type`, `accept`, `user-agent`, and
|
|
102
|
+
* pagination/signature headers outside this allowlist) must remain intact.
|
|
103
|
+
*/
|
|
104
|
+
export function isCredentialHeaderName(name: string): boolean {
|
|
105
|
+
return CREDENTIAL_HEADER_PATTERNS.some((pattern) => pattern.test(name));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Redact only exact known-secret provenance from a structured object key.
|
|
110
|
+
* Generic field/header heuristics intentionally do not run here: a key is
|
|
111
|
+
* metadata unless the caller has proved that its bytes are secret material.
|
|
112
|
+
*/
|
|
113
|
+
export function redactSensitiveKey(
|
|
114
|
+
key: string,
|
|
115
|
+
knownSecrets: readonly SecretForRedaction[] = [],
|
|
116
|
+
): string {
|
|
117
|
+
return replacePreparedSecrets(key, prepareSecrets(knownSecrets));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Redact known secret provenance and common credential-bearing text shapes.
|
|
122
|
+
* This is deliberately a conservative safety boundary, not a promise of
|
|
123
|
+
* general-purpose DLP. It never includes a matched value in a marker or error.
|
|
124
|
+
*/
|
|
125
|
+
export function redactSensitiveText(
|
|
126
|
+
text: string,
|
|
127
|
+
knownSecrets: readonly SecretForRedaction[] = [],
|
|
128
|
+
): string {
|
|
129
|
+
let redacted = replacePreparedSecrets(text, prepareSecrets(knownSecrets));
|
|
130
|
+
|
|
131
|
+
redacted = redacted.replace(
|
|
132
|
+
AUTHORIZATION_HEADER_PATTERN,
|
|
133
|
+
(match, prefix: string, rawValue: string) => {
|
|
134
|
+
const value = rawValue.trimEnd();
|
|
135
|
+
const trailingWhitespace = rawValue.slice(value.length);
|
|
136
|
+
const schemeMatch = value.match(/^([A-Za-z][A-Za-z0-9_-]*)(\s+)(.+)$/);
|
|
137
|
+
if (schemeMatch) {
|
|
138
|
+
const scheme = schemeMatch[1];
|
|
139
|
+
const whitespace = schemeMatch[2];
|
|
140
|
+
const credential = schemeMatch[3];
|
|
141
|
+
if (scheme && whitespace && credential) {
|
|
142
|
+
return isRedactionMarker(credential.trim())
|
|
143
|
+
? match
|
|
144
|
+
: `${prefix}${scheme}${whitespace}${REDACTED}${trailingWhitespace}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return isRedactionMarker(value) ? match : `${prefix}${REDACTED}${trailingWhitespace}`;
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
redacted = redacted.replace(COOKIE_HEADER_PATTERN, `$1${REDACTED}`);
|
|
151
|
+
redacted = redacted.replace(API_KEY_HEADER_PATTERN, `$1${REDACTED}`);
|
|
152
|
+
redacted = redacted.replace(CURL_USER_PATTERN, (_match, prefix: string) => {
|
|
153
|
+
return `${prefix}${REDACTED}`;
|
|
154
|
+
});
|
|
155
|
+
redacted = redacted.replace(URL_USERINFO_PATTERN, `$1${REDACTED}@`);
|
|
156
|
+
redacted = redacted.replace(SIGNED_QUERY_PATTERN, `$1${REDACTED}`);
|
|
157
|
+
redacted = redacted.replace(
|
|
158
|
+
QUOTED_SECRET_ASSIGNMENT_PATTERN,
|
|
159
|
+
(match, prefix: string, quote: string, value: string) =>
|
|
160
|
+
isRedactionMarker(value) ? match : `${prefix}${quote}${REDACTED}${quote}`,
|
|
161
|
+
);
|
|
162
|
+
redacted = redacted.replace(
|
|
163
|
+
UNQUOTED_SECRET_ASSIGNMENT_PATTERN,
|
|
164
|
+
(match, prefix: string, value: string) =>
|
|
165
|
+
isRedactionMarker(value) ? match : `${prefix}${REDACTED}`,
|
|
166
|
+
);
|
|
167
|
+
redacted = redacted.replace(
|
|
168
|
+
SECRET_ENV_ASSIGNMENT_PATTERN,
|
|
169
|
+
(
|
|
170
|
+
match,
|
|
171
|
+
prefix: string,
|
|
172
|
+
doubleQuoted: string | undefined,
|
|
173
|
+
singleQuoted: string | undefined,
|
|
174
|
+
bare: string | undefined,
|
|
175
|
+
) => {
|
|
176
|
+
const value = doubleQuoted ?? singleQuoted ?? bare ?? "";
|
|
177
|
+
return isRedactionMarker(stripMatchingQuotes(value)) ? match : `${prefix}${REDACTED}`;
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
for (const pattern of PROVIDER_TOKEN_PATTERNS) {
|
|
181
|
+
redacted = redacted.replace(pattern, REDACTED);
|
|
182
|
+
}
|
|
183
|
+
return redacted;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Deeply redact plain structured data while retaining its diagnostic shape. */
|
|
187
|
+
export function redactSensitiveData<T>(
|
|
188
|
+
value: T,
|
|
189
|
+
knownSecrets: readonly SecretForRedaction[] = [],
|
|
190
|
+
): T {
|
|
191
|
+
return redactSensitiveDataDeep(value, knownSecrets, new WeakSet<object>(), 0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Build the worker-friendly single-argument redactor used at turn boundaries. */
|
|
195
|
+
export function createSecretRedactor(
|
|
196
|
+
knownSecrets: readonly SecretForRedaction[],
|
|
197
|
+
): (value: unknown) => unknown {
|
|
198
|
+
const prepared = prepareSecrets(knownSecrets).map(({ marker, value }) => ({
|
|
199
|
+
name: marker.slice("[redacted:".length, -1),
|
|
200
|
+
value,
|
|
201
|
+
}));
|
|
202
|
+
return (value: unknown) => redactSensitiveData(value, prepared);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Redact a serialized JSON checkpoint without requiring it to be valid JSON.
|
|
207
|
+
* Valid JSON retains structure; malformed/opaque text still receives text
|
|
208
|
+
* classification and exact-known-value replacement.
|
|
209
|
+
*/
|
|
210
|
+
export function redactSerializedJson(
|
|
211
|
+
serialized: string,
|
|
212
|
+
knownSecrets: readonly SecretForRedaction[] = [],
|
|
213
|
+
): string {
|
|
214
|
+
try {
|
|
215
|
+
return JSON.stringify(redactSensitiveData(JSON.parse(serialized), knownSecrets));
|
|
216
|
+
} catch {
|
|
217
|
+
return redactSensitiveText(serialized, knownSecrets);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function identityRedactor<T>(value: T): T {
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function redactSensitiveDataDeep<T>(
|
|
226
|
+
value: T,
|
|
227
|
+
knownSecrets: readonly SecretForRedaction[],
|
|
228
|
+
seen: WeakSet<object>,
|
|
229
|
+
depth: number,
|
|
230
|
+
): T {
|
|
231
|
+
if (typeof value === "string") {
|
|
232
|
+
return redactSensitiveText(value, knownSecrets) as T;
|
|
233
|
+
}
|
|
234
|
+
if (!value || typeof value !== "object" || value instanceof Date) {
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
if (depth >= MAX_REDACTION_DEPTH) {
|
|
238
|
+
return DEPTH_MARKER as T;
|
|
239
|
+
}
|
|
240
|
+
if (seen.has(value)) {
|
|
241
|
+
return CYCLE_MARKER as T;
|
|
242
|
+
}
|
|
243
|
+
seen.add(value);
|
|
244
|
+
try {
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
return value.map((item) => redactSensitiveDataDeep(item, knownSecrets, seen, depth + 1)) as T;
|
|
247
|
+
}
|
|
248
|
+
if (!isPlainObject(value)) {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
const usedKeys = new Set<string>();
|
|
252
|
+
return Object.fromEntries(
|
|
253
|
+
Object.entries(value).map(([key, child]) => {
|
|
254
|
+
const safeKey = nextUniqueKey(redactSensitiveKey(key, knownSecrets), usedKeys);
|
|
255
|
+
if (isSensitiveFieldName(key)) {
|
|
256
|
+
return [safeKey, REDACTED] as const;
|
|
257
|
+
}
|
|
258
|
+
if (normalizeFieldName(key) === "headers") {
|
|
259
|
+
return [safeKey, redactHeaderMap(child, knownSecrets, seen, depth + 1)] as const;
|
|
260
|
+
}
|
|
261
|
+
return [safeKey, redactSensitiveDataDeep(child, knownSecrets, seen, depth + 1)] as const;
|
|
262
|
+
}),
|
|
263
|
+
) as T;
|
|
264
|
+
} finally {
|
|
265
|
+
seen.delete(value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function redactHeaderMap(
|
|
270
|
+
value: unknown,
|
|
271
|
+
knownSecrets: readonly SecretForRedaction[],
|
|
272
|
+
seen: WeakSet<object>,
|
|
273
|
+
depth: number,
|
|
274
|
+
): unknown {
|
|
275
|
+
if (!isPlainObject(value)) {
|
|
276
|
+
return redactSensitiveDataDeep(value, knownSecrets, seen, depth);
|
|
277
|
+
}
|
|
278
|
+
if (depth >= MAX_REDACTION_DEPTH) return DEPTH_MARKER;
|
|
279
|
+
if (seen.has(value)) return CYCLE_MARKER;
|
|
280
|
+
seen.add(value);
|
|
281
|
+
try {
|
|
282
|
+
const usedKeys = new Set<string>();
|
|
283
|
+
return Object.fromEntries(
|
|
284
|
+
Object.entries(value).map(([key, child]) => {
|
|
285
|
+
const safeKey = nextUniqueKey(redactSensitiveKey(key, knownSecrets), usedKeys);
|
|
286
|
+
return [
|
|
287
|
+
safeKey,
|
|
288
|
+
isCredentialHeaderName(key)
|
|
289
|
+
? REDACTED
|
|
290
|
+
: redactSensitiveDataDeep(child, knownSecrets, seen, depth + 1),
|
|
291
|
+
];
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
} finally {
|
|
295
|
+
seen.delete(value);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function prepareSecrets(knownSecrets: readonly SecretForRedaction[]): PreparedSecret[] {
|
|
300
|
+
const unique = new Map<string, string>();
|
|
301
|
+
for (const secret of knownSecrets) {
|
|
302
|
+
if (secret.value.length < MIN_REDACTABLE_VALUE_LENGTH || unique.has(secret.value)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
unique.set(secret.value, `[redacted:${safeSecretName(secret.name)}]`);
|
|
306
|
+
}
|
|
307
|
+
return [...unique]
|
|
308
|
+
.map(([value, marker]) => ({ marker, value }))
|
|
309
|
+
.sort((a, b) => b.value.length - a.value.length || a.marker.localeCompare(b.marker));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function replacePreparedSecrets(text: string, prepared: readonly PreparedSecret[]): string {
|
|
313
|
+
let redacted = text;
|
|
314
|
+
for (const secret of prepared) {
|
|
315
|
+
if (redacted.includes(secret.value)) {
|
|
316
|
+
redacted = redacted.split(secret.value).join(secret.marker);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return redacted;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function nextUniqueKey(base: string, usedKeys: Set<string>): string {
|
|
323
|
+
let candidate = base;
|
|
324
|
+
let suffix = 2;
|
|
325
|
+
while (usedKeys.has(candidate)) {
|
|
326
|
+
candidate = `${base}#${suffix}`;
|
|
327
|
+
suffix += 1;
|
|
328
|
+
}
|
|
329
|
+
usedKeys.add(candidate);
|
|
330
|
+
return candidate;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function safeSecretName(name: string): string {
|
|
334
|
+
const safe = name
|
|
335
|
+
.toUpperCase()
|
|
336
|
+
.replace(/[^A-Z0-9_]+/g, "_")
|
|
337
|
+
.replace(/^_+|_+$/g, "");
|
|
338
|
+
return safe.slice(0, 64) || "KNOWN_SECRET";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeFieldName(name: string): string {
|
|
342
|
+
return name.toLowerCase().replace(/[-_\s]/g, "");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
346
|
+
if (!value || typeof value !== "object") return false;
|
|
347
|
+
const prototype = Object.getPrototypeOf(value);
|
|
348
|
+
return prototype === Object.prototype || prototype === null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isRedactionMarker(value: string): boolean {
|
|
352
|
+
return /^\[redacted(?::[A-Z0-9_]{1,64})?\]$/.test(value);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function stripMatchingQuotes(value: string): string {
|
|
356
|
+
if (
|
|
357
|
+
value.length >= 2 &&
|
|
358
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
359
|
+
(value.startsWith("'") && value.endsWith("'")))
|
|
360
|
+
) {
|
|
361
|
+
return value.slice(1, -1);
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS = 262_144;
|
|
4
|
+
export const WORKSPACE_INSTRUCTION_POLICY_REASON_MAX_CHARS = 4_096;
|
|
5
|
+
export const WORKSPACE_INSTRUCTION_POLICY_ROLE_KEY_MAX_CHARS = 64;
|
|
6
|
+
export const WORKSPACE_INSTRUCTION_POLICY_SOURCE_ID_MAX_CHARS = 512;
|
|
7
|
+
|
|
8
|
+
export const WorkspaceInstructionPolicyKind = z.enum(["charter", "policy"]);
|
|
9
|
+
export type WorkspaceInstructionPolicyKind = z.infer<typeof WorkspaceInstructionPolicyKind>;
|
|
10
|
+
|
|
11
|
+
export const WorkspaceInstructionPolicyScope = z.enum(["global", "role"]);
|
|
12
|
+
export type WorkspaceInstructionPolicyScope = z.infer<typeof WorkspaceInstructionPolicyScope>;
|
|
13
|
+
|
|
14
|
+
export const WorkspaceInstructionPolicyProvenanceSource = z.enum([
|
|
15
|
+
"human",
|
|
16
|
+
"onboarding",
|
|
17
|
+
"knowledge_proposal",
|
|
18
|
+
"legacy_import",
|
|
19
|
+
]);
|
|
20
|
+
export type WorkspaceInstructionPolicyProvenanceSource = z.infer<
|
|
21
|
+
typeof WorkspaceInstructionPolicyProvenanceSource
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export const WorkspaceInstructionPolicyDraftProvenanceSource = z.enum([
|
|
25
|
+
"human",
|
|
26
|
+
"onboarding",
|
|
27
|
+
"knowledge_proposal",
|
|
28
|
+
]);
|
|
29
|
+
export type WorkspaceInstructionPolicyDraftProvenanceSource = z.infer<
|
|
30
|
+
typeof WorkspaceInstructionPolicyDraftProvenanceSource
|
|
31
|
+
>;
|
|
32
|
+
|
|
33
|
+
export const WorkspaceInstructionPolicyActivationType = z.enum(["activate", "rollback"]);
|
|
34
|
+
export type WorkspaceInstructionPolicyActivationType = z.infer<
|
|
35
|
+
typeof WorkspaceInstructionPolicyActivationType
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export const WorkspaceInstructionPolicyRoleKey = z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(WORKSPACE_INSTRUCTION_POLICY_ROLE_KEY_MAX_CHARS)
|
|
42
|
+
.regex(/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/);
|
|
43
|
+
export type WorkspaceInstructionPolicyRoleKey = z.infer<typeof WorkspaceInstructionPolicyRoleKey>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Role-policy keys are identifiers rather than display names. Normalize once
|
|
47
|
+
* at ingress so activation uniqueness and every client use the same key.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeWorkspaceInstructionPolicyRoleKey(value: string): string {
|
|
50
|
+
return value.normalize("NFKC").trim().toLowerCase().replace(/\s+/gu, "-").replace(/-+/g, "-");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const RequestRoleKey = z
|
|
54
|
+
.string()
|
|
55
|
+
.transform(normalizeWorkspaceInstructionPolicyRoleKey)
|
|
56
|
+
.pipe(WorkspaceInstructionPolicyRoleKey);
|
|
57
|
+
|
|
58
|
+
const targetShape = {
|
|
59
|
+
kind: WorkspaceInstructionPolicyKind,
|
|
60
|
+
scope: WorkspaceInstructionPolicyScope,
|
|
61
|
+
roleKey: WorkspaceInstructionPolicyRoleKey.nullable(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function validateTarget(
|
|
65
|
+
value: {
|
|
66
|
+
kind: WorkspaceInstructionPolicyKind;
|
|
67
|
+
scope: WorkspaceInstructionPolicyScope;
|
|
68
|
+
roleKey: string | null;
|
|
69
|
+
},
|
|
70
|
+
context: z.RefinementCtx,
|
|
71
|
+
): void {
|
|
72
|
+
if (value.kind === "charter" && (value.scope !== "global" || value.roleKey !== null)) {
|
|
73
|
+
context.addIssue({
|
|
74
|
+
code: "custom",
|
|
75
|
+
path: ["scope"],
|
|
76
|
+
message: "a charter must use global scope with no role key",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (value.scope === "global" && value.roleKey !== null) {
|
|
80
|
+
context.addIssue({
|
|
81
|
+
code: "custom",
|
|
82
|
+
path: ["roleKey"],
|
|
83
|
+
message: "a global instruction policy must not have a role key",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (value.scope === "role" && (value.kind !== "policy" || value.roleKey === null)) {
|
|
87
|
+
context.addIssue({
|
|
88
|
+
code: "custom",
|
|
89
|
+
path: ["roleKey"],
|
|
90
|
+
message: "role scope requires a policy and a normalized role key",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const WorkspaceInstructionPolicyTarget = z.object(targetShape).superRefine(validateTarget);
|
|
96
|
+
export type WorkspaceInstructionPolicyTarget = z.infer<typeof WorkspaceInstructionPolicyTarget>;
|
|
97
|
+
|
|
98
|
+
export const WorkspaceInstructionPolicyProvenance = z.object({
|
|
99
|
+
source: WorkspaceInstructionPolicyProvenanceSource,
|
|
100
|
+
sourceId: z.string().min(1).max(WORKSPACE_INSTRUCTION_POLICY_SOURCE_ID_MAX_CHARS).nullable(),
|
|
101
|
+
});
|
|
102
|
+
export type WorkspaceInstructionPolicyProvenance = z.infer<
|
|
103
|
+
typeof WorkspaceInstructionPolicyProvenance
|
|
104
|
+
>;
|
|
105
|
+
|
|
106
|
+
const revisionIdentityShape = {
|
|
107
|
+
id: z.string().uuid(),
|
|
108
|
+
revision: z.number().int().positive(),
|
|
109
|
+
contentHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const WorkspaceInstructionPolicyRevisionIdentity = z.object(revisionIdentityShape);
|
|
113
|
+
export type WorkspaceInstructionPolicyRevisionIdentity = z.infer<
|
|
114
|
+
typeof WorkspaceInstructionPolicyRevisionIdentity
|
|
115
|
+
>;
|
|
116
|
+
|
|
117
|
+
export const WorkspaceInstructionPolicyRevision = z.object({
|
|
118
|
+
...revisionIdentityShape,
|
|
119
|
+
accountId: z.string().uuid(),
|
|
120
|
+
workspaceId: z.string().uuid(),
|
|
121
|
+
...targetShape,
|
|
122
|
+
content: z.string().min(1).max(WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS),
|
|
123
|
+
provenance: WorkspaceInstructionPolicyProvenance,
|
|
124
|
+
supersedesRevisionId: z.string().uuid().nullable(),
|
|
125
|
+
createdBySubjectId: z.string().min(1),
|
|
126
|
+
createdAt: z.string().datetime(),
|
|
127
|
+
});
|
|
128
|
+
export type WorkspaceInstructionPolicyRevision = z.infer<typeof WorkspaceInstructionPolicyRevision>;
|
|
129
|
+
|
|
130
|
+
export const WorkspaceInstructionPolicyHead = z.object({
|
|
131
|
+
workspaceId: z.string().uuid(),
|
|
132
|
+
...targetShape,
|
|
133
|
+
revisionId: z.string().uuid(),
|
|
134
|
+
revision: z.number().int().positive(),
|
|
135
|
+
contentHash: z.string().regex(/^[0-9a-f]{64}$/),
|
|
136
|
+
activationVersion: z.number().int().positive(),
|
|
137
|
+
activatedAt: z.string().datetime(),
|
|
138
|
+
});
|
|
139
|
+
export type WorkspaceInstructionPolicyHead = z.infer<typeof WorkspaceInstructionPolicyHead>;
|
|
140
|
+
|
|
141
|
+
export const WorkspaceInstructionPolicyActivationEvent = z.object({
|
|
142
|
+
id: z.string().uuid(),
|
|
143
|
+
accountId: z.string().uuid(),
|
|
144
|
+
workspaceId: z.string().uuid(),
|
|
145
|
+
...targetShape,
|
|
146
|
+
type: WorkspaceInstructionPolicyActivationType,
|
|
147
|
+
activationVersion: z.number().int().positive(),
|
|
148
|
+
oldRevision: WorkspaceInstructionPolicyRevisionIdentity.nullable(),
|
|
149
|
+
newRevision: WorkspaceInstructionPolicyRevisionIdentity,
|
|
150
|
+
actorSubjectId: z.string().min(1),
|
|
151
|
+
reason: z.string().min(1).max(WORKSPACE_INSTRUCTION_POLICY_REASON_MAX_CHARS),
|
|
152
|
+
createdAt: z.string().datetime(),
|
|
153
|
+
});
|
|
154
|
+
export type WorkspaceInstructionPolicyActivationEvent = z.infer<
|
|
155
|
+
typeof WorkspaceInstructionPolicyActivationEvent
|
|
156
|
+
>;
|
|
157
|
+
|
|
158
|
+
export const CreateWorkspaceInstructionPolicyDraftRequest = z
|
|
159
|
+
.object({
|
|
160
|
+
kind: WorkspaceInstructionPolicyKind,
|
|
161
|
+
scope: WorkspaceInstructionPolicyScope,
|
|
162
|
+
roleKey: RequestRoleKey.nullable().default(null),
|
|
163
|
+
content: z
|
|
164
|
+
.string()
|
|
165
|
+
.min(1)
|
|
166
|
+
.max(WORKSPACE_INSTRUCTION_POLICY_CONTENT_MAX_CHARS)
|
|
167
|
+
.refine((value) => value.trim().length > 0, "instruction policy content must not be blank"),
|
|
168
|
+
provenanceSource: WorkspaceInstructionPolicyDraftProvenanceSource.default("human"),
|
|
169
|
+
provenanceSourceId: z
|
|
170
|
+
.string()
|
|
171
|
+
.min(1)
|
|
172
|
+
.max(WORKSPACE_INSTRUCTION_POLICY_SOURCE_ID_MAX_CHARS)
|
|
173
|
+
.nullable()
|
|
174
|
+
.default(null),
|
|
175
|
+
supersedesRevisionId: z.string().uuid().nullable().default(null),
|
|
176
|
+
})
|
|
177
|
+
.superRefine(validateTarget);
|
|
178
|
+
export type CreateWorkspaceInstructionPolicyDraftRequest = z.infer<
|
|
179
|
+
typeof CreateWorkspaceInstructionPolicyDraftRequest
|
|
180
|
+
>;
|
|
181
|
+
|
|
182
|
+
export const ImportLegacyWorkspaceInstructionPolicyDraftRequest = z
|
|
183
|
+
.object({
|
|
184
|
+
supersedesRevisionId: z.string().uuid().nullable().default(null),
|
|
185
|
+
})
|
|
186
|
+
.strict();
|
|
187
|
+
export type ImportLegacyWorkspaceInstructionPolicyDraftRequest = z.infer<
|
|
188
|
+
typeof ImportLegacyWorkspaceInstructionPolicyDraftRequest
|
|
189
|
+
>;
|
|
190
|
+
|
|
191
|
+
export const WorkspaceInstructionPolicyListQuery = z.object({
|
|
192
|
+
kind: WorkspaceInstructionPolicyKind.optional(),
|
|
193
|
+
scope: WorkspaceInstructionPolicyScope.optional(),
|
|
194
|
+
roleKey: RequestRoleKey.nullable().optional(),
|
|
195
|
+
afterRevision: z.coerce.number().int().positive().optional(),
|
|
196
|
+
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
197
|
+
});
|
|
198
|
+
export type WorkspaceInstructionPolicyListQuery = z.infer<
|
|
199
|
+
typeof WorkspaceInstructionPolicyListQuery
|
|
200
|
+
>;
|
|
201
|
+
|
|
202
|
+
export const WorkspaceInstructionPolicyListResponse = z.object({
|
|
203
|
+
revisions: z.array(WorkspaceInstructionPolicyRevision),
|
|
204
|
+
activeHeads: z.array(WorkspaceInstructionPolicyHead),
|
|
205
|
+
activationEvents: z.array(WorkspaceInstructionPolicyActivationEvent),
|
|
206
|
+
nextAfterRevision: z.number().int().positive().nullable(),
|
|
207
|
+
});
|
|
208
|
+
export type WorkspaceInstructionPolicyListResponse = z.infer<
|
|
209
|
+
typeof WorkspaceInstructionPolicyListResponse
|
|
210
|
+
>;
|
|
211
|
+
|
|
212
|
+
export const WorkspaceInstructionPolicyDiffRequest = z
|
|
213
|
+
.object({
|
|
214
|
+
fromRevisionId: z.string().uuid(),
|
|
215
|
+
toRevisionId: z.string().uuid(),
|
|
216
|
+
})
|
|
217
|
+
.refine((value) => value.fromRevisionId !== value.toRevisionId, {
|
|
218
|
+
message: "diff revisions must be different",
|
|
219
|
+
path: ["toRevisionId"],
|
|
220
|
+
});
|
|
221
|
+
export type WorkspaceInstructionPolicyDiffRequest = z.infer<
|
|
222
|
+
typeof WorkspaceInstructionPolicyDiffRequest
|
|
223
|
+
>;
|
|
224
|
+
|
|
225
|
+
export const WorkspaceInstructionPolicyDiffResponse = z.object({
|
|
226
|
+
from: WorkspaceInstructionPolicyRevision,
|
|
227
|
+
to: WorkspaceInstructionPolicyRevision,
|
|
228
|
+
format: z.literal("unified"),
|
|
229
|
+
diff: z.string(),
|
|
230
|
+
});
|
|
231
|
+
export type WorkspaceInstructionPolicyDiffResponse = z.infer<
|
|
232
|
+
typeof WorkspaceInstructionPolicyDiffResponse
|
|
233
|
+
>;
|
|
234
|
+
|
|
235
|
+
export const ActivateWorkspaceInstructionPolicyRequest = z.object({
|
|
236
|
+
expectedCurrentRevisionId: z.string().uuid().nullable(),
|
|
237
|
+
reason: z.string().trim().min(1).max(WORKSPACE_INSTRUCTION_POLICY_REASON_MAX_CHARS),
|
|
238
|
+
});
|
|
239
|
+
export type ActivateWorkspaceInstructionPolicyRequest = z.infer<
|
|
240
|
+
typeof ActivateWorkspaceInstructionPolicyRequest
|
|
241
|
+
>;
|
|
242
|
+
|
|
243
|
+
export const RollbackWorkspaceInstructionPolicyRequest = z.object({
|
|
244
|
+
targetRevisionId: z.string().uuid(),
|
|
245
|
+
expectedCurrentRevisionId: z.string().uuid(),
|
|
246
|
+
reason: z.string().trim().min(1).max(WORKSPACE_INSTRUCTION_POLICY_REASON_MAX_CHARS),
|
|
247
|
+
});
|
|
248
|
+
export type RollbackWorkspaceInstructionPolicyRequest = z.infer<
|
|
249
|
+
typeof RollbackWorkspaceInstructionPolicyRequest
|
|
250
|
+
>;
|
|
251
|
+
|
|
252
|
+
export const WorkspaceInstructionPolicyActivationResponse = z.object({
|
|
253
|
+
head: WorkspaceInstructionPolicyHead,
|
|
254
|
+
event: WorkspaceInstructionPolicyActivationEvent,
|
|
255
|
+
});
|
|
256
|
+
export type WorkspaceInstructionPolicyActivationResponse = z.infer<
|
|
257
|
+
typeof WorkspaceInstructionPolicyActivationResponse
|
|
258
|
+
>;
|
|
259
|
+
|
|
260
|
+
export const WorkspaceInstructionPolicyConflictResponse = z.object({
|
|
261
|
+
code: z.literal("WORKSPACE_INSTRUCTION_POLICY_CONFLICT"),
|
|
262
|
+
message: z.string(),
|
|
263
|
+
currentHead: WorkspaceInstructionPolicyHead.nullable(),
|
|
264
|
+
});
|
|
265
|
+
export type WorkspaceInstructionPolicyConflictResponse = z.infer<
|
|
266
|
+
typeof WorkspaceInstructionPolicyConflictResponse
|
|
267
|
+
>;
|