@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.
@@ -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
+ >;