@company-semantics/contracts 2.4.1 → 2.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -115,7 +115,7 @@
115
115
  "zod": "^4.4.3"
116
116
  },
117
117
  "devDependencies": {
118
- "@types/node": "^25.9.1",
118
+ "@types/node": "^22.19.19",
119
119
  "husky": "^9.1.7",
120
120
  "lint-staged": "^17.0.5",
121
121
  "markdownlint-cli2": "^0.22.1",
@@ -3234,7 +3234,7 @@ export interface components {
3234
3234
  level: "none" | "viewer" | "commenter" | "editor";
3235
3235
  reasons: {
3236
3236
  /** @enum {string} */
3237
- source: "org_rbac" | "sharing_policy" | "unit_baseline" | "acl_grant" | "doc_ownership";
3237
+ source: "org_rbac" | "sharing_policy" | "unit_baseline" | "unit_delegation" | "acl_grant" | "doc_ownership";
3238
3238
  detail: string;
3239
3239
  }[];
3240
3240
  canShare: boolean;
@@ -57,14 +57,19 @@ export const openApiRoutes = {
57
57
  '/api/org-units/{unitId}/ancestors': ['GET'],
58
58
  '/api/org-units/{unitId}/archive': ['POST'],
59
59
  '/api/org-units/{unitId}/children': ['GET'],
60
+ '/api/org-units/{unitId}/delegations': ['POST'],
61
+ '/api/org-units/{unitId}/delegations/{id}': ['DELETE', 'PATCH'],
60
62
  '/api/org-units/{unitId}/descendants': ['GET'],
61
63
  '/api/org-units/{unitId}/memberships': ['GET', 'POST'],
62
64
  '/api/org-units/{unitId}/memberships/{userId}': ['DELETE'],
63
65
  '/api/org-units/{unitId}/memberships/{userId}/role': ['PUT'],
66
+ '/api/org-units/{unitId}/owners': ['GET'],
64
67
  '/api/org-units/{unitId}/permissions': ['GET'],
65
68
  '/api/org-units/{unitId}/relationships': ['GET', 'POST'],
66
69
  '/api/org-units/{unitId}/reorder': ['POST'],
67
70
  '/api/org-units/{unitId}/reparent': ['POST'],
71
+ '/api/org-units/{unitId}/structural-leaders': ['POST'],
72
+ '/api/org-units/{unitId}/structural-leaders/{userId}': ['DELETE'],
68
73
  '/api/org/cancel-deletion': ['POST'],
69
74
  '/api/org/delete': ['POST'],
70
75
  '/api/org/deletion-eligibility': ['GET'],
@@ -75,6 +80,8 @@ export const openApiRoutes = {
75
80
  '/api/org/transfer-ownership/accept': ['POST'],
76
81
  '/api/org/transfer-ownership/preview': ['POST'],
77
82
  '/api/org/transfer-ownership/status': ['GET'],
83
+ '/api/orgs/{orgId}/admins': ['POST'],
84
+ '/api/orgs/{orgId}/admins/{userId}': ['DELETE'],
78
85
  '/api/orgs/{orgId}/ai-usage': ['GET'],
79
86
  '/api/orgs/{orgId}/billing': ['GET'],
80
87
  '/api/orgs/{orgId}/budget-config': ['GET', 'PUT'],
@@ -93,6 +100,8 @@ export const openApiRoutes = {
93
100
  '/api/user/profile': ['PATCH'],
94
101
  '/api/user/resync-slack-avatar': ['POST'],
95
102
  '/api/users/org-chart': ['GET'],
103
+ '/api/users/org-chart/import': ['POST'],
104
+ '/api/users/org-chart/import/{operationId}/retry': ['POST'],
96
105
  '/api/work-items/{id}': ['GET'],
97
106
  '/api/work-items/{id}/content': ['PUT'],
98
107
  '/api/work-items/{id}/title': ['PUT'],
package/src/index.ts CHANGED
@@ -663,6 +663,27 @@ export { openApiRoutes, type OpenApiRoute, type OpenApiMethod } from './generate
663
663
  export type { Secret } from './security/index'
664
664
  export { wrapSecret, unwrapSecret } from './security/index'
665
665
 
666
+ // Org secrets DTO schemas (PRD-00629)
667
+ // @see src/security/org-secrets.ts for invariants (no value field on summary)
668
+ export {
669
+ UsageClassSchema,
670
+ OrgSecretsActionSchema,
671
+ SecretValueStringSchema,
672
+ OrgSecretSummarySchema,
673
+ CreateSecretRequestSchema,
674
+ RotateSecretRequestSchema,
675
+ DisableSecretRequestSchema,
676
+ } from './security/index'
677
+ export type {
678
+ UsageClass,
679
+ OrgSecretsAction,
680
+ SecretValueString,
681
+ OrgSecretSummary,
682
+ CreateSecretRequest,
683
+ RotateSecretRequest,
684
+ DisableSecretRequest,
685
+ } from './security/index'
686
+
666
687
  // Analytics response metadata (shared vocabulary for OLTP/OLAP separation)
667
688
  // @see ADR-CTRL-053 for design rationale
668
689
  export type { AnalyticsBackend, AnalyticsResponseMeta } from './types/analytics'
@@ -722,3 +743,7 @@ export type {
722
743
  ControllerInput,
723
744
  ControllerOutput,
724
745
  } from './autotune'
746
+
747
+ // Meeting recorder vocabulary (PRD-00651)
748
+ // @see ./meetings/schemas.ts for invariants
749
+ export * from './meetings'
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Meeting recorder vocabulary barrel (PRD-00651).
3
+ *
4
+ * @see ./schemas.ts for the full schema definitions and invariants.
5
+ */
6
+ export {
7
+ RecordingIdSchema,
8
+ RecordingSourceSchema,
9
+ DetectedMeetingAppSchema,
10
+ TranscriptionProviderSchema,
11
+ RecordingStatusSchema,
12
+ RecordingQualitySchema,
13
+ MeetingVisibilitySchema,
14
+ SourceSegmentSchema,
15
+ TranscriptChunkSchema,
16
+ RecordingEventSchema,
17
+ TranscriptionSessionGrantSchema,
18
+ MeetingMetadataProjectionSchema,
19
+ } from './schemas';
20
+
21
+ export type {
22
+ RecordingId,
23
+ RecordingSource,
24
+ DetectedMeetingApp,
25
+ TranscriptionProvider,
26
+ RecordingStatus,
27
+ RecordingQuality,
28
+ MeetingVisibility,
29
+ SourceSegment,
30
+ TranscriptChunk,
31
+ RecordingEvent,
32
+ TranscriptionSessionGrant,
33
+ MeetingMetadataProjection,
34
+ } from './schemas';
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Meeting recorder shared vocabulary (PRD-00651).
3
+ *
4
+ * Zod schemas for the meeting capture / transcription pipeline. Consumed by
5
+ * the mac shell (capture runtime), backend (persistence + event projection),
6
+ * and app (UI surfaces). Lives in contracts because the boundary crosses
7
+ * three independent repos and breaking it would require coordinated releases
8
+ * (see CONTRACTS_POLICY.md promotion rule).
9
+ *
10
+ * INVARIANTS this vocabulary enforces:
11
+ * - RecordingId is the unified trace key (INV-MTG-7) — ULID, sortable,
12
+ * opaque to consumers.
13
+ * - The transcription grant is provider-agnostic (INV-MTG-15): the mac
14
+ * shell never sees a provider name; only `connectUrl` + `authHeaders` +
15
+ * audio config.
16
+ * - Visibility is meeting-scoped by default (`meeting_only`) and only
17
+ * widens via explicit policy decisions.
18
+ *
19
+ * Per the durable user preference `feedback_structured_metadata_not_json_strings`,
20
+ * descriptions live in `.meta({ description })` — structured metadata, not
21
+ * stringified JSON blobs on the schema.
22
+ */
23
+ import { z } from 'zod';
24
+
25
+ // =============================================================================
26
+ // Recording identity
27
+ // =============================================================================
28
+
29
+ /**
30
+ * ULID (Crockford base32, 26 chars). Used as the unified trace key spanning
31
+ * mac → backend → app per INV-MTG-7. Sortable by encoded timestamp prefix so
32
+ * recordings can be ordered without a separate `createdAt` column read.
33
+ */
34
+ export const RecordingIdSchema = z
35
+ .string()
36
+ .regex(/^[0-9A-HJKMNP-TV-Z]{26}$/, 'ULID required')
37
+ .meta({ description: 'ULID; unified trace key for the recording (INV-MTG-7).' });
38
+ export type RecordingId = z.infer<typeof RecordingIdSchema>;
39
+
40
+ // =============================================================================
41
+ // Enums (vocabulary)
42
+ // =============================================================================
43
+
44
+ /** Which capture stream a chunk came from. Two channels: mic (channel 0) and system (channel 1). */
45
+ export const RecordingSourceSchema = z.enum(['mic', 'system']).meta({
46
+ description: 'Capture stream identity. Mic = channel 0, system audio = channel 1.',
47
+ });
48
+ export type RecordingSource = z.infer<typeof RecordingSourceSchema>;
49
+
50
+ /**
51
+ * Which meeting app the system audio is coming from. `manual` covers the
52
+ * "user started a recording with no detected app" debug path. The
53
+ * `meet-browser` variant covers Google Meet running inside a browser tab,
54
+ * which the BrowserAudioDetector heuristic identifies (PRD-00655).
55
+ */
56
+ export const DetectedMeetingAppSchema = z
57
+ .enum(['zoom', 'teams', 'meet-browser', 'slack-huddle', 'manual'])
58
+ .meta({ description: 'Auto-detected meeting app that owns the system audio stream.' });
59
+ export type DetectedMeetingApp = z.infer<typeof DetectedMeetingAppSchema>;
60
+
61
+ /**
62
+ * Transcription provider class. The mac shell never sees this — it's a
63
+ * backend / app concern for routing + billing visibility (INV-MTG-15 ensures
64
+ * the mac side stays provider-opaque).
65
+ */
66
+ export const TranscriptionProviderSchema = z
67
+ .enum(['deepgram', 'assemblyai', 'whisper-local'])
68
+ .meta({ description: 'Server-side selection of transcription provider; never crosses to mac.' });
69
+ export type TranscriptionProvider = z.infer<typeof TranscriptionProviderSchema>;
70
+
71
+ /**
72
+ * Lifecycle states for a recording entity. `partial-transcript-network` is
73
+ * the explicit non-binary state for "audio captured but the transcript
74
+ * stream dropped mid-session" — keeping it distinct from `failed` lets the
75
+ * UI surface a remediation hint without losing the captured artifact.
76
+ */
77
+ export const RecordingStatusSchema = z
78
+ .enum([
79
+ 'draft',
80
+ 'recording',
81
+ 'processing',
82
+ 'finalized',
83
+ 'failed',
84
+ 'partial-transcript-network',
85
+ 'cancelled',
86
+ ])
87
+ .meta({ description: 'Coarse lifecycle state for a recording entity.' });
88
+ export type RecordingStatus = z.infer<typeof RecordingStatusSchema>;
89
+
90
+ /** Quality signal emitted by the capture runtime. Used to surface degradation in the UI. */
91
+ export const RecordingQualitySchema = z.enum(['clean', 'degraded']).meta({
92
+ description: 'Capture-runtime quality signal. `degraded` triggers a UI hint.',
93
+ });
94
+ export type RecordingQuality = z.infer<typeof RecordingQualitySchema>;
95
+
96
+ /**
97
+ * Visibility band for the finalized meeting projection. `meeting_only` is the
98
+ * default — captured material stays visible only to attendees of the meeting
99
+ * itself. `shared` opens it to anyone the owner explicitly shares with;
100
+ * `org` opens it to the org; `finalized_private` is the explicit "private
101
+ * even after finalization" lock per the participant-only constraint
102
+ * (`feedback_retrieval_default_off_for_noisy_sources`).
103
+ */
104
+ export const MeetingVisibilitySchema = z
105
+ .enum(['meeting_only', 'shared', 'org', 'finalized_private'])
106
+ .meta({
107
+ description:
108
+ 'Visibility band for the finalized meeting projection. Default `meeting_only`.',
109
+ });
110
+ export type MeetingVisibility = z.infer<typeof MeetingVisibilitySchema>;
111
+
112
+ // =============================================================================
113
+ // Source segments (the per-capture-stream view of a recording)
114
+ // =============================================================================
115
+
116
+ /**
117
+ * A continuous segment captured from a single stream within a recording.
118
+ * One recording has at least one segment per active source. New segments
119
+ * are emitted when the upstream signal pauses and resumes.
120
+ */
121
+ export const SourceSegmentSchema = z
122
+ .object({
123
+ recordingId: RecordingIdSchema,
124
+ source: RecordingSourceSchema,
125
+ sequence: z.number().int().nonnegative(),
126
+ startedAtMs: z.number().int().nonnegative(),
127
+ endedAtMs: z.number().int().nonnegative().nullable(),
128
+ sampleRateHz: z.literal(16000),
129
+ channels: z.literal(1),
130
+ })
131
+ .meta({ description: 'Per-source continuous capture segment within a recording.' });
132
+ export type SourceSegment = z.infer<typeof SourceSegmentSchema>;
133
+
134
+ // =============================================================================
135
+ // Transcript chunks (the per-window result of transcription)
136
+ // =============================================================================
137
+
138
+ /**
139
+ * A single transcript window from the provider. `final = true` means the
140
+ * provider has committed to this text; `false` is an interim hypothesis.
141
+ * The mac shell forwards both kinds — the UI is responsible for collapsing
142
+ * interim results on top of the latest commit.
143
+ */
144
+ export const TranscriptChunkSchema = z
145
+ .object({
146
+ recordingId: RecordingIdSchema,
147
+ source: RecordingSourceSchema,
148
+ sequence: z.number().int().nonnegative(),
149
+ startedAtMs: z.number().int().nonnegative(),
150
+ endedAtMs: z.number().int().nonnegative(),
151
+ text: z.string(),
152
+ final: z.boolean(),
153
+ speakerLabel: z.string().nullable().optional(),
154
+ confidence: z.number().min(0).max(1).optional(),
155
+ })
156
+ .meta({ description: 'A single transcript window (interim or final) from the provider.' });
157
+ export type TranscriptChunk = z.infer<typeof TranscriptChunkSchema>;
158
+
159
+ // =============================================================================
160
+ // Recording events (the event stream that drives backend persistence)
161
+ // =============================================================================
162
+
163
+ const SessionOpenedEventSchema = z.object({
164
+ kind: z.literal('session_opened'),
165
+ recordingId: RecordingIdSchema,
166
+ atMs: z.number().int().nonnegative(),
167
+ });
168
+
169
+ const ChunkBatchEventSchema = z.object({
170
+ kind: z.literal('chunk_batch'),
171
+ recordingId: RecordingIdSchema,
172
+ chunks: z.array(TranscriptChunkSchema).min(1),
173
+ });
174
+
175
+ const QualityDegradedEventSchema = z.object({
176
+ kind: z.literal('quality_degraded'),
177
+ recordingId: RecordingIdSchema,
178
+ reason: z.string().min(1),
179
+ atMs: z.number().int().nonnegative(),
180
+ });
181
+
182
+ const SessionStoppedEventSchema = z.object({
183
+ kind: z.literal('session_stopped'),
184
+ recordingId: RecordingIdSchema,
185
+ atMs: z.number().int().nonnegative(),
186
+ expectedLastSequence: z.number().int().nonnegative(),
187
+ });
188
+
189
+ /**
190
+ * Discriminated union of events emitted by the mac shell during a recording
191
+ * session. The backend consumes these and projects them onto the
192
+ * `MeetingMetadataProjection` row.
193
+ *
194
+ * Ordering invariant: `session_opened` is always the first event for a
195
+ * recordingId; `session_stopped` is always the last. `chunk_batch` and
196
+ * `quality_degraded` may interleave.
197
+ */
198
+ export const RecordingEventSchema = z
199
+ .discriminatedUnion('kind', [
200
+ SessionOpenedEventSchema,
201
+ ChunkBatchEventSchema,
202
+ QualityDegradedEventSchema,
203
+ SessionStoppedEventSchema,
204
+ ])
205
+ .meta({
206
+ description:
207
+ 'Event emitted by the capture runtime during a recording session (INV-MTG ordering).',
208
+ });
209
+ export type RecordingEvent = z.infer<typeof RecordingEventSchema>;
210
+
211
+ // =============================================================================
212
+ // Transcription session grant (provider-agnostic handshake)
213
+ // =============================================================================
214
+
215
+ /**
216
+ * Server-issued grant that authorizes the mac shell to open a WebSocket to
217
+ * the transcription provider. Per INV-MTG-15 the mac never sees a provider
218
+ * name — only `connectUrl`, `authHeaders`, and the audio config it must
219
+ * speak. `authHeaders` carries an ephemeral token; rotation is the server's
220
+ * job, and the grant has a short `expiresAt`.
221
+ */
222
+ export const TranscriptionSessionGrantSchema = z
223
+ .object({
224
+ sessionId: RecordingIdSchema,
225
+ connectUrl: z.string().url(),
226
+ authHeaders: z.record(z.string(), z.string()),
227
+ expiresAt: z.string().datetime(),
228
+ audioConfig: z.object({
229
+ sampleRateHz: z.literal(16000),
230
+ channels: z.literal(2),
231
+ encoding: z.literal('linear16'),
232
+ chunkMs: z.literal(200),
233
+ }),
234
+ capabilities: z.object({
235
+ interimResults: z.boolean(),
236
+ multichannelDiarization: z.boolean(),
237
+ reconnectionTokens: z.boolean(),
238
+ }),
239
+ })
240
+ .meta({
241
+ description:
242
+ 'Provider-agnostic grant the mac shell consumes. Mac never sees provider name (INV-MTG-15).',
243
+ });
244
+ export type TranscriptionSessionGrant = z.infer<typeof TranscriptionSessionGrantSchema>;
245
+
246
+ // =============================================================================
247
+ // Meeting metadata projection (finalized read model)
248
+ // =============================================================================
249
+
250
+ /**
251
+ * Finalized projection row for a completed recording. This is what the app
252
+ * reads for the meeting list / detail views. Drafts and in-flight recordings
253
+ * are excluded from this projection per
254
+ * `feedback_live_document_semantics` — indexers filter on `status` to
255
+ * suppress drafts from the search authority.
256
+ */
257
+ export const MeetingMetadataProjectionSchema = z
258
+ .object({
259
+ recordingId: RecordingIdSchema,
260
+ ownerUserId: z.string().uuid(),
261
+ orgId: z.string().uuid(),
262
+ title: z.string().nullable(),
263
+ detectedApp: DetectedMeetingAppSchema,
264
+ visibility: MeetingVisibilitySchema,
265
+ status: RecordingStatusSchema,
266
+ quality: RecordingQualitySchema,
267
+ startedAt: z.string().datetime(),
268
+ endedAt: z.string().datetime().nullable(),
269
+ durationMs: z.number().int().nonnegative(),
270
+ participantUserIds: z.array(z.string().uuid()),
271
+ transcriptAvailable: z.boolean(),
272
+ })
273
+ .meta({
274
+ description: 'Finalized read projection for a recording (excludes drafts).',
275
+ });
276
+ export type MeetingMetadataProjection = z.infer<typeof MeetingMetadataProjectionSchema>;
@@ -98,7 +98,7 @@ describe('OrgUnitMembershipSchema', () => {
98
98
  orgId: UUID_B,
99
99
  unitId: UUID_C,
100
100
  userId: UUID_A,
101
- membershipRole: 'manager',
101
+ membershipRole: 'l1_unit_owner',
102
102
  status: 'active',
103
103
  source: 'google_groups',
104
104
  sourceRef: 'eng@example.com',
@@ -226,7 +226,7 @@ describe('Enum exhaustiveness', () => {
226
226
  describe('OrgUnitPermissionsEntrySchema', () => {
227
227
  const entry = {
228
228
  userId: UUID_A,
229
- membershipRole: 'manager' as const,
229
+ membershipRole: 'l1_unit_owner' as const,
230
230
  inheritedFromUnitId: UUID_C,
231
231
  inheritedFromUnitName: 'Engineering',
232
232
  };
@@ -1,2 +1,21 @@
1
1
  export type { Secret } from './secret';
2
2
  export { wrapSecret, unwrapSecret } from './secret';
3
+
4
+ export {
5
+ UsageClassSchema,
6
+ OrgSecretsActionSchema,
7
+ SecretValueStringSchema,
8
+ OrgSecretSummarySchema,
9
+ CreateSecretRequestSchema,
10
+ RotateSecretRequestSchema,
11
+ DisableSecretRequestSchema,
12
+ } from './org-secrets';
13
+ export type {
14
+ UsageClass,
15
+ OrgSecretsAction,
16
+ SecretValueString,
17
+ OrgSecretSummary,
18
+ CreateSecretRequest,
19
+ RotateSecretRequest,
20
+ DisableSecretRequest,
21
+ } from './org-secrets';
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Org Secrets DTO schemas (PRD-00629).
3
+ *
4
+ * Wire shapes for the `org_secrets` admin surface. The decrypted credential
5
+ * value never crosses this boundary — `OrgSecretSummarySchema` exposes only a
6
+ * masked prefix derived from the plaintext (e.g. the first few characters of
7
+ * `sk-...`), never the value itself. Plaintext travels INTO the server through
8
+ * `CreateSecretRequestSchema` / `RotateSecretRequestSchema` via
9
+ * `SecretValueStringSchema`, which is a branded bounded string so accidental
10
+ * propagation through logs / template literals is obvious in code review.
11
+ *
12
+ * Server is expected to wrap incoming `plaintext` in `Secret<T>` (see
13
+ * `./secret.ts`) before storage / encryption.
14
+ */
15
+ import { z } from 'zod';
16
+
17
+ /**
18
+ * Usage class of an org secret. Drives which resolver layer is allowed to read
19
+ * the value (AI provider keys, webhook signing secrets, generic credentials).
20
+ */
21
+ export const UsageClassSchema = z.enum(['AI_PROVIDER', 'WEBHOOK_SIGNING', 'GENERIC']);
22
+ export type UsageClass = z.infer<typeof UsageClassSchema>;
23
+
24
+ /**
25
+ * Audit-log action vocabulary for `org_secrets_audit_log`. Includes both
26
+ * lifecycle events (`created`, `rotated`, `disabled`, `enabled`) and access
27
+ * signals (`access_denied`, `explicit_export`, `unusual_access`).
28
+ */
29
+ export const OrgSecretsActionSchema = z.enum([
30
+ 'created',
31
+ 'rotated',
32
+ 'disabled',
33
+ 'enabled',
34
+ 'access_denied',
35
+ 'explicit_export',
36
+ 'unusual_access',
37
+ ]);
38
+ export type OrgSecretsAction = z.infer<typeof OrgSecretsActionSchema>;
39
+
40
+ declare const SecretValueStringBrand: unique symbol;
41
+
42
+ /**
43
+ * Plaintext credential string. Bounded (min 1, max 8192 chars) so accidental
44
+ * logs cannot leak megabytes, and branded so propagation outside the secrets
45
+ * domain is visible at the type level. Server MUST wrap in `Secret<T>` before
46
+ * storage so the multi-surface redaction protection applies.
47
+ */
48
+ export const SecretValueStringSchema = z
49
+ .string()
50
+ .min(1)
51
+ .max(8192)
52
+ .brand<typeof SecretValueStringBrand>();
53
+ export type SecretValueString = z.infer<typeof SecretValueStringSchema>;
54
+
55
+ /**
56
+ * Admin-facing summary of an `org_secrets` row.
57
+ *
58
+ * INVARIANT: this schema MUST NOT contain the decrypted `value`. The only
59
+ * credential-derived field exposed is `maskedPrefix`, a short, lossy preview.
60
+ * Adding a `value` field here would defeat the purpose of the whole subsystem;
61
+ * the secrets API never decrypts to clients.
62
+ */
63
+ export const OrgSecretSummarySchema = z.object({
64
+ id: z.string().uuid(),
65
+ orgId: z.string().uuid().nullable(),
66
+ usageClass: UsageClassSchema,
67
+ secretName: z.string().min(1),
68
+ maskedPrefix: z.string().min(1).max(64),
69
+ version: z.number().int().positive(),
70
+ enabled: z.boolean(),
71
+ lastUsedAt: z.string().datetime().nullable(),
72
+ usageCount: z.string(),
73
+ rotatedAt: z.string().datetime().nullable(),
74
+ createdAt: z.string().datetime(),
75
+ });
76
+ export type OrgSecretSummary = z.infer<typeof OrgSecretSummarySchema>;
77
+
78
+ /**
79
+ * Request body for `POST /api/admin/secrets`. `orgId` is nullable so global
80
+ * (platform-wide) secrets are expressible. `plaintext` is the only field that
81
+ * carries credential material; everything else is metadata.
82
+ */
83
+ export const CreateSecretRequestSchema = z.object({
84
+ orgId: z.string().uuid().nullable(),
85
+ usageClass: UsageClassSchema,
86
+ secretName: z.string().min(1).max(128),
87
+ plaintext: SecretValueStringSchema,
88
+ reason: z.string().max(512).optional(),
89
+ });
90
+ export type CreateSecretRequest = z.infer<typeof CreateSecretRequestSchema>;
91
+
92
+ /**
93
+ * Request body for `POST /api/admin/secrets/:id/rotate`. Re-supplies the
94
+ * plaintext under a new version; the previous version is retained per the
95
+ * rotation policy documented in the schema migration.
96
+ */
97
+ export const RotateSecretRequestSchema = z.object({
98
+ newPlaintext: SecretValueStringSchema,
99
+ reason: z.string().max(512).optional(),
100
+ });
101
+ export type RotateSecretRequest = z.infer<typeof RotateSecretRequestSchema>;
102
+
103
+ /**
104
+ * Request body for `POST /api/admin/secrets/:id/disable`. A non-empty reason
105
+ * is required so the audit trail records WHY the secret was disabled, not just
106
+ * that it was.
107
+ */
108
+ export const DisableSecretRequestSchema = z.object({
109
+ reason: z.string().min(1).max(512),
110
+ });
111
+ export type DisableSecretRequest = z.infer<typeof DisableSecretRequestSchema>;