@driftgate/sdk 0.1.0-rc.1

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/src/index.ts ADDED
@@ -0,0 +1,1065 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { z } from "zod";
3
+ import {
4
+ CanonicalPolicyRefSchema,
5
+ CanonicalRiskMetaSchema,
6
+ CanonicalRouteRefSchema,
7
+ DataBoundaryDecisionSchema,
8
+ EdgeInterceptorRegistrationSchema,
9
+ EdgeInterceptorStatusSchema,
10
+ FirewallEventsResponseSchema,
11
+ FirewallInspectRequestSchema,
12
+ FirewallInspectResponseSchema,
13
+ FirewallInspectResultSchema,
14
+ RunStateSchema,
15
+ V4ExecutionRequestSchema,
16
+ V4SessionResourceSchema,
17
+ V4SessionStartRequestSchema,
18
+ WorkflowVersionSchema,
19
+ type CanonicalPolicyRef,
20
+ type CanonicalRiskMeta,
21
+ type CanonicalRouteRef,
22
+ type EdgeInterceptorRegisterRequest,
23
+ type EdgeInterceptorRegistration,
24
+ type EdgeInterceptorStatus,
25
+ type FirewallEvent,
26
+ type FirewallInspectRequest,
27
+ type FirewallInspectResponse,
28
+ type PolicyDecision,
29
+ type WorkflowVersion
30
+ } from "@driftgate/contracts";
31
+
32
+ const HeadlessErrorEnvelopeSchema = z.object({
33
+ code: z.string(),
34
+ message: z.string(),
35
+ correlation_id: z.string().optional(),
36
+ details: z.unknown().optional()
37
+ });
38
+
39
+ const LegacyErrorEnvelopeSchema = z.object({
40
+ error: z.string(),
41
+ message: z.string(),
42
+ issues: z.unknown().optional()
43
+ });
44
+
45
+ const CanonicalTimingMsSchema = z.object({
46
+ total: z.number(),
47
+ policy: z.number().optional(),
48
+ route: z.number().optional(),
49
+ tool: z.number().optional()
50
+ });
51
+
52
+ const CanonicalMetaSchema = z.object({
53
+ requestId: z.string(),
54
+ sessionId: z.string().optional(),
55
+ executionId: z.string().optional(),
56
+ lineageId: z.string().optional(),
57
+ policy: CanonicalPolicyRefSchema.optional(),
58
+ route: CanonicalRouteRefSchema.optional(),
59
+ risk: CanonicalRiskMetaSchema.optional(),
60
+ timingMs: CanonicalTimingMsSchema
61
+ });
62
+
63
+ const CanonicalErrorSchema = z.object({
64
+ code: z.string(),
65
+ message: z.string(),
66
+ status: z.number(),
67
+ retryable: z.boolean(),
68
+ details: z.record(z.unknown()).optional()
69
+ });
70
+
71
+ const REQUIRED_V4_ERROR_CODES = [
72
+ "AUTH_INVALID",
73
+ "POLICY_DENIED",
74
+ "RISK_EXCEEDED",
75
+ "ROUTE_UNAVAILABLE",
76
+ "TOOL_BLOCKED",
77
+ "RATE_LIMITED",
78
+ "TIMEOUT",
79
+ "INTERNAL"
80
+ ] as const;
81
+
82
+ const RunRecordSchema = z.object({
83
+ id: z.string(),
84
+ workspaceId: z.string(),
85
+ workflowVersionId: z.string(),
86
+ state: RunStateSchema,
87
+ correlationId: z.string(),
88
+ idempotencyKey: z.string().nullable().optional(),
89
+ triggerSource: z.enum(["ui", "api", "sdk", "cli", "hosted", "webhook"]),
90
+ requestedBy: z.string(),
91
+ requestedAt: z.string(),
92
+ startedAt: z.string().nullable().optional(),
93
+ completedAt: z.string().nullable().optional(),
94
+ slaPolicyId: z.string().nullable().optional(),
95
+ slaDueAt: z.string().nullable().optional(),
96
+ slaViolatedAt: z.string().nullable().optional()
97
+ });
98
+
99
+ const ApprovalSchema = z.object({
100
+ id: z.string(),
101
+ runId: z.string(),
102
+ requiredRole: z.string(),
103
+ status: z.enum(["pending", "approved", "denied"]),
104
+ createdAt: z.string(),
105
+ decidedAt: z.string().nullable().optional(),
106
+ decidedBy: z.string().nullable().optional()
107
+ });
108
+
109
+ const RunResponseSchema = z.object({
110
+ run: RunRecordSchema,
111
+ approval: ApprovalSchema.nullable().optional(),
112
+ blocked: z.boolean().optional(),
113
+ policyDecisions: z
114
+ .array(
115
+ z.object({
116
+ mode: z.enum(["monitor", "enforce"]),
117
+ decision: z.enum(["allow", "deny"]),
118
+ policyId: z.string(),
119
+ ruleId: z.string(),
120
+ reasonCode: z.string(),
121
+ reasonText: z.string(),
122
+ correlationId: z.string(),
123
+ trace: z.record(z.unknown())
124
+ })
125
+ )
126
+ .optional(),
127
+ entitlementDecision: z
128
+ .object({
129
+ id: z.string(),
130
+ reasonCode: z.string(),
131
+ reasonText: z.string(),
132
+ entitled: z.boolean()
133
+ })
134
+ .optional(),
135
+ usageEntry: z
136
+ .object({
137
+ id: z.string(),
138
+ quantity: z.number()
139
+ })
140
+ .optional(),
141
+ boundaryDecision: DataBoundaryDecisionSchema.nullable().optional(),
142
+ firewallDecision: FirewallInspectResultSchema.nullable().optional()
143
+ });
144
+
145
+ const V4SessionStartDataSchema = z.object({
146
+ session: V4SessionResourceSchema
147
+ });
148
+
149
+ const CanonicalEnvelopeSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
150
+ z.object({
151
+ ok: z.boolean(),
152
+ data: dataSchema.nullable(),
153
+ meta: CanonicalMetaSchema,
154
+ error: CanonicalErrorSchema.nullable()
155
+ });
156
+
157
+ const V4SessionStartResponseSchema = CanonicalEnvelopeSchema(V4SessionStartDataSchema);
158
+ const V4ExecutionResponseSchema = CanonicalEnvelopeSchema(RunResponseSchema);
159
+ const V4EphemeralExecuteRequestBodySchema = V4SessionStartRequestSchema.extend({
160
+ input: z.record(z.unknown())
161
+ });
162
+ const V4EphemeralExecuteDataSchema = z.object({
163
+ session: V4SessionResourceSchema,
164
+ execution: RunResponseSchema
165
+ });
166
+ const V4EphemeralExecutionResponseSchema = CanonicalEnvelopeSchema(V4EphemeralExecuteDataSchema);
167
+ const CanonicalErrorEnvelopeSchema = z.object({
168
+ ok: z.literal(false),
169
+ data: z.null(),
170
+ meta: CanonicalMetaSchema,
171
+ error: CanonicalErrorSchema
172
+ });
173
+
174
+ const RunEventsResponseSchema = z.object({
175
+ events: z.array(
176
+ z.object({
177
+ id: z.string(),
178
+ runId: z.string(),
179
+ type: z.string(),
180
+ payload: z.record(z.unknown()),
181
+ createdAt: z.string()
182
+ })
183
+ )
184
+ });
185
+
186
+ const ApprovalsListSchema = z.object({
187
+ approvals: z.array(
188
+ z.object({
189
+ approval: ApprovalSchema,
190
+ run: RunRecordSchema
191
+ })
192
+ )
193
+ });
194
+
195
+ const DeployResponseSchema = z.object({
196
+ project: z.object({
197
+ id: z.string(),
198
+ workspaceId: z.string(),
199
+ name: z.string(),
200
+ createdBy: z.string(),
201
+ createdAt: z.string(),
202
+ updatedAt: z.string()
203
+ }),
204
+ workflow: z.object({
205
+ id: z.string(),
206
+ projectId: z.string(),
207
+ workspaceId: z.string(),
208
+ name: z.string(),
209
+ status: z.enum(["draft", "published", "archived"]),
210
+ createdBy: z.string(),
211
+ createdAt: z.string(),
212
+ updatedAt: z.string()
213
+ }),
214
+ draft: z.object({
215
+ workflowId: z.string(),
216
+ workspaceId: z.string(),
217
+ version: z.number(),
218
+ nodes: z.array(z.unknown()),
219
+ edges: z.array(z.unknown()),
220
+ viewport: z.object({ x: z.number(), y: z.number(), zoom: z.number() }),
221
+ updatedAt: z.string()
222
+ }),
223
+ compile: z.object({
224
+ checksum: z.string(),
225
+ mutationNodeIds: z.array(z.string()),
226
+ compiledPlan: z.record(z.unknown())
227
+ })
228
+ });
229
+
230
+ const PublishResponseSchema = z.object({
231
+ version: WorkflowVersionSchema
232
+ });
233
+
234
+ const ConnectorRecordSchema = z.object({
235
+ id: z.string(),
236
+ workspaceId: z.string(),
237
+ name: z.string(),
238
+ connectorType: z.string(),
239
+ status: z.enum(["active", "disabled"]),
240
+ config: z.record(z.unknown()),
241
+ createdBy: z.string(),
242
+ createdAt: z.string(),
243
+ updatedAt: z.string()
244
+ });
245
+
246
+ const WorkspaceSecretRecordSchema = z.object({
247
+ id: z.string(),
248
+ workspaceId: z.string(),
249
+ connectorId: z.string().nullable(),
250
+ name: z.string(),
251
+ keyVersion: z.string(),
252
+ metadata: z.record(z.unknown()),
253
+ createdBy: z.string(),
254
+ createdAt: z.string(),
255
+ rotatedAt: z.string().nullable(),
256
+ revokedAt: z.string().nullable(),
257
+ maskedValue: z.string()
258
+ });
259
+
260
+ const WorkspaceWebhookRecordSchema = z.object({
261
+ id: z.string(),
262
+ workspaceId: z.string(),
263
+ connectorId: z.string().nullable(),
264
+ name: z.string(),
265
+ path: z.string(),
266
+ targetWorkflowId: z.string(),
267
+ status: z.enum(["active", "disabled"]),
268
+ eventFilter: z.record(z.unknown()),
269
+ createdBy: z.string(),
270
+ createdAt: z.string(),
271
+ updatedAt: z.string(),
272
+ lastReceivedAt: z.string().nullable(),
273
+ revokedAt: z.string().nullable(),
274
+ signingSecretConfigured: z.boolean()
275
+ });
276
+
277
+ const ConnectorListSchema = z.object({
278
+ connectors: z.array(ConnectorRecordSchema)
279
+ });
280
+
281
+ const ConnectorMutationSchema = z.object({
282
+ connector: ConnectorRecordSchema
283
+ });
284
+
285
+ const SecretListSchema = z.object({
286
+ secrets: z.array(WorkspaceSecretRecordSchema)
287
+ });
288
+
289
+ const SecretMutationSchema = z.object({
290
+ secret: WorkspaceSecretRecordSchema
291
+ });
292
+
293
+ const WebhookListSchema = z.object({
294
+ webhooks: z.array(WorkspaceWebhookRecordSchema)
295
+ });
296
+
297
+ const WebhookMutationSchema = z.object({
298
+ webhook: WorkspaceWebhookRecordSchema
299
+ });
300
+
301
+ const EdgeInterceptorListSchema = z.object({
302
+ registrations: z.array(EdgeInterceptorRegistrationSchema)
303
+ });
304
+
305
+ const EdgeInterceptorMutationSchema = z.object({
306
+ registration: EdgeInterceptorRegistrationSchema
307
+ });
308
+
309
+ const FirewallInspectBodySchema = FirewallInspectRequestSchema.omit({
310
+ workspaceId: true
311
+ });
312
+
313
+ export class DriftGateError extends Error {
314
+ constructor(
315
+ public readonly code: string,
316
+ message: string,
317
+ public readonly status: number,
318
+ public readonly correlationId?: string,
319
+ public readonly details?: unknown
320
+ ) {
321
+ super(message);
322
+ this.name = "DriftGateError";
323
+ }
324
+ }
325
+
326
+ export type DriftGateClientOptions = {
327
+ baseUrl: string;
328
+ sessionToken?: string;
329
+ apiKey?: string;
330
+ fetchImpl?: typeof fetch;
331
+ };
332
+
333
+ export type DriftGateRunInput = {
334
+ workspaceId: string;
335
+ workflowVersionId: string;
336
+ requiresApproval?: boolean;
337
+ requiredRole?: string;
338
+ slaPolicyId?: string;
339
+ idempotencyKey?: string;
340
+ correlationId?: string;
341
+ triggerSource?: "ui" | "api" | "sdk" | "cli" | "hosted" | "webhook";
342
+ input?: Record<string, unknown>;
343
+ };
344
+
345
+ export type DriftGateCanonicalMeta = z.infer<typeof CanonicalMetaSchema>;
346
+ export type DriftGateCanonicalError = z.infer<typeof CanonicalErrorSchema>;
347
+ export type DriftGateCanonicalErrorCode = (typeof REQUIRED_V4_ERROR_CODES)[number];
348
+ export type DriftGateCanonicalResponse<T> = {
349
+ ok: boolean;
350
+ data: T | null;
351
+ meta: DriftGateCanonicalMeta;
352
+ error: DriftGateCanonicalError | null;
353
+ raw: unknown;
354
+ };
355
+
356
+ export type DriftGateSessionStartInput = z.input<typeof V4SessionStartRequestSchema>;
357
+ export type DriftGateSessionExecuteInput = z.input<typeof V4ExecutionRequestSchema>;
358
+ export type DriftGateEphemeralExecuteInput = z.input<typeof V4SessionStartRequestSchema> & {
359
+ input: Record<string, unknown>;
360
+ };
361
+ export type DriftGateSessionStartEnvelope = z.infer<typeof V4SessionStartResponseSchema>;
362
+ export type DriftGateExecutionEnvelope = z.infer<typeof V4ExecutionResponseSchema>;
363
+ export type DriftGateEphemeralExecutionEnvelope = z.infer<typeof V4EphemeralExecutionResponseSchema>;
364
+
365
+ export type WaitForTerminalOptions = {
366
+ intervalMs?: number;
367
+ timeoutMs?: number;
368
+ };
369
+
370
+ export type DeployWorkflowInput = {
371
+ workspaceId: string;
372
+ workflowYaml: string;
373
+ projectId?: string;
374
+ projectName?: string;
375
+ workflowId?: string;
376
+ workflowName?: string;
377
+ };
378
+
379
+ export type ConnectorCreateInput = {
380
+ name: string;
381
+ connectorType: string;
382
+ status?: "active" | "disabled";
383
+ config?: Record<string, unknown>;
384
+ };
385
+
386
+ export type ConnectorUpdateInput = {
387
+ name?: string;
388
+ connectorType?: string;
389
+ status?: "active" | "disabled";
390
+ config?: Record<string, unknown>;
391
+ };
392
+
393
+ export type SecretCreateInput = {
394
+ connectorId?: string | null;
395
+ name: string;
396
+ value: string;
397
+ keyVersion?: string;
398
+ metadata?: Record<string, unknown>;
399
+ };
400
+
401
+ export type SecretUpdateInput = {
402
+ connectorId?: string | null;
403
+ name?: string;
404
+ value?: string;
405
+ keyVersion?: string;
406
+ metadata?: Record<string, unknown>;
407
+ };
408
+
409
+ export type WebhookExecutionOptions = {
410
+ requiresApproval?: boolean;
411
+ requiredRole?: string;
412
+ slaPolicyId?: string;
413
+ };
414
+
415
+ export type WebhookCreateInput = {
416
+ connectorId?: string | null;
417
+ name: string;
418
+ path: string;
419
+ targetWorkflowId: string;
420
+ status?: "active" | "disabled";
421
+ eventFilter?: Record<string, unknown>;
422
+ execution?: WebhookExecutionOptions;
423
+ signingSecret: string;
424
+ };
425
+
426
+ export type WebhookUpdateInput = {
427
+ connectorId?: string | null;
428
+ name?: string;
429
+ path?: string;
430
+ targetWorkflowId?: string;
431
+ status?: "active" | "disabled";
432
+ eventFilter?: Record<string, unknown>;
433
+ execution?: WebhookExecutionOptions;
434
+ signingSecret?: string;
435
+ };
436
+
437
+ export type EdgeInterceptorRegisterInput = Omit<EdgeInterceptorRegisterRequest, "workspaceId">;
438
+ export type FirewallInspectInput = Omit<FirewallInspectRequest, "workspaceId">;
439
+
440
+ export type EdgeInterceptorEnforcementMode = "monitor" | "enforce";
441
+
442
+ export type EdgeInterceptorDecision = {
443
+ allowed: boolean;
444
+ reasonCode: string;
445
+ reasonText: string;
446
+ requiredCapabilities: string[];
447
+ grantedCapabilities: string[];
448
+ };
449
+
450
+ export type EdgeInterceptorHooks = {
451
+ beforeRun?: (input: DriftGateRunInput, decision: EdgeInterceptorDecision) => void | Promise<void>;
452
+ onBlocked?: (input: DriftGateRunInput, decision: EdgeInterceptorDecision) => void | Promise<void>;
453
+ afterRun?: (
454
+ input: DriftGateRunInput,
455
+ decision: EdgeInterceptorDecision,
456
+ response: z.infer<typeof RunResponseSchema>
457
+ ) => void | Promise<void>;
458
+ };
459
+
460
+ export type EdgeInterceptorEnableInput = {
461
+ workspaceId: string;
462
+ registration: EdgeInterceptorRegisterInput;
463
+ enforcement?: EdgeInterceptorEnforcementMode;
464
+ hooks?: EdgeInterceptorHooks;
465
+ };
466
+
467
+ export class DriftGateSessionHandle {
468
+ constructor(
469
+ private readonly client: DriftGateClient,
470
+ public readonly session: z.infer<typeof V4SessionResourceSchema>,
471
+ public readonly startEnvelope: DriftGateCanonicalResponse<z.infer<typeof V4SessionStartDataSchema>>
472
+ ) {}
473
+
474
+ get sessionId(): string {
475
+ return this.session.sessionId;
476
+ }
477
+
478
+ get rawEnvelope(): unknown {
479
+ return this.startEnvelope.raw;
480
+ }
481
+
482
+ async execute(
483
+ input: DriftGateSessionExecuteInput
484
+ ): Promise<DriftGateCanonicalResponse<z.infer<typeof RunResponseSchema>>> {
485
+ return this.client.executeSession(this.session.sessionId, input);
486
+ }
487
+ }
488
+
489
+ function isTerminalState(state: z.infer<typeof RunStateSchema>): boolean {
490
+ return ["succeeded", "failed", "denied", "timed_out", "canceled", "aborted"].includes(state);
491
+ }
492
+
493
+ export class DriftGateClient {
494
+ private readonly baseUrl: string;
495
+ private readonly sessionToken?: string;
496
+ private readonly apiKey?: string;
497
+ private readonly fetchImpl: typeof fetch;
498
+ private edgeInterceptorState: {
499
+ workspaceId: string;
500
+ registration: EdgeInterceptorRegistration;
501
+ enforcement: EdgeInterceptorEnforcementMode;
502
+ hooks?: EdgeInterceptorHooks;
503
+ } | null = null;
504
+ readonly session: {
505
+ start: (input: DriftGateSessionStartInput) => Promise<DriftGateSessionHandle>;
506
+ };
507
+
508
+ readonly approvals: {
509
+ list: (workspaceId: string, status?: "pending" | "approved" | "denied") => Promise<z.infer<typeof ApprovalsListSchema>["approvals"]>;
510
+ approve: (approvalId: string) => Promise<z.infer<typeof RunResponseSchema>>;
511
+ deny: (approvalId: string) => Promise<z.infer<typeof RunResponseSchema>>;
512
+ };
513
+ readonly connectors: {
514
+ list: (workspaceId: string) => Promise<z.infer<typeof ConnectorRecordSchema>[]>;
515
+ create: (workspaceId: string, input: ConnectorCreateInput) => Promise<z.infer<typeof ConnectorRecordSchema>>;
516
+ update: (
517
+ workspaceId: string,
518
+ connectorId: string,
519
+ input: ConnectorUpdateInput
520
+ ) => Promise<z.infer<typeof ConnectorRecordSchema>>;
521
+ delete: (workspaceId: string, connectorId: string) => Promise<z.infer<typeof ConnectorRecordSchema>>;
522
+ };
523
+ readonly secrets: {
524
+ list: (workspaceId: string) => Promise<z.infer<typeof WorkspaceSecretRecordSchema>[]>;
525
+ create: (workspaceId: string, input: SecretCreateInput) => Promise<z.infer<typeof WorkspaceSecretRecordSchema>>;
526
+ update: (
527
+ workspaceId: string,
528
+ secretId: string,
529
+ input: SecretUpdateInput
530
+ ) => Promise<z.infer<typeof WorkspaceSecretRecordSchema>>;
531
+ delete: (workspaceId: string, secretId: string) => Promise<z.infer<typeof WorkspaceSecretRecordSchema>>;
532
+ };
533
+ readonly webhooks: {
534
+ list: (workspaceId: string) => Promise<z.infer<typeof WorkspaceWebhookRecordSchema>[]>;
535
+ create: (workspaceId: string, input: WebhookCreateInput) => Promise<z.infer<typeof WorkspaceWebhookRecordSchema>>;
536
+ update: (
537
+ workspaceId: string,
538
+ webhookId: string,
539
+ input: WebhookUpdateInput
540
+ ) => Promise<z.infer<typeof WorkspaceWebhookRecordSchema>>;
541
+ delete: (workspaceId: string, webhookId: string) => Promise<z.infer<typeof WorkspaceWebhookRecordSchema>>;
542
+ };
543
+ readonly edgeInterceptors: {
544
+ list: (workspaceId: string) => Promise<EdgeInterceptorRegistration[]>;
545
+ register: (workspaceId: string, input: EdgeInterceptorRegisterInput) => Promise<EdgeInterceptorRegistration>;
546
+ setStatus: (
547
+ workspaceId: string,
548
+ registrationId: string,
549
+ status: EdgeInterceptorStatus
550
+ ) => Promise<EdgeInterceptorRegistration>;
551
+ };
552
+ readonly firewall: {
553
+ inspect: (workspaceId: string, input: FirewallInspectInput) => Promise<FirewallInspectResponse>;
554
+ events: (workspaceId: string) => Promise<FirewallEvent[]>;
555
+ };
556
+
557
+ constructor(options: DriftGateClientOptions) {
558
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
559
+ this.sessionToken = options.sessionToken;
560
+ this.apiKey = options.apiKey;
561
+ this.fetchImpl = options.fetchImpl ?? fetch;
562
+ this.session = {
563
+ start: async (input) => {
564
+ const payload = V4SessionStartRequestSchema.parse(input);
565
+ const raw = await this.request("/v4/sessions.start", {
566
+ method: "POST",
567
+ body: JSON.stringify(payload)
568
+ });
569
+ const parsed = V4SessionStartResponseSchema.parse(raw);
570
+ if (!parsed.ok || !parsed.data) {
571
+ const canonicalCode = parsed.error?.code ?? "INTERNAL";
572
+ throw new DriftGateError(
573
+ canonicalCode,
574
+ parsed.error?.message ?? "session.start failed",
575
+ parsed.error?.status ?? 500,
576
+ parsed.meta.requestId,
577
+ parsed.error?.details
578
+ );
579
+ }
580
+ const envelope: DriftGateCanonicalResponse<z.infer<typeof V4SessionStartDataSchema>> = {
581
+ ok: parsed.ok,
582
+ data: parsed.data,
583
+ meta: parsed.meta,
584
+ error: parsed.error,
585
+ raw
586
+ };
587
+ return new DriftGateSessionHandle(this, parsed.data.session, envelope);
588
+ }
589
+ };
590
+
591
+ this.approvals = {
592
+ list: async (workspaceId, status) => {
593
+ const query = status ? `?status=${encodeURIComponent(status)}` : "";
594
+ const response = await this.request(`/v1/headless/workspaces/${encodeURIComponent(workspaceId)}/approvals${query}`);
595
+ return ApprovalsListSchema.parse(response).approvals;
596
+ },
597
+ approve: async (approvalId) => {
598
+ const response = await this.request(`/v1/headless/approvals/${encodeURIComponent(approvalId)}/approve`, {
599
+ method: "POST"
600
+ });
601
+ return RunResponseSchema.parse(response);
602
+ },
603
+ deny: async (approvalId) => {
604
+ const response = await this.request(`/v1/headless/approvals/${encodeURIComponent(approvalId)}/deny`, {
605
+ method: "POST"
606
+ });
607
+ return RunResponseSchema.parse(response);
608
+ }
609
+ };
610
+
611
+ this.connectors = {
612
+ list: async (workspaceId) => {
613
+ const response = await this.request(
614
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/connectors`
615
+ );
616
+ return ConnectorListSchema.parse(response).connectors;
617
+ },
618
+ create: async (workspaceId, input) => {
619
+ const response = await this.request(
620
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/connectors`,
621
+ {
622
+ method: "POST",
623
+ body: JSON.stringify(input)
624
+ }
625
+ );
626
+ return ConnectorMutationSchema.parse(response).connector;
627
+ },
628
+ update: async (workspaceId, connectorId, input) => {
629
+ const response = await this.request(
630
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/connectors/${encodeURIComponent(
631
+ connectorId
632
+ )}`,
633
+ {
634
+ method: "PATCH",
635
+ body: JSON.stringify(input)
636
+ }
637
+ );
638
+ return ConnectorMutationSchema.parse(response).connector;
639
+ },
640
+ delete: async (workspaceId, connectorId) => {
641
+ const response = await this.request(
642
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/connectors/${encodeURIComponent(
643
+ connectorId
644
+ )}`,
645
+ {
646
+ method: "DELETE"
647
+ }
648
+ );
649
+ return ConnectorMutationSchema.parse(response).connector;
650
+ }
651
+ };
652
+
653
+ this.secrets = {
654
+ list: async (workspaceId) => {
655
+ const response = await this.request(
656
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets`
657
+ );
658
+ return SecretListSchema.parse(response).secrets;
659
+ },
660
+ create: async (workspaceId, input) => {
661
+ const response = await this.request(
662
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets`,
663
+ {
664
+ method: "POST",
665
+ body: JSON.stringify(input)
666
+ }
667
+ );
668
+ return SecretMutationSchema.parse(response).secret;
669
+ },
670
+ update: async (workspaceId, secretId, input) => {
671
+ const response = await this.request(
672
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets/${encodeURIComponent(secretId)}`,
673
+ {
674
+ method: "PATCH",
675
+ body: JSON.stringify(input)
676
+ }
677
+ );
678
+ return SecretMutationSchema.parse(response).secret;
679
+ },
680
+ delete: async (workspaceId, secretId) => {
681
+ const response = await this.request(
682
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/secrets/${encodeURIComponent(secretId)}`,
683
+ {
684
+ method: "DELETE"
685
+ }
686
+ );
687
+ return SecretMutationSchema.parse(response).secret;
688
+ }
689
+ };
690
+
691
+ this.webhooks = {
692
+ list: async (workspaceId) => {
693
+ const response = await this.request(
694
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks`
695
+ );
696
+ return WebhookListSchema.parse(response).webhooks;
697
+ },
698
+ create: async (workspaceId, input) => {
699
+ const response = await this.request(
700
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks`,
701
+ {
702
+ method: "POST",
703
+ body: JSON.stringify(input)
704
+ }
705
+ );
706
+ return WebhookMutationSchema.parse(response).webhook;
707
+ },
708
+ update: async (workspaceId, webhookId, input) => {
709
+ const response = await this.request(
710
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(
711
+ webhookId
712
+ )}`,
713
+ {
714
+ method: "PATCH",
715
+ body: JSON.stringify(input)
716
+ }
717
+ );
718
+ return WebhookMutationSchema.parse(response).webhook;
719
+ },
720
+ delete: async (workspaceId, webhookId) => {
721
+ const response = await this.request(
722
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/webhooks/${encodeURIComponent(
723
+ webhookId
724
+ )}`,
725
+ {
726
+ method: "DELETE"
727
+ }
728
+ );
729
+ return WebhookMutationSchema.parse(response).webhook;
730
+ }
731
+ };
732
+
733
+ this.edgeInterceptors = {
734
+ list: async (workspaceId) => {
735
+ const response = await this.request(
736
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/edge/interceptors`
737
+ );
738
+ return EdgeInterceptorListSchema.parse(response).registrations;
739
+ },
740
+ register: async (workspaceId, input) => {
741
+ const response = await this.request(
742
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/edge/interceptors`,
743
+ {
744
+ method: "POST",
745
+ body: JSON.stringify({
746
+ ...input,
747
+ workspaceId
748
+ })
749
+ }
750
+ );
751
+ return EdgeInterceptorMutationSchema.parse(response).registration;
752
+ },
753
+ setStatus: async (workspaceId, registrationId, status) => {
754
+ const parsedStatus = EdgeInterceptorStatusSchema.parse(status);
755
+ const response = await this.request(
756
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/edge/interceptors/${encodeURIComponent(
757
+ registrationId
758
+ )}/status`,
759
+ {
760
+ method: "PATCH",
761
+ body: JSON.stringify({ status: parsedStatus })
762
+ }
763
+ );
764
+ return EdgeInterceptorMutationSchema.parse(response).registration;
765
+ }
766
+ };
767
+
768
+ this.firewall = {
769
+ inspect: async (workspaceId, input) => {
770
+ const parsedInput = FirewallInspectBodySchema.parse(input);
771
+ const response = await this.request(
772
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/firewall/inspect`,
773
+ {
774
+ method: "POST",
775
+ body: JSON.stringify(parsedInput)
776
+ }
777
+ );
778
+ return FirewallInspectResponseSchema.parse(response);
779
+ },
780
+ events: async (workspaceId) => {
781
+ const response = await this.request(
782
+ `/v1/workspaces/${encodeURIComponent(workspaceId)}/firewall/events`
783
+ );
784
+ return FirewallEventsResponseSchema.parse(response).events;
785
+ }
786
+ };
787
+ }
788
+
789
+ async executeSession(
790
+ sessionId: string,
791
+ input: DriftGateSessionExecuteInput
792
+ ): Promise<DriftGateCanonicalResponse<z.infer<typeof RunResponseSchema>>> {
793
+ const payload = V4ExecutionRequestSchema.parse(input);
794
+ const raw = await this.request(`/v4/sessions/${encodeURIComponent(sessionId)}/executions.execute`, {
795
+ method: "POST",
796
+ body: JSON.stringify(payload)
797
+ });
798
+ const parsed = V4ExecutionResponseSchema.parse(raw);
799
+ if (!parsed.ok || !parsed.data) {
800
+ const canonicalCode = parsed.error?.code ?? "INTERNAL";
801
+ throw new DriftGateError(
802
+ canonicalCode,
803
+ parsed.error?.message ?? "session.execute failed",
804
+ parsed.error?.status ?? 500,
805
+ parsed.meta.requestId,
806
+ parsed.error?.details
807
+ );
808
+ }
809
+ return {
810
+ ok: parsed.ok,
811
+ data: parsed.data,
812
+ meta: parsed.meta,
813
+ error: parsed.error,
814
+ raw
815
+ };
816
+ }
817
+
818
+ async execute(
819
+ input: DriftGateEphemeralExecuteInput
820
+ ): Promise<DriftGateCanonicalResponse<z.infer<typeof V4EphemeralExecuteDataSchema>>> {
821
+ const payload = V4EphemeralExecuteRequestBodySchema.parse(input);
822
+ const raw = await this.request("/v4/execute", {
823
+ method: "POST",
824
+ body: JSON.stringify(payload)
825
+ });
826
+ const parsed = V4EphemeralExecutionResponseSchema.parse(raw);
827
+ if (!parsed.ok || !parsed.data) {
828
+ const canonicalCode = parsed.error?.code ?? "INTERNAL";
829
+ throw new DriftGateError(
830
+ canonicalCode,
831
+ parsed.error?.message ?? "execute failed",
832
+ parsed.error?.status ?? 500,
833
+ parsed.meta.requestId,
834
+ parsed.error?.details
835
+ );
836
+ }
837
+ return {
838
+ ok: parsed.ok,
839
+ data: parsed.data,
840
+ meta: parsed.meta,
841
+ error: parsed.error,
842
+ raw
843
+ };
844
+ }
845
+
846
+ async enableEdgeMode(input: EdgeInterceptorEnableInput): Promise<EdgeInterceptorRegistration> {
847
+ const registration = await this.edgeInterceptors.register(input.workspaceId, input.registration);
848
+ this.edgeInterceptorState = {
849
+ workspaceId: input.workspaceId,
850
+ registration,
851
+ enforcement: input.enforcement ?? "monitor",
852
+ hooks: input.hooks
853
+ };
854
+ return registration;
855
+ }
856
+
857
+ disableEdgeMode(): void {
858
+ this.edgeInterceptorState = null;
859
+ }
860
+
861
+ async run(input: DriftGateRunInput): Promise<z.infer<typeof RunResponseSchema>> {
862
+ const edgeDecision = this.evaluateEdgeDecision(input);
863
+ const edgeState = this.edgeInterceptorState;
864
+ if (edgeDecision && edgeState?.hooks?.beforeRun) {
865
+ await edgeState.hooks.beforeRun(input, edgeDecision);
866
+ }
867
+ if (edgeDecision && !edgeDecision.allowed) {
868
+ if (edgeState?.hooks?.onBlocked) {
869
+ await edgeState.hooks.onBlocked(input, edgeDecision);
870
+ }
871
+ if (edgeState?.enforcement === "enforce") {
872
+ throw new DriftGateError(
873
+ "edge_interceptor_denied",
874
+ edgeDecision.reasonText,
875
+ 403,
876
+ undefined,
877
+ {
878
+ reasonCode: edgeDecision.reasonCode,
879
+ requiredCapabilities: edgeDecision.requiredCapabilities,
880
+ grantedCapabilities: edgeDecision.grantedCapabilities,
881
+ registrationId: edgeState.registration.registrationId
882
+ }
883
+ );
884
+ }
885
+ }
886
+
887
+ const response = await this.request("/v1/headless/runs", {
888
+ method: "POST",
889
+ body: JSON.stringify(input)
890
+ });
891
+ const parsed = RunResponseSchema.parse(response);
892
+ if (edgeDecision && edgeState?.hooks?.afterRun) {
893
+ await edgeState.hooks.afterRun(input, edgeDecision, parsed);
894
+ }
895
+ return parsed;
896
+ }
897
+
898
+ async status(runId: string): Promise<z.infer<typeof RunResponseSchema>> {
899
+ const response = await this.request(`/v1/headless/runs/${encodeURIComponent(runId)}`);
900
+ return RunResponseSchema.parse(response);
901
+ }
902
+
903
+ async events(runId: string): Promise<z.infer<typeof RunEventsResponseSchema>["events"]> {
904
+ const response = await this.request(`/v1/headless/runs/${encodeURIComponent(runId)}/events`);
905
+ return RunEventsResponseSchema.parse(response).events;
906
+ }
907
+
908
+ async waitForTerminal(
909
+ runId: string,
910
+ options: WaitForTerminalOptions = {}
911
+ ): Promise<z.infer<typeof RunResponseSchema>> {
912
+ const intervalMs = options.intervalMs ?? 1_500;
913
+ const timeoutMs = options.timeoutMs ?? 120_000;
914
+ const startedAt = Date.now();
915
+
916
+ while (true) {
917
+ const current = await this.status(runId);
918
+ if (isTerminalState(current.run.state)) {
919
+ return current;
920
+ }
921
+ if (Date.now() - startedAt >= timeoutMs) {
922
+ throw new DriftGateError("timeout", `run ${runId} did not reach terminal state before timeout`, 408);
923
+ }
924
+ await sleep(intervalMs);
925
+ }
926
+ }
927
+
928
+ async deployWorkflow(input: DeployWorkflowInput): Promise<z.infer<typeof DeployResponseSchema>> {
929
+ const response = await this.request("/v1/headless/workflows/deploy", {
930
+ method: "POST",
931
+ body: JSON.stringify(input)
932
+ });
933
+ return DeployResponseSchema.parse(response);
934
+ }
935
+
936
+ async publishWorkflow(workflowId: string, workflowYaml?: string): Promise<WorkflowVersion> {
937
+ const response = await this.request(`/v1/headless/workflows/${encodeURIComponent(workflowId)}/publish`, {
938
+ method: "POST",
939
+ body: JSON.stringify(workflowYaml ? { workflowYaml } : {})
940
+ });
941
+ const parsed = PublishResponseSchema.parse(response);
942
+ return parsed.version;
943
+ }
944
+
945
+ private evaluateEdgeDecision(input: DriftGateRunInput): EdgeInterceptorDecision | null {
946
+ const state = this.edgeInterceptorState;
947
+ if (!state || state.workspaceId !== input.workspaceId) {
948
+ return null;
949
+ }
950
+ if (state.registration.status !== "active") {
951
+ return {
952
+ allowed: true,
953
+ reasonCode: "edge.interceptor.disabled",
954
+ reasonText: "Edge interceptor registration is disabled; enforcement skipped.",
955
+ requiredCapabilities: [],
956
+ grantedCapabilities: state.registration.capabilities
957
+ };
958
+ }
959
+
960
+ const requiredCapabilities = ["runs:create"];
961
+ const grantedCapabilities = state.registration.capabilities;
962
+ const grantedSet = new Set(grantedCapabilities);
963
+ const missingCapabilities = requiredCapabilities.filter(
964
+ (capability) => !grantedSet.has(capability)
965
+ );
966
+ if (missingCapabilities.length === 0) {
967
+ return {
968
+ allowed: true,
969
+ reasonCode: "edge.interceptor.allow",
970
+ reasonText: "Edge interceptor capability checks passed.",
971
+ requiredCapabilities,
972
+ grantedCapabilities
973
+ };
974
+ }
975
+
976
+ return {
977
+ allowed: false,
978
+ reasonCode: "edge.interceptor.denied.missing_capability",
979
+ reasonText: `Missing required edge capabilities: ${missingCapabilities.join(", ")}`,
980
+ requiredCapabilities,
981
+ grantedCapabilities
982
+ };
983
+ }
984
+
985
+ private async request(path: string, init: RequestInit = {}): Promise<unknown> {
986
+ const headers = new Headers(init.headers ?? {});
987
+ if (!headers.has("content-type") && init.body) {
988
+ headers.set("content-type", "application/json");
989
+ }
990
+
991
+ if (this.apiKey) {
992
+ headers.set("x-driftgate-api-key", this.apiKey);
993
+ } else if (this.sessionToken) {
994
+ headers.set("authorization", `Bearer ${this.sessionToken}`);
995
+ }
996
+
997
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
998
+ ...init,
999
+ headers
1000
+ });
1001
+
1002
+ const rawText = await response.text();
1003
+ const body = rawText.length > 0 ? safelyParseJson(rawText) : null;
1004
+
1005
+ if (!response.ok) {
1006
+ const canonicalEnvelope = CanonicalErrorEnvelopeSchema.safeParse(body);
1007
+ if (canonicalEnvelope.success) {
1008
+ throw new DriftGateError(
1009
+ canonicalEnvelope.data.error.code,
1010
+ canonicalEnvelope.data.error.message,
1011
+ canonicalEnvelope.data.error.status,
1012
+ canonicalEnvelope.data.meta.requestId,
1013
+ canonicalEnvelope.data.error.details
1014
+ );
1015
+ }
1016
+
1017
+ const envelope = HeadlessErrorEnvelopeSchema.safeParse(body);
1018
+ if (envelope.success) {
1019
+ throw new DriftGateError(
1020
+ envelope.data.code,
1021
+ envelope.data.message,
1022
+ response.status,
1023
+ envelope.data.correlation_id,
1024
+ envelope.data.details
1025
+ );
1026
+ }
1027
+
1028
+ const legacyEnvelope = LegacyErrorEnvelopeSchema.safeParse(body);
1029
+ if (legacyEnvelope.success) {
1030
+ throw new DriftGateError(
1031
+ legacyEnvelope.data.error,
1032
+ legacyEnvelope.data.message,
1033
+ response.status,
1034
+ undefined,
1035
+ legacyEnvelope.data.issues
1036
+ );
1037
+ }
1038
+
1039
+ throw new DriftGateError(
1040
+ "http_error",
1041
+ `request failed (${response.status})${rawText ? `: ${rawText}` : ""}`,
1042
+ response.status
1043
+ );
1044
+ }
1045
+
1046
+ return body;
1047
+ }
1048
+ }
1049
+
1050
+ function safelyParseJson(input: string): unknown {
1051
+ try {
1052
+ return JSON.parse(input);
1053
+ } catch {
1054
+ return { raw: input };
1055
+ }
1056
+ }
1057
+
1058
+ export type DriftGateRunResponse = z.infer<typeof RunResponseSchema>;
1059
+ export type DriftGateRunEvent = z.infer<typeof RunEventsResponseSchema>["events"][number];
1060
+ export type DriftGatePolicyDecision = PolicyDecision;
1061
+ export type DriftGateEdgeInterceptorRegistration = EdgeInterceptorRegistration;
1062
+ export type DriftGateEdgeInterceptorDecision = EdgeInterceptorDecision;
1063
+ export type DriftGateFirewallInspectInput = FirewallInspectInput;
1064
+ export type DriftGateFirewallInspectResponse = FirewallInspectResponse;
1065
+ export type DriftGateFirewallEvent = FirewallEvent;