@velajs/studio-protocol 2.0.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.js ADDED
@@ -0,0 +1,775 @@
1
+ import { z } from "zod";
2
+ //#region src/errors.ts
3
+ /**
4
+ * The closed set of Studio-specific error codes. `const` array + derived union
5
+ * so the compile-time type and any runtime membership check can never drift.
6
+ * Parenthetical statuses document the intended HTTP mapping (the wire carries
7
+ * the concrete status on {@link AdminErrorBody}).
8
+ */
9
+ const STUDIO_ERROR_CODES = [
10
+ "STUDIO_DISABLED",
11
+ "STUDIO_UNAUTHORIZED",
12
+ "STUDIO_UNKNOWN_OP",
13
+ "STUDIO_OP_FORBIDDEN",
14
+ "STUDIO_SUB_TOKEN_INVALID",
15
+ "STUDIO_RATE_LIMITED",
16
+ "STUDIO_CONFIRM_REQUIRED",
17
+ "STUDIO_UNKNOWN_MODEL",
18
+ "DATA_EDIT_DISABLED",
19
+ "TIMETRAVEL_UNAVAILABLE",
20
+ "TIMETRAVEL_SCHEMA_MISMATCH",
21
+ "FEATURE_UNCONFIGURED"
22
+ ];
23
+ //#endregion
24
+ //#region src/capabilities.ts
25
+ /**
26
+ * The closed set of feature keys the UI navigates by. `const` array + derived
27
+ * union so a feature panel can light up iff the app actually wired that package.
28
+ */
29
+ const STUDIO_FEATURE_KEYS = [
30
+ "app",
31
+ "openapi",
32
+ "data",
33
+ "timeTravel",
34
+ "transfer",
35
+ "auth",
36
+ "authOrganizations",
37
+ "queue",
38
+ "schedule",
39
+ "flags",
40
+ "logs",
41
+ "audit",
42
+ "live",
43
+ "presence"
44
+ ];
45
+ //#endregion
46
+ //#region src/data.ts
47
+ /**
48
+ * Data-browser wire contract. Model discovery + row access shapes.
49
+ *
50
+ * `StudioFilterOperator` is a compatible subset of `@velajs/crud`'s
51
+ * `FilterOperator` (`crud/packages/core/src/adapter/query-types.ts`), and
52
+ * `StudioPageInfo` is a structural mirror of crud's `PageInfo` (snake_case
53
+ * preserved). Drift guards against the real crud package land in the server
54
+ * package; see the report for mirror source paths.
55
+ */
56
+ /**
57
+ * Grid filter operators — a subset of crud's `FILTER_OPERATORS`, chosen so every
58
+ * member is assignment-compatible with crud's `FilterOperator`. `const` array +
59
+ * derived union so compile-time type and runtime membership can't drift.
60
+ */
61
+ const STUDIO_FILTER_OPERATORS = [
62
+ "eq",
63
+ "ne",
64
+ "gt",
65
+ "gte",
66
+ "lt",
67
+ "lte",
68
+ "in",
69
+ "nin",
70
+ "like",
71
+ "ilike",
72
+ "null",
73
+ "between"
74
+ ];
75
+ //#endregion
76
+ //#region src/connection.ts
77
+ function isRecord(value) {
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+ function isLocalPath(value) {
81
+ return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") && !/[\\\\?#\s]/.test(value);
82
+ }
83
+ function parseStudioConnection(value) {
84
+ if (!isRecord(value) || value.protocolVersion !== 2 || !isLocalPath(value.routerBasePath) || !isLocalPath(value.adminBasePath) || !isLocalPath(value.apiRequestPath) || typeof value.sessionToken !== "string" || value.sessionToken.length === 0) throw new Error("Invalid Studio connection configuration.");
85
+ return {
86
+ protocolVersion: value.protocolVersion,
87
+ routerBasePath: value.routerBasePath,
88
+ adminBasePath: value.adminBasePath,
89
+ apiRequestPath: value.apiRequestPath,
90
+ sessionToken: value.sessionToken
91
+ };
92
+ }
93
+ //#endregion
94
+ //#region src/app.ts
95
+ function stringMap(input) {
96
+ if (input === void 0) return void 0;
97
+ if (!isRecord(input)) throw new Error("Expected string-valued headers or query parameters.");
98
+ const result = {};
99
+ for (const [key, item] of Object.entries(input)) {
100
+ if (typeof item !== "string") throw new Error("Expected string-valued headers or query parameters.");
101
+ result[key] = item;
102
+ }
103
+ return result;
104
+ }
105
+ /** Validate JSON from the local API explorer before any request can be sent. */
106
+ function parseTryItRequest(value) {
107
+ if (!isRecord(value) || typeof value.method !== "string" || !/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$/i.test(value.method) || typeof value.path !== "string" || !value.path.startsWith("/") || value.path.startsWith("//") || /[\\\\#\s{}]/.test(value.path)) throw new Error("Expected an HTTP method and a resolved absolute path on the Worker.");
108
+ return {
109
+ method: value.method.toUpperCase(),
110
+ path: value.path,
111
+ query: stringMap(value.query),
112
+ headers: stringMap(value.headers),
113
+ body: value.body
114
+ };
115
+ }
116
+ function parseTryItResponse(value) {
117
+ if (!isRecord(value) || typeof value.status !== "number" || !Number.isInteger(value.status) || value.status < 100 || value.status > 599 || !isRecord(value.headers) || !("body" in value)) throw new Error("Invalid API response.");
118
+ const headers = {};
119
+ for (const [key, item] of Object.entries(value.headers)) {
120
+ if (typeof item !== "string") throw new Error("Invalid API response headers.");
121
+ headers[key] = item;
122
+ }
123
+ return {
124
+ status: value.status,
125
+ headers,
126
+ body: value.body
127
+ };
128
+ }
129
+ //#endregion
130
+ //#region src/ops.ts
131
+ /**
132
+ * Op meta for every op. `as const satisfies` enforces total key coverage while
133
+ * preserving literal types (so `destructive` narrows exactly), which drives the
134
+ * type-level destructive-confirm guard in the tests.
135
+ */
136
+ const STUDIO_OP_META = {
137
+ "studio.capabilities": {
138
+ mode: "read",
139
+ feature: "app"
140
+ },
141
+ "app.routes": {
142
+ mode: "read",
143
+ feature: "app"
144
+ },
145
+ "app.modules": {
146
+ mode: "read",
147
+ feature: "app"
148
+ },
149
+ "app.entrypoints": {
150
+ mode: "read",
151
+ feature: "app"
152
+ },
153
+ "app.openapi": {
154
+ mode: "read",
155
+ feature: "openapi"
156
+ },
157
+ /** Authorizes a host HTTP request; server gates and audits this operational action. */
158
+ "api.authorizeTryIt": {
159
+ mode: "write",
160
+ feature: "openapi",
161
+ gate: "opsEditable"
162
+ },
163
+ "data.listModels": {
164
+ mode: "read",
165
+ feature: "data"
166
+ },
167
+ "data.describeModel": {
168
+ mode: "read",
169
+ feature: "data"
170
+ },
171
+ "data.listRows": {
172
+ mode: "read",
173
+ feature: "data"
174
+ },
175
+ "data.readRow": {
176
+ mode: "read",
177
+ feature: "data"
178
+ },
179
+ "data.facets": {
180
+ mode: "read",
181
+ feature: "data"
182
+ },
183
+ "data.cascadePreview": {
184
+ mode: "read",
185
+ feature: "data"
186
+ },
187
+ "data.writeRow": {
188
+ mode: "write",
189
+ feature: "data",
190
+ gate: "dataEditable"
191
+ },
192
+ "data.deleteRows": {
193
+ mode: "write",
194
+ feature: "data",
195
+ gate: "dataEditable",
196
+ destructive: true
197
+ },
198
+ "data.clearTable": {
199
+ mode: "write",
200
+ feature: "data",
201
+ gate: "dataEditable",
202
+ destructive: true
203
+ },
204
+ "data.generateRows": {
205
+ mode: "write",
206
+ feature: "data",
207
+ gate: "dataEditable"
208
+ },
209
+ "timeTravel.capabilities": {
210
+ mode: "read",
211
+ feature: "timeTravel"
212
+ },
213
+ "timeTravel.currentMark": {
214
+ mode: "read",
215
+ feature: "timeTravel"
216
+ },
217
+ "timeTravel.markForTime": {
218
+ mode: "read",
219
+ feature: "timeTravel"
220
+ },
221
+ "timeTravel.listMarks": {
222
+ mode: "read",
223
+ feature: "timeTravel"
224
+ },
225
+ "timeTravel.preview": {
226
+ mode: "read",
227
+ feature: "timeTravel"
228
+ },
229
+ "timeTravel.armRestore": {
230
+ mode: "write",
231
+ feature: "timeTravel",
232
+ gate: "timeTravelRestore",
233
+ destructive: true
234
+ },
235
+ "timeTravel.undo": {
236
+ mode: "write",
237
+ feature: "timeTravel",
238
+ gate: "timeTravelRestore",
239
+ destructive: true
240
+ },
241
+ "timeTravel.createSnapshot": {
242
+ mode: "write",
243
+ feature: "timeTravel"
244
+ },
245
+ "timeTravel.prune": {
246
+ mode: "write",
247
+ feature: "timeTravel",
248
+ gate: "timeTravelRestore",
249
+ destructive: true
250
+ },
251
+ "transfer.export": {
252
+ mode: "read",
253
+ feature: "transfer"
254
+ },
255
+ "transfer.import": {
256
+ mode: "write",
257
+ feature: "transfer",
258
+ gate: "transferImport",
259
+ destructive: true
260
+ },
261
+ "auth.users": {
262
+ mode: "read",
263
+ feature: "auth"
264
+ },
265
+ "auth.userDetail": {
266
+ mode: "read",
267
+ feature: "auth"
268
+ },
269
+ "auth.sessions": {
270
+ mode: "read",
271
+ feature: "auth"
272
+ },
273
+ "auth.revokeSession": {
274
+ mode: "write",
275
+ feature: "auth",
276
+ gate: "opsEditable"
277
+ },
278
+ "auth.organizations": {
279
+ mode: "read",
280
+ feature: "authOrganizations"
281
+ },
282
+ "queue.list": {
283
+ mode: "read",
284
+ feature: "queue"
285
+ },
286
+ "queue.depths": {
287
+ mode: "read",
288
+ feature: "queue"
289
+ },
290
+ "queue.dlq": {
291
+ mode: "read",
292
+ feature: "queue"
293
+ },
294
+ "queue.send": {
295
+ mode: "write",
296
+ feature: "queue",
297
+ gate: "opsEditable"
298
+ },
299
+ "queue.replay": {
300
+ mode: "write",
301
+ feature: "queue",
302
+ gate: "opsEditable"
303
+ },
304
+ "schedule.jobs": {
305
+ mode: "read",
306
+ feature: "schedule"
307
+ },
308
+ "schedule.triggers": {
309
+ mode: "read",
310
+ feature: "schedule"
311
+ },
312
+ "schedule.runNow": {
313
+ mode: "write",
314
+ feature: "schedule",
315
+ gate: "opsEditable"
316
+ },
317
+ "flags.list": {
318
+ mode: "read",
319
+ feature: "flags"
320
+ },
321
+ "flags.evaluate": {
322
+ mode: "read",
323
+ feature: "flags"
324
+ },
325
+ "logs.tail": {
326
+ mode: "read",
327
+ feature: "logs"
328
+ },
329
+ "live.subscriptions": {
330
+ mode: "read",
331
+ feature: "live"
332
+ },
333
+ "presence.rooms": {
334
+ mode: "read",
335
+ feature: "presence"
336
+ },
337
+ "audit.tail": {
338
+ mode: "read",
339
+ feature: "audit"
340
+ }
341
+ };
342
+ /**
343
+ * Every op name as a runtime const array. Kept as a literal tuple (via
344
+ * `as const satisfies`) so the exhaustiveness drift guard can compare its
345
+ * element union against `keyof StudioRpcMap` in both directions.
346
+ */
347
+ const STUDIO_OPS = [
348
+ "studio.capabilities",
349
+ "app.routes",
350
+ "app.modules",
351
+ "app.entrypoints",
352
+ "app.openapi",
353
+ "api.authorizeTryIt",
354
+ "data.listModels",
355
+ "data.describeModel",
356
+ "data.listRows",
357
+ "data.readRow",
358
+ "data.facets",
359
+ "data.cascadePreview",
360
+ "data.writeRow",
361
+ "data.deleteRows",
362
+ "data.clearTable",
363
+ "data.generateRows",
364
+ "timeTravel.capabilities",
365
+ "timeTravel.currentMark",
366
+ "timeTravel.markForTime",
367
+ "timeTravel.listMarks",
368
+ "timeTravel.preview",
369
+ "timeTravel.armRestore",
370
+ "timeTravel.undo",
371
+ "timeTravel.createSnapshot",
372
+ "timeTravel.prune",
373
+ "transfer.export",
374
+ "transfer.import",
375
+ "auth.users",
376
+ "auth.userDetail",
377
+ "auth.sessions",
378
+ "auth.revokeSession",
379
+ "auth.organizations",
380
+ "queue.list",
381
+ "queue.depths",
382
+ "queue.dlq",
383
+ "queue.send",
384
+ "queue.replay",
385
+ "schedule.jobs",
386
+ "schedule.triggers",
387
+ "schedule.runNow",
388
+ "flags.list",
389
+ "flags.evaluate",
390
+ "logs.tail",
391
+ "live.subscriptions",
392
+ "presence.rooms",
393
+ "audit.tail"
394
+ ];
395
+ //#endregion
396
+ //#region src/http.ts
397
+ /**
398
+ * Transport constants shared by the server module, the UI, and the dev host.
399
+ * String literals + one integer version marker; zero runtime dependencies.
400
+ */
401
+ /** Default reserved prefix the admin surface mounts under. */
402
+ const STUDIO_DEFAULT_PATH = "/_vela/admin";
403
+ /** Unauthenticated health probe suffix (`GET {prefix}/health`). */
404
+ const STUDIO_HEALTH_SUFFIX = "/health";
405
+ /** RPC dispatch suffix (`POST {prefix}/rpc/:op`). */
406
+ const STUDIO_RPC_SUFFIX = "/rpc/";
407
+ /** Ephemeral WS sub-token mint suffix (`POST {prefix}/ws-token`). */
408
+ const STUDIO_WS_TOKEN_SUFFIX = "/ws-token";
409
+ /** Snapshot/transfer export suffix (`GET {prefix}/export`). */
410
+ const STUDIO_EXPORT_SUFFIX = "/export";
411
+ /** The header carrying the master bearer token (`Authorization: Bearer <token>`). */
412
+ const STUDIO_TOKEN_HEADER = "authorization";
413
+ /** The wire protocol version. Bumped only on a breaking envelope change. */
414
+ const STUDIO_PROTOCOL_VERSION = 2;
415
+ //#endregion
416
+ //#region src/responses.ts
417
+ const row = z.record(z.string(), z.unknown());
418
+ const strings = z.array(z.string());
419
+ const timeTravelCapabilities = z.object({
420
+ markByTime: z.boolean(),
421
+ list: z.boolean(),
422
+ undo: z.boolean(),
423
+ inPlace: z.boolean(),
424
+ restartRequired: z.boolean(),
425
+ portableExport: z.boolean(),
426
+ createOnDemand: z.boolean(),
427
+ granularity: z.enum([
428
+ "bookmark",
429
+ "snapshot",
430
+ "snapshot+cdc"
431
+ ]),
432
+ scopeNote: z.string()
433
+ });
434
+ const mark = z.object({
435
+ id: z.string(),
436
+ kind: z.enum(["bookmark", "snapshot"]),
437
+ time: z.number().optional(),
438
+ label: z.string().optional(),
439
+ schemaHash: z.string().optional(),
440
+ sizeBytes: z.number().optional(),
441
+ tables: strings.optional()
442
+ });
443
+ const restoreOutcome = z.object({
444
+ restoredTo: z.string(),
445
+ undoMark: mark.optional(),
446
+ applied: z.boolean(),
447
+ restartRequested: z.boolean()
448
+ });
449
+ const user = z.object({
450
+ id: z.string(),
451
+ email: z.string(),
452
+ name: z.string().optional(),
453
+ emailVerified: z.boolean(),
454
+ image: z.string().optional(),
455
+ role: z.string().optional(),
456
+ banned: z.boolean().optional(),
457
+ createdAt: z.number()
458
+ });
459
+ const session = z.object({
460
+ id: z.string(),
461
+ userId: z.string(),
462
+ createdAt: z.number(),
463
+ expiresAt: z.number(),
464
+ ipAddress: z.string().optional(),
465
+ userAgent: z.string().optional()
466
+ });
467
+ const organization = z.object({
468
+ id: z.string(),
469
+ name: z.string(),
470
+ slug: z.string().optional(),
471
+ memberCount: z.number().optional(),
472
+ createdAt: z.number()
473
+ });
474
+ const flagValue = z.union([
475
+ z.boolean(),
476
+ z.string(),
477
+ z.number(),
478
+ row,
479
+ z.array(z.unknown())
480
+ ]);
481
+ const deleted = z.object({ deleted: z.number() });
482
+ const acknowledged = z.object({ ok: z.literal(true) });
483
+ /** Every operation must supply a concrete validator for its declared output. */
484
+ const STUDIO_RESPONSE_PARSERS = {
485
+ "studio.capabilities": z.object({
486
+ operations: z.array(z.enum(STUDIO_OPS)),
487
+ features: z.record(z.enum(STUDIO_FEATURE_KEYS), z.boolean()),
488
+ writes: z.object({
489
+ dataEditable: z.boolean(),
490
+ schemaEditable: z.boolean(),
491
+ opsEditable: z.boolean(),
492
+ runAsIdentity: z.boolean(),
493
+ timeTravelRestore: z.boolean(),
494
+ transferImport: z.boolean()
495
+ }),
496
+ timeTravel: timeTravelCapabilities.nullable()
497
+ }).parse,
498
+ "app.routes": z.array(z.object({
499
+ method: z.string(),
500
+ path: z.string(),
501
+ handler: z.string(),
502
+ source: z.enum(["controller", "mounted"])
503
+ })).parse,
504
+ "app.modules": z.array(z.object({
505
+ moduleId: z.string(),
506
+ imports: strings,
507
+ isGlobal: z.boolean(),
508
+ lazy: z.boolean(),
509
+ providers: strings,
510
+ exports: strings
511
+ })).parse,
512
+ "app.entrypoints": z.array(z.object({
513
+ kind: z.string(),
514
+ target: z.string(),
515
+ meta: z.unknown().optional()
516
+ })).parse,
517
+ "app.openapi": z.unknown().parse,
518
+ "api.authorizeTryIt": z.object({ authorized: z.literal(true) }).parse,
519
+ "data.listModels": z.array(z.object({
520
+ name: z.string(),
521
+ table: z.string(),
522
+ label: z.string(),
523
+ capabilities: strings
524
+ })).parse,
525
+ "data.describeModel": z.object({
526
+ name: z.string(),
527
+ table: z.string(),
528
+ primaryKeys: strings,
529
+ columns: z.array(z.object({
530
+ name: z.string(),
531
+ type: z.enum([
532
+ "string",
533
+ "number",
534
+ "boolean",
535
+ "date",
536
+ "json",
537
+ "unknown"
538
+ ]),
539
+ pk: z.boolean(),
540
+ nullable: z.boolean(),
541
+ unique: z.boolean(),
542
+ managed: z.boolean(),
543
+ fk: z.object({
544
+ table: z.string(),
545
+ relation: z.string()
546
+ }).optional()
547
+ })),
548
+ relations: z.array(z.object({
549
+ name: z.string(),
550
+ type: z.enum([
551
+ "hasOne",
552
+ "hasMany",
553
+ "belongsTo"
554
+ ]),
555
+ target: z.string(),
556
+ foreignKey: z.string(),
557
+ cascade: z.string().optional()
558
+ })),
559
+ flags: z.object({
560
+ softDelete: z.boolean(),
561
+ multiTenant: z.boolean(),
562
+ versioning: z.boolean(),
563
+ audit: z.boolean()
564
+ }),
565
+ supports: z.object({
566
+ bulkWrites: z.boolean(),
567
+ facets: z.boolean(),
568
+ search: z.boolean(),
569
+ cascade: z.boolean()
570
+ })
571
+ }).parse,
572
+ "data.listRows": z.object({
573
+ rows: z.array(row),
574
+ info: z.object({
575
+ page: z.number(),
576
+ per_page: z.number(),
577
+ total_count: z.number().optional(),
578
+ total_pages: z.number().optional(),
579
+ has_next_page: z.boolean(),
580
+ has_prev_page: z.boolean(),
581
+ next_cursor: z.string().optional()
582
+ })
583
+ }).parse,
584
+ "data.readRow": row.nullable().parse,
585
+ "data.facets": z.object({ buckets: z.array(z.object({
586
+ value: z.unknown(),
587
+ count: z.number()
588
+ })) }).parse,
589
+ "data.cascadePreview": z.object({ relations: z.array(z.object({
590
+ relation: z.string(),
591
+ target: z.string(),
592
+ action: z.string(),
593
+ affected: z.number()
594
+ })) }).parse,
595
+ "data.writeRow": row.parse,
596
+ "data.deleteRows": deleted.parse,
597
+ "data.clearTable": deleted.parse,
598
+ "data.generateRows": z.object({ inserted: z.number() }).parse,
599
+ "timeTravel.capabilities": timeTravelCapabilities.parse,
600
+ "timeTravel.currentMark": mark.parse,
601
+ "timeTravel.markForTime": mark.nullable().parse,
602
+ "timeTravel.listMarks": z.object({
603
+ marks: z.array(mark),
604
+ nextCursor: z.string().optional()
605
+ }).parse,
606
+ "timeTravel.preview": z.object({
607
+ target: mark,
608
+ affectedTables: z.array(z.object({
609
+ table: z.string(),
610
+ approxRows: z.number().optional()
611
+ })),
612
+ schemaCompatible: z.boolean(),
613
+ incompatibleTables: strings,
614
+ undoAvailable: z.boolean(),
615
+ restartRequired: z.boolean(),
616
+ confirmToken: z.string(),
617
+ expiresAt: z.number()
618
+ }).parse,
619
+ "timeTravel.armRestore": restoreOutcome.parse,
620
+ "timeTravel.undo": restoreOutcome.parse,
621
+ "timeTravel.createSnapshot": mark.parse,
622
+ "timeTravel.prune": z.object({ pruned: z.number() }).parse,
623
+ "transfer.export": z.object({ exportUrl: z.string() }).parse,
624
+ "transfer.import": z.object({
625
+ imported: z.number(),
626
+ errors: z.array(z.object({
627
+ line: z.number(),
628
+ message: z.string()
629
+ }))
630
+ }).parse,
631
+ "auth.users": z.object({
632
+ rows: z.array(user),
633
+ nextCursor: z.string().optional()
634
+ }).parse,
635
+ "auth.userDetail": z.object({
636
+ user,
637
+ sessions: z.array(session),
638
+ organizations: z.array(organization)
639
+ }).parse,
640
+ "auth.sessions": z.array(session).parse,
641
+ "auth.revokeSession": acknowledged.parse,
642
+ "auth.organizations": z.array(organization).parse,
643
+ "queue.list": z.array(z.object({
644
+ name: z.string(),
645
+ kind: z.string(),
646
+ depth: z.number().optional()
647
+ })).parse,
648
+ "queue.depths": z.array(z.object({
649
+ name: z.string(),
650
+ depth: z.number(),
651
+ inFlight: z.number().optional()
652
+ })).parse,
653
+ "queue.dlq": z.array(z.object({
654
+ id: z.string(),
655
+ queue: z.string(),
656
+ failedAt: z.number(),
657
+ attempts: z.number(),
658
+ error: z.string().optional(),
659
+ payload: z.unknown().optional()
660
+ })).parse,
661
+ "queue.send": z.object({ id: z.string() }).parse,
662
+ "queue.replay": z.object({ replayed: z.number() }).parse,
663
+ "schedule.jobs": z.array(z.object({
664
+ name: z.string(),
665
+ kind: z.enum(["cron", "interval"]),
666
+ expression: z.string().optional(),
667
+ ms: z.number().optional(),
668
+ lastRun: z.number().optional(),
669
+ nextRun: z.number().optional()
670
+ })).parse,
671
+ "schedule.triggers": z.array(z.object({
672
+ name: z.string(),
673
+ cron: z.string(),
674
+ nextRun: z.number().optional()
675
+ })).parse,
676
+ "schedule.runNow": acknowledged.parse,
677
+ "flags.list": z.array(z.object({
678
+ key: z.string(),
679
+ value: flagValue
680
+ })).parse,
681
+ "flags.evaluate": z.object({
682
+ flagKey: z.string(),
683
+ value: flagValue,
684
+ reason: z.enum([
685
+ "STATIC",
686
+ "DEFAULT",
687
+ "ERROR"
688
+ ]),
689
+ errorMessage: z.string().optional()
690
+ }).parse,
691
+ "logs.tail": z.array(z.object({
692
+ ts: z.number(),
693
+ level: z.enum([
694
+ "debug",
695
+ "info",
696
+ "warn",
697
+ "error"
698
+ ]),
699
+ msg: z.string(),
700
+ source: z.string().optional(),
701
+ fields: row.optional()
702
+ })).parse,
703
+ "live.subscriptions": z.array(z.object({
704
+ id: z.string(),
705
+ room: z.string(),
706
+ tags: strings,
707
+ connectedAt: z.number(),
708
+ clientId: z.string().optional()
709
+ })).parse,
710
+ "presence.rooms": z.array(z.object({
711
+ room: z.string(),
712
+ count: z.number(),
713
+ members: strings.optional()
714
+ })).parse,
715
+ "audit.tail": z.array(z.object({
716
+ ts: z.number(),
717
+ op: z.string(),
718
+ mode: z.enum(["read", "write"]),
719
+ subject: z.string(),
720
+ status: z.number(),
721
+ ms: z.number(),
722
+ ip: z.string().nullable(),
723
+ detail: z.object({
724
+ target: z.string().optional(),
725
+ summary: z.string().optional(),
726
+ extra: row.optional()
727
+ }).optional()
728
+ })).parse
729
+ };
730
+ /** The result type comes only from the selected operation and its validator. */
731
+ function parseStudioResponse(op, value) {
732
+ return STUDIO_RESPONSE_PARSERS[op](value);
733
+ }
734
+ const errorStatus = z.number().int().min(400).max(599);
735
+ const envelope = z.discriminatedUnion("ok", [z.object({
736
+ ok: z.literal(true),
737
+ op: z.string(),
738
+ data: z.unknown(),
739
+ meta: z.object({
740
+ ms: z.number(),
741
+ op: z.string(),
742
+ mode: z.enum(["read", "write"])
743
+ })
744
+ }), z.object({
745
+ ok: z.literal(false),
746
+ op: z.string(),
747
+ status: errorStatus,
748
+ error: z.object({
749
+ code: z.string(),
750
+ title: z.string(),
751
+ status: errorStatus,
752
+ message: z.string(),
753
+ hint: z.string().optional(),
754
+ docsUrl: z.string().optional(),
755
+ details: z.unknown().optional()
756
+ })
757
+ })]);
758
+ /** Validate the envelope, operation identity, and operation-specific payload. */
759
+ function parseStudioRpcResponse(op, value) {
760
+ const parsed = envelope.parse(value);
761
+ if (parsed.op !== op) throw new Error("Studio response operation does not match the request.");
762
+ if (!parsed.ok) {
763
+ if (parsed.status !== parsed.error.status) throw new Error("Inconsistent Studio error status.");
764
+ return parsed;
765
+ }
766
+ if (parsed.meta.op !== op || parsed.meta.mode !== STUDIO_OP_META[op].mode) throw new Error("Studio response metadata does not match the request.");
767
+ return {
768
+ ...parsed,
769
+ data: parseStudioResponse(op, parsed.data)
770
+ };
771
+ }
772
+ //#endregion
773
+ export { STUDIO_DEFAULT_PATH, STUDIO_ERROR_CODES, STUDIO_EXPORT_SUFFIX, STUDIO_FEATURE_KEYS, STUDIO_FILTER_OPERATORS, STUDIO_HEALTH_SUFFIX, STUDIO_OPS, STUDIO_OP_META, STUDIO_PROTOCOL_VERSION, STUDIO_RESPONSE_PARSERS, STUDIO_RPC_SUFFIX, STUDIO_TOKEN_HEADER, STUDIO_WS_TOKEN_SUFFIX, isRecord, parseStudioConnection, parseStudioResponse, parseStudioRpcResponse, parseTryItRequest, parseTryItResponse };
774
+
775
+ //# sourceMappingURL=index.js.map