@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 +2 -2
- package/src/api/generated.ts +1 -1
- package/src/generated/openapi-routes.ts +9 -0
- package/src/index.ts +25 -0
- package/src/meetings/index.ts +34 -0
- package/src/meetings/schemas.ts +276 -0
- package/src/org/__tests__/org-units.test.ts +2 -2
- package/src/security/index.ts +19 -0
- package/src/security/org-secrets.ts +111 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "2.
|
|
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": "^
|
|
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",
|
package/src/api/generated.ts
CHANGED
|
@@ -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: '
|
|
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: '
|
|
229
|
+
membershipRole: 'l1_unit_owner' as const,
|
|
230
230
|
inheritedFromUnitId: UUID_C,
|
|
231
231
|
inheritedFromUnitName: 'Engineering',
|
|
232
232
|
};
|
package/src/security/index.ts
CHANGED
|
@@ -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>;
|