@runplane/runplane-sdk 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @runplane/runplane-sdk - TypeScript types for the Runplane SDK
3
+ */
4
+ /** Decision returned by the Runplane control plane */
5
+ type Decision = "ALLOW" | "BLOCK" | "REQUIRE_APPROVAL";
6
+ /** Approval status for pending actions */
7
+ type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
8
+ /** Behavior when the API is unreachable or times out */
9
+ type FailMode = "open" | "closed";
10
+ /**
11
+ * Configuration options for the Runplane client
12
+ */
13
+ interface RunplaneConfig {
14
+ /** Your Runplane API key (starts with ars_) */
15
+ apiKey: string;
16
+ /** Base URL for the Runplane API. Defaults to https://runplane.ai */
17
+ baseUrl?: string;
18
+ /** Timeout for API requests in milliseconds. Defaults to 3000 */
19
+ timeoutMs?: number;
20
+ /**
21
+ * Behavior when API is unreachable:
22
+ * - "closed" (default): Block actions when API is unavailable
23
+ * - "open": Allow actions when API is unavailable (less secure)
24
+ */
25
+ failMode?: FailMode;
26
+ /** Timeout for approval polling in milliseconds. Defaults to 300000 (5 min) */
27
+ approvalTimeoutMs?: number;
28
+ /** Interval between approval polls in milliseconds. Defaults to 2000 */
29
+ approvalPollIntervalMs?: number;
30
+ }
31
+ /**
32
+ * Request payload for the decide endpoint
33
+ */
34
+ interface DecideRequest {
35
+ /** Type of action being performed (e.g., "send_email", "delete_record") */
36
+ actionType: string;
37
+ /** Target resource or destination (e.g., "prod-db", "user@example.com") */
38
+ target: string;
39
+ /** Additional context for risk evaluation */
40
+ context?: Record<string, unknown>;
41
+ /** Optional request ID for idempotency */
42
+ requestId?: string;
43
+ }
44
+ /**
45
+ * Response from the decide endpoint
46
+ */
47
+ interface DecideResponse {
48
+ /** The decision: ALLOW, BLOCK, or REQUIRE_APPROVAL */
49
+ decision: Decision;
50
+ /** Human-readable reason for the decision */
51
+ reason: string;
52
+ /** Unique identifier for this request (use for approval polling) */
53
+ requestId: string;
54
+ /** The policy rule that matched, if any */
55
+ matchedRule?: string;
56
+ /** Computed risk score (0-100) */
57
+ riskScore?: number;
58
+ /** Risk severity classification */
59
+ severity?: string;
60
+ }
61
+ /**
62
+ * Response from the approval polling endpoint
63
+ */
64
+ interface ApprovalPollResponse {
65
+ /** Current status of the approval request */
66
+ status: ApprovalStatus;
67
+ /** ISO timestamp when the approval was resolved */
68
+ resolvedAt?: string;
69
+ /** User who approved/denied the request */
70
+ resolvedBy?: string;
71
+ /** Optional comment from the approver */
72
+ comment?: string;
73
+ }
74
+ /**
75
+ * Options for waitForApproval method
76
+ */
77
+ interface WaitForApprovalOptions {
78
+ /** Override the default approval timeout */
79
+ timeoutMs?: number;
80
+ /** Override the default poll interval */
81
+ pollIntervalMs?: number;
82
+ /** Callback invoked on each poll (for progress updates) */
83
+ onPoll?: (status: ApprovalStatus, elapsedMs: number) => void;
84
+ }
85
+ /**
86
+ * Result from waitForApproval method
87
+ */
88
+ interface WaitForApprovalResult {
89
+ /** Whether the action was approved */
90
+ approved: boolean;
91
+ /** Final status */
92
+ status: ApprovalStatus;
93
+ /** Optional comment from the approver */
94
+ comment?: string;
95
+ /** User who resolved the request */
96
+ resolvedBy?: string;
97
+ }
98
+ /**
99
+ * Error codes for RunplaneError
100
+ */
101
+ type RunplaneErrorCode = "BLOCKED" | "DENIED" | "TIMEOUT" | "NETWORK_ERROR" | "INVALID_CONFIG" | "UNKNOWN";
102
+
103
+ /**
104
+ * @runplane/runplane-sdk - Main client for the Runplane API
105
+ */
106
+
107
+ /**
108
+ * Runplane SDK Client
109
+ *
110
+ * The main entry point for interacting with the Runplane control plane.
111
+ * Use this to request execution clearance before performing sensitive actions.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * import { Runplane } from "@runplane/runplane-sdk";
116
+ *
117
+ * const runplane = new Runplane({
118
+ * apiKey: process.env.RUNPLANE_SYSTEM_KEY!,
119
+ * baseUrl: "https://runplane.ai",
120
+ * failMode: "closed"
121
+ * });
122
+ *
123
+ * const decision = await runplane.decide({
124
+ * actionType: "send_email",
125
+ * target: "marketing_list",
126
+ * context: { recipients: 1200 }
127
+ * });
128
+ *
129
+ * if (decision.decision === "ALLOW") {
130
+ * // Proceed with the action
131
+ * }
132
+ * ```
133
+ */
134
+ declare class Runplane {
135
+ private readonly baseUrl;
136
+ private readonly apiKey;
137
+ private readonly timeoutMs;
138
+ private readonly failMode;
139
+ private readonly approvalPoller;
140
+ constructor(config: RunplaneConfig);
141
+ /**
142
+ * Request a decision from the Runplane control plane.
143
+ *
144
+ * Call this before executing any sensitive action. The response will indicate
145
+ * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
146
+ *
147
+ * @param request - The action details to evaluate
148
+ * @returns The decision response
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const decision = await runplane.decide({
153
+ * actionType: "delete_record",
154
+ * target: "users.prod",
155
+ * context: { recordId: "usr_123", reason: "gdpr_request" }
156
+ * });
157
+ *
158
+ * switch (decision.decision) {
159
+ * case "ALLOW":
160
+ * await deleteRecord(recordId);
161
+ * break;
162
+ * case "BLOCK":
163
+ * console.error("Action blocked:", decision.reason);
164
+ * break;
165
+ * case "REQUIRE_APPROVAL":
166
+ * // Wait for human approval
167
+ * const result = await runplane.waitForApproval(decision.requestId);
168
+ * if (result.approved) {
169
+ * await deleteRecord(recordId);
170
+ * }
171
+ * break;
172
+ * }
173
+ * ```
174
+ */
175
+ decide(request: DecideRequest): Promise<DecideResponse>;
176
+ /**
177
+ * Wait for an approval decision on a pending request.
178
+ *
179
+ * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
180
+ * the approval endpoint until the request is approved, denied, or times out.
181
+ *
182
+ * @param requestId - The requestId from the decide() response
183
+ * @param options - Optional configuration for polling behavior
184
+ * @returns The approval result
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * const decision = await runplane.decide({ ... });
189
+ *
190
+ * if (decision.decision === "REQUIRE_APPROVAL") {
191
+ * console.log("Waiting for approval...");
192
+ *
193
+ * const result = await runplane.waitForApproval(decision.requestId, {
194
+ * timeoutMs: 600000, // 10 minutes
195
+ * onPoll: (status, elapsed) => {
196
+ * console.log(`Still waiting... ${elapsed}ms elapsed`);
197
+ * }
198
+ * });
199
+ *
200
+ * if (result.approved) {
201
+ * console.log("Approved by:", result.resolvedBy);
202
+ * } else {
203
+ * console.log("Denied:", result.comment);
204
+ * }
205
+ * }
206
+ * ```
207
+ */
208
+ waitForApproval(requestId: string, options?: WaitForApprovalOptions): Promise<WaitForApprovalResult>;
209
+ /**
210
+ * Guard a function with containment evaluation.
211
+ *
212
+ * This is a convenience method that wraps decide() and waitForApproval()
213
+ * into a single call. If the action is blocked or denied, it throws.
214
+ * If approved, it executes the provided function.
215
+ *
216
+ * @param actionType - Type of action being performed
217
+ * @param target - Target resource
218
+ * @param context - Additional context
219
+ * @param fn - Function to execute if allowed
220
+ * @returns The result of fn()
221
+ * @throws RunplaneError if blocked, denied, or times out (in closed mode)
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * const result = await runplane.guard(
226
+ * "payment_transfer",
227
+ * "external_bank",
228
+ * { amount: 50000, currency: "USD" },
229
+ * async () => {
230
+ * return await paymentService.transfer(amount, recipient);
231
+ * }
232
+ * );
233
+ * ```
234
+ */
235
+ guard<T>(actionType: string, target: string, context: Record<string, unknown> | null, fn: () => Promise<T>): Promise<T>;
236
+ /**
237
+ * Handle API failures according to failMode
238
+ */
239
+ private handleFailure;
240
+ }
241
+
242
+ /**
243
+ * @runplane/runplane-sdk - Error handling
244
+ */
245
+
246
+ /**
247
+ * Error thrown by the Runplane SDK
248
+ *
249
+ * This error is thrown when:
250
+ * - An action is blocked by policy
251
+ * - An approval request is denied
252
+ * - Approval times out (in fail-closed mode)
253
+ * - Network errors occur (in fail-closed mode)
254
+ * - Invalid configuration is provided
255
+ */
256
+ declare class RunplaneError extends Error {
257
+ /** Error classification code */
258
+ readonly code: RunplaneErrorCode;
259
+ /** Request ID associated with this error, if available */
260
+ readonly requestId?: string;
261
+ constructor(message: string, code: RunplaneErrorCode, requestId?: string);
262
+ isBlocked(): boolean;
263
+ isDenied(): boolean;
264
+ isTimeout(): boolean;
265
+ isNetworkError(): boolean;
266
+ }
267
+
268
+ export { type ApprovalPollResponse, type ApprovalStatus, type DecideRequest, type DecideResponse, type Decision, type FailMode, Runplane, type RunplaneConfig, RunplaneError, type RunplaneErrorCode, type WaitForApprovalOptions, type WaitForApprovalResult };
@@ -0,0 +1,268 @@
1
+ /**
2
+ * @runplane/runplane-sdk - TypeScript types for the Runplane SDK
3
+ */
4
+ /** Decision returned by the Runplane control plane */
5
+ type Decision = "ALLOW" | "BLOCK" | "REQUIRE_APPROVAL";
6
+ /** Approval status for pending actions */
7
+ type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
8
+ /** Behavior when the API is unreachable or times out */
9
+ type FailMode = "open" | "closed";
10
+ /**
11
+ * Configuration options for the Runplane client
12
+ */
13
+ interface RunplaneConfig {
14
+ /** Your Runplane API key (starts with ars_) */
15
+ apiKey: string;
16
+ /** Base URL for the Runplane API. Defaults to https://runplane.ai */
17
+ baseUrl?: string;
18
+ /** Timeout for API requests in milliseconds. Defaults to 3000 */
19
+ timeoutMs?: number;
20
+ /**
21
+ * Behavior when API is unreachable:
22
+ * - "closed" (default): Block actions when API is unavailable
23
+ * - "open": Allow actions when API is unavailable (less secure)
24
+ */
25
+ failMode?: FailMode;
26
+ /** Timeout for approval polling in milliseconds. Defaults to 300000 (5 min) */
27
+ approvalTimeoutMs?: number;
28
+ /** Interval between approval polls in milliseconds. Defaults to 2000 */
29
+ approvalPollIntervalMs?: number;
30
+ }
31
+ /**
32
+ * Request payload for the decide endpoint
33
+ */
34
+ interface DecideRequest {
35
+ /** Type of action being performed (e.g., "send_email", "delete_record") */
36
+ actionType: string;
37
+ /** Target resource or destination (e.g., "prod-db", "user@example.com") */
38
+ target: string;
39
+ /** Additional context for risk evaluation */
40
+ context?: Record<string, unknown>;
41
+ /** Optional request ID for idempotency */
42
+ requestId?: string;
43
+ }
44
+ /**
45
+ * Response from the decide endpoint
46
+ */
47
+ interface DecideResponse {
48
+ /** The decision: ALLOW, BLOCK, or REQUIRE_APPROVAL */
49
+ decision: Decision;
50
+ /** Human-readable reason for the decision */
51
+ reason: string;
52
+ /** Unique identifier for this request (use for approval polling) */
53
+ requestId: string;
54
+ /** The policy rule that matched, if any */
55
+ matchedRule?: string;
56
+ /** Computed risk score (0-100) */
57
+ riskScore?: number;
58
+ /** Risk severity classification */
59
+ severity?: string;
60
+ }
61
+ /**
62
+ * Response from the approval polling endpoint
63
+ */
64
+ interface ApprovalPollResponse {
65
+ /** Current status of the approval request */
66
+ status: ApprovalStatus;
67
+ /** ISO timestamp when the approval was resolved */
68
+ resolvedAt?: string;
69
+ /** User who approved/denied the request */
70
+ resolvedBy?: string;
71
+ /** Optional comment from the approver */
72
+ comment?: string;
73
+ }
74
+ /**
75
+ * Options for waitForApproval method
76
+ */
77
+ interface WaitForApprovalOptions {
78
+ /** Override the default approval timeout */
79
+ timeoutMs?: number;
80
+ /** Override the default poll interval */
81
+ pollIntervalMs?: number;
82
+ /** Callback invoked on each poll (for progress updates) */
83
+ onPoll?: (status: ApprovalStatus, elapsedMs: number) => void;
84
+ }
85
+ /**
86
+ * Result from waitForApproval method
87
+ */
88
+ interface WaitForApprovalResult {
89
+ /** Whether the action was approved */
90
+ approved: boolean;
91
+ /** Final status */
92
+ status: ApprovalStatus;
93
+ /** Optional comment from the approver */
94
+ comment?: string;
95
+ /** User who resolved the request */
96
+ resolvedBy?: string;
97
+ }
98
+ /**
99
+ * Error codes for RunplaneError
100
+ */
101
+ type RunplaneErrorCode = "BLOCKED" | "DENIED" | "TIMEOUT" | "NETWORK_ERROR" | "INVALID_CONFIG" | "UNKNOWN";
102
+
103
+ /**
104
+ * @runplane/runplane-sdk - Main client for the Runplane API
105
+ */
106
+
107
+ /**
108
+ * Runplane SDK Client
109
+ *
110
+ * The main entry point for interacting with the Runplane control plane.
111
+ * Use this to request execution clearance before performing sensitive actions.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * import { Runplane } from "@runplane/runplane-sdk";
116
+ *
117
+ * const runplane = new Runplane({
118
+ * apiKey: process.env.RUNPLANE_SYSTEM_KEY!,
119
+ * baseUrl: "https://runplane.ai",
120
+ * failMode: "closed"
121
+ * });
122
+ *
123
+ * const decision = await runplane.decide({
124
+ * actionType: "send_email",
125
+ * target: "marketing_list",
126
+ * context: { recipients: 1200 }
127
+ * });
128
+ *
129
+ * if (decision.decision === "ALLOW") {
130
+ * // Proceed with the action
131
+ * }
132
+ * ```
133
+ */
134
+ declare class Runplane {
135
+ private readonly baseUrl;
136
+ private readonly apiKey;
137
+ private readonly timeoutMs;
138
+ private readonly failMode;
139
+ private readonly approvalPoller;
140
+ constructor(config: RunplaneConfig);
141
+ /**
142
+ * Request a decision from the Runplane control plane.
143
+ *
144
+ * Call this before executing any sensitive action. The response will indicate
145
+ * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
146
+ *
147
+ * @param request - The action details to evaluate
148
+ * @returns The decision response
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const decision = await runplane.decide({
153
+ * actionType: "delete_record",
154
+ * target: "users.prod",
155
+ * context: { recordId: "usr_123", reason: "gdpr_request" }
156
+ * });
157
+ *
158
+ * switch (decision.decision) {
159
+ * case "ALLOW":
160
+ * await deleteRecord(recordId);
161
+ * break;
162
+ * case "BLOCK":
163
+ * console.error("Action blocked:", decision.reason);
164
+ * break;
165
+ * case "REQUIRE_APPROVAL":
166
+ * // Wait for human approval
167
+ * const result = await runplane.waitForApproval(decision.requestId);
168
+ * if (result.approved) {
169
+ * await deleteRecord(recordId);
170
+ * }
171
+ * break;
172
+ * }
173
+ * ```
174
+ */
175
+ decide(request: DecideRequest): Promise<DecideResponse>;
176
+ /**
177
+ * Wait for an approval decision on a pending request.
178
+ *
179
+ * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
180
+ * the approval endpoint until the request is approved, denied, or times out.
181
+ *
182
+ * @param requestId - The requestId from the decide() response
183
+ * @param options - Optional configuration for polling behavior
184
+ * @returns The approval result
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * const decision = await runplane.decide({ ... });
189
+ *
190
+ * if (decision.decision === "REQUIRE_APPROVAL") {
191
+ * console.log("Waiting for approval...");
192
+ *
193
+ * const result = await runplane.waitForApproval(decision.requestId, {
194
+ * timeoutMs: 600000, // 10 minutes
195
+ * onPoll: (status, elapsed) => {
196
+ * console.log(`Still waiting... ${elapsed}ms elapsed`);
197
+ * }
198
+ * });
199
+ *
200
+ * if (result.approved) {
201
+ * console.log("Approved by:", result.resolvedBy);
202
+ * } else {
203
+ * console.log("Denied:", result.comment);
204
+ * }
205
+ * }
206
+ * ```
207
+ */
208
+ waitForApproval(requestId: string, options?: WaitForApprovalOptions): Promise<WaitForApprovalResult>;
209
+ /**
210
+ * Guard a function with containment evaluation.
211
+ *
212
+ * This is a convenience method that wraps decide() and waitForApproval()
213
+ * into a single call. If the action is blocked or denied, it throws.
214
+ * If approved, it executes the provided function.
215
+ *
216
+ * @param actionType - Type of action being performed
217
+ * @param target - Target resource
218
+ * @param context - Additional context
219
+ * @param fn - Function to execute if allowed
220
+ * @returns The result of fn()
221
+ * @throws RunplaneError if blocked, denied, or times out (in closed mode)
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * const result = await runplane.guard(
226
+ * "payment_transfer",
227
+ * "external_bank",
228
+ * { amount: 50000, currency: "USD" },
229
+ * async () => {
230
+ * return await paymentService.transfer(amount, recipient);
231
+ * }
232
+ * );
233
+ * ```
234
+ */
235
+ guard<T>(actionType: string, target: string, context: Record<string, unknown> | null, fn: () => Promise<T>): Promise<T>;
236
+ /**
237
+ * Handle API failures according to failMode
238
+ */
239
+ private handleFailure;
240
+ }
241
+
242
+ /**
243
+ * @runplane/runplane-sdk - Error handling
244
+ */
245
+
246
+ /**
247
+ * Error thrown by the Runplane SDK
248
+ *
249
+ * This error is thrown when:
250
+ * - An action is blocked by policy
251
+ * - An approval request is denied
252
+ * - Approval times out (in fail-closed mode)
253
+ * - Network errors occur (in fail-closed mode)
254
+ * - Invalid configuration is provided
255
+ */
256
+ declare class RunplaneError extends Error {
257
+ /** Error classification code */
258
+ readonly code: RunplaneErrorCode;
259
+ /** Request ID associated with this error, if available */
260
+ readonly requestId?: string;
261
+ constructor(message: string, code: RunplaneErrorCode, requestId?: string);
262
+ isBlocked(): boolean;
263
+ isDenied(): boolean;
264
+ isTimeout(): boolean;
265
+ isNetworkError(): boolean;
266
+ }
267
+
268
+ export { type ApprovalPollResponse, type ApprovalStatus, type DecideRequest, type DecideResponse, type Decision, type FailMode, Runplane, type RunplaneConfig, RunplaneError, type RunplaneErrorCode, type WaitForApprovalOptions, type WaitForApprovalResult };
package/dist/index.js CHANGED
@@ -1,21 +1,341 @@
1
- class Runplane {
2
- constructor(apiKey, baseUrl = "https://runplane.ai") {
3
- this.apiKey = apiKey;
4
- this.baseUrl = baseUrl;
5
- }
6
-
7
- async decide(payload) {
8
- const response = await fetch(`${this.baseUrl}/api/decide`, {
9
- method: "POST",
10
- headers: {
11
- "Content-Type": "application/json",
12
- "Authorization": `Bearer ${this.apiKey}`
13
- },
14
- body: JSON.stringify(payload)
15
- });
16
-
17
- return response.json();
18
- }
19
- }
20
-
21
- module.exports = Runplane;
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Runplane: () => Runplane,
24
+ RunplaneError: () => RunplaneError
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/error.ts
29
+ var RunplaneError = class _RunplaneError extends Error {
30
+ constructor(message, code, requestId) {
31
+ super(message);
32
+ this.name = "RunplaneError";
33
+ this.code = code;
34
+ this.requestId = requestId;
35
+ if (Error.captureStackTrace) {
36
+ Error.captureStackTrace(this, _RunplaneError);
37
+ }
38
+ }
39
+ isBlocked() {
40
+ return this.code === "BLOCKED";
41
+ }
42
+ isDenied() {
43
+ return this.code === "DENIED";
44
+ }
45
+ isTimeout() {
46
+ return this.code === "TIMEOUT";
47
+ }
48
+ isNetworkError() {
49
+ return this.code === "NETWORK_ERROR";
50
+ }
51
+ };
52
+
53
+ // src/approval.ts
54
+ var ApprovalPoller = class {
55
+ // Cap at 10 seconds
56
+ constructor(config) {
57
+ this.maxPollIntervalMs = 1e4;
58
+ this.baseUrl = config.baseUrl;
59
+ this.apiKey = config.apiKey;
60
+ this.defaultTimeoutMs = config.timeoutMs;
61
+ this.defaultPollIntervalMs = config.pollIntervalMs;
62
+ }
63
+ /**
64
+ * Poll for approval status until resolved or timeout
65
+ */
66
+ async poll(requestId, options) {
67
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
68
+ const initialInterval = options?.pollIntervalMs ?? this.defaultPollIntervalMs;
69
+ const onPoll = options?.onPoll;
70
+ const startTime = Date.now();
71
+ let interval = initialInterval;
72
+ while (Date.now() - startTime < timeoutMs) {
73
+ try {
74
+ const response = await this.fetchApprovalStatus(requestId);
75
+ const elapsed = Date.now() - startTime;
76
+ if (onPoll) {
77
+ onPoll(response.status, elapsed);
78
+ }
79
+ if (response.status !== "pending") {
80
+ return {
81
+ approved: response.status === "approved",
82
+ status: response.status,
83
+ comment: response.comment,
84
+ resolvedBy: response.resolvedBy
85
+ };
86
+ }
87
+ await this.sleep(interval);
88
+ interval = Math.min(interval * 1.5, this.maxPollIntervalMs);
89
+ } catch {
90
+ await this.sleep(interval);
91
+ interval = Math.min(interval * 2, this.maxPollIntervalMs);
92
+ }
93
+ }
94
+ return {
95
+ approved: false,
96
+ status: "expired"
97
+ };
98
+ }
99
+ /**
100
+ * Fetch current approval status from the API
101
+ */
102
+ async fetchApprovalStatus(requestId) {
103
+ const response = await fetch(
104
+ `${this.baseUrl}/api/approvals/poll/${requestId}`,
105
+ {
106
+ headers: {
107
+ "Authorization": `Bearer ${this.apiKey}`
108
+ }
109
+ }
110
+ );
111
+ if (!response.ok) {
112
+ throw new Error(`Failed to fetch approval status: ${response.statusText}`);
113
+ }
114
+ return response.json();
115
+ }
116
+ sleep(ms) {
117
+ return new Promise((resolve) => setTimeout(resolve, ms));
118
+ }
119
+ };
120
+
121
+ // src/client.ts
122
+ var DEFAULT_BASE_URL = "https://runplane.ai";
123
+ var DEFAULT_TIMEOUT_MS = 3e3;
124
+ var DEFAULT_FAIL_MODE = "closed";
125
+ var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
126
+ var DEFAULT_APPROVAL_POLL_INTERVAL_MS = 2e3;
127
+ var Runplane = class {
128
+ constructor(config) {
129
+ if (!config.apiKey) {
130
+ throw new RunplaneError(
131
+ "API key is required",
132
+ "INVALID_CONFIG"
133
+ );
134
+ }
135
+ this.apiKey = config.apiKey;
136
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
137
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
138
+ this.failMode = config.failMode ?? DEFAULT_FAIL_MODE;
139
+ this.approvalPoller = new ApprovalPoller({
140
+ baseUrl: this.baseUrl,
141
+ apiKey: this.apiKey,
142
+ timeoutMs: config.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
143
+ pollIntervalMs: config.approvalPollIntervalMs ?? DEFAULT_APPROVAL_POLL_INTERVAL_MS
144
+ });
145
+ }
146
+ /**
147
+ * Request a decision from the Runplane control plane.
148
+ *
149
+ * Call this before executing any sensitive action. The response will indicate
150
+ * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
151
+ *
152
+ * @param request - The action details to evaluate
153
+ * @returns The decision response
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const decision = await runplane.decide({
158
+ * actionType: "delete_record",
159
+ * target: "users.prod",
160
+ * context: { recordId: "usr_123", reason: "gdpr_request" }
161
+ * });
162
+ *
163
+ * switch (decision.decision) {
164
+ * case "ALLOW":
165
+ * await deleteRecord(recordId);
166
+ * break;
167
+ * case "BLOCK":
168
+ * console.error("Action blocked:", decision.reason);
169
+ * break;
170
+ * case "REQUIRE_APPROVAL":
171
+ * // Wait for human approval
172
+ * const result = await runplane.waitForApproval(decision.requestId);
173
+ * if (result.approved) {
174
+ * await deleteRecord(recordId);
175
+ * }
176
+ * break;
177
+ * }
178
+ * ```
179
+ */
180
+ async decide(request) {
181
+ const controller = new AbortController();
182
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
183
+ try {
184
+ const response = await fetch(`${this.baseUrl}/api/decide`, {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json",
188
+ "Authorization": `Bearer ${this.apiKey}`
189
+ },
190
+ body: JSON.stringify({
191
+ agentKey: this.apiKey,
192
+ actionType: request.actionType,
193
+ target: request.target,
194
+ context: request.context ?? {},
195
+ requestId: request.requestId
196
+ }),
197
+ signal: controller.signal
198
+ });
199
+ const json = await response.json();
200
+ if (!response.ok) {
201
+ return this.handleFailure(
202
+ `API error: ${json.error ?? response.statusText}`,
203
+ request
204
+ );
205
+ }
206
+ return {
207
+ decision: json.decision,
208
+ reason: json.reason,
209
+ requestId: json.requestId,
210
+ matchedRule: json.matchedRule,
211
+ riskScore: json.riskScore,
212
+ severity: json.severity
213
+ };
214
+ } catch (error) {
215
+ const message = error instanceof Error ? error.message : "Unknown error";
216
+ return this.handleFailure(`Network failure: ${message}`, request);
217
+ } finally {
218
+ clearTimeout(timeout);
219
+ }
220
+ }
221
+ /**
222
+ * Wait for an approval decision on a pending request.
223
+ *
224
+ * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
225
+ * the approval endpoint until the request is approved, denied, or times out.
226
+ *
227
+ * @param requestId - The requestId from the decide() response
228
+ * @param options - Optional configuration for polling behavior
229
+ * @returns The approval result
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * const decision = await runplane.decide({ ... });
234
+ *
235
+ * if (decision.decision === "REQUIRE_APPROVAL") {
236
+ * console.log("Waiting for approval...");
237
+ *
238
+ * const result = await runplane.waitForApproval(decision.requestId, {
239
+ * timeoutMs: 600000, // 10 minutes
240
+ * onPoll: (status, elapsed) => {
241
+ * console.log(`Still waiting... ${elapsed}ms elapsed`);
242
+ * }
243
+ * });
244
+ *
245
+ * if (result.approved) {
246
+ * console.log("Approved by:", result.resolvedBy);
247
+ * } else {
248
+ * console.log("Denied:", result.comment);
249
+ * }
250
+ * }
251
+ * ```
252
+ */
253
+ async waitForApproval(requestId, options) {
254
+ return this.approvalPoller.poll(requestId, options);
255
+ }
256
+ /**
257
+ * Guard a function with containment evaluation.
258
+ *
259
+ * This is a convenience method that wraps decide() and waitForApproval()
260
+ * into a single call. If the action is blocked or denied, it throws.
261
+ * If approved, it executes the provided function.
262
+ *
263
+ * @param actionType - Type of action being performed
264
+ * @param target - Target resource
265
+ * @param context - Additional context
266
+ * @param fn - Function to execute if allowed
267
+ * @returns The result of fn()
268
+ * @throws RunplaneError if blocked, denied, or times out (in closed mode)
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * const result = await runplane.guard(
273
+ * "payment_transfer",
274
+ * "external_bank",
275
+ * { amount: 50000, currency: "USD" },
276
+ * async () => {
277
+ * return await paymentService.transfer(amount, recipient);
278
+ * }
279
+ * );
280
+ * ```
281
+ */
282
+ async guard(actionType, target, context, fn) {
283
+ const response = await this.decide({
284
+ actionType,
285
+ target,
286
+ context: context ?? void 0
287
+ });
288
+ if (response.decision === "BLOCK") {
289
+ throw new RunplaneError(
290
+ `Action blocked: ${response.reason}`,
291
+ "BLOCKED",
292
+ response.requestId
293
+ );
294
+ }
295
+ if (response.decision === "ALLOW") {
296
+ return fn();
297
+ }
298
+ const approval = await this.waitForApproval(response.requestId);
299
+ if (approval.approved) {
300
+ return fn();
301
+ }
302
+ if (approval.status === "denied") {
303
+ throw new RunplaneError(
304
+ `Action denied: ${approval.comment || "No reason provided"}`,
305
+ "DENIED",
306
+ response.requestId
307
+ );
308
+ }
309
+ if (this.failMode === "open") {
310
+ return fn();
311
+ }
312
+ throw new RunplaneError(
313
+ "Approval timeout - action blocked (fail-closed)",
314
+ "TIMEOUT",
315
+ response.requestId
316
+ );
317
+ }
318
+ /**
319
+ * Handle API failures according to failMode
320
+ */
321
+ handleFailure(reason, request) {
322
+ const requestId = request.requestId ?? crypto.randomUUID();
323
+ if (this.failMode === "open") {
324
+ return {
325
+ decision: "ALLOW",
326
+ reason: `${reason} (fail-open)`,
327
+ requestId
328
+ };
329
+ }
330
+ return {
331
+ decision: "BLOCK",
332
+ reason: `${reason} (fail-closed)`,
333
+ requestId
334
+ };
335
+ }
336
+ };
337
+ // Annotate the CommonJS export names for ESM import in node:
338
+ 0 && (module.exports = {
339
+ Runplane,
340
+ RunplaneError
341
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,313 @@
1
+ // src/error.ts
2
+ var RunplaneError = class _RunplaneError extends Error {
3
+ constructor(message, code, requestId) {
4
+ super(message);
5
+ this.name = "RunplaneError";
6
+ this.code = code;
7
+ this.requestId = requestId;
8
+ if (Error.captureStackTrace) {
9
+ Error.captureStackTrace(this, _RunplaneError);
10
+ }
11
+ }
12
+ isBlocked() {
13
+ return this.code === "BLOCKED";
14
+ }
15
+ isDenied() {
16
+ return this.code === "DENIED";
17
+ }
18
+ isTimeout() {
19
+ return this.code === "TIMEOUT";
20
+ }
21
+ isNetworkError() {
22
+ return this.code === "NETWORK_ERROR";
23
+ }
24
+ };
25
+
26
+ // src/approval.ts
27
+ var ApprovalPoller = class {
28
+ // Cap at 10 seconds
29
+ constructor(config) {
30
+ this.maxPollIntervalMs = 1e4;
31
+ this.baseUrl = config.baseUrl;
32
+ this.apiKey = config.apiKey;
33
+ this.defaultTimeoutMs = config.timeoutMs;
34
+ this.defaultPollIntervalMs = config.pollIntervalMs;
35
+ }
36
+ /**
37
+ * Poll for approval status until resolved or timeout
38
+ */
39
+ async poll(requestId, options) {
40
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
41
+ const initialInterval = options?.pollIntervalMs ?? this.defaultPollIntervalMs;
42
+ const onPoll = options?.onPoll;
43
+ const startTime = Date.now();
44
+ let interval = initialInterval;
45
+ while (Date.now() - startTime < timeoutMs) {
46
+ try {
47
+ const response = await this.fetchApprovalStatus(requestId);
48
+ const elapsed = Date.now() - startTime;
49
+ if (onPoll) {
50
+ onPoll(response.status, elapsed);
51
+ }
52
+ if (response.status !== "pending") {
53
+ return {
54
+ approved: response.status === "approved",
55
+ status: response.status,
56
+ comment: response.comment,
57
+ resolvedBy: response.resolvedBy
58
+ };
59
+ }
60
+ await this.sleep(interval);
61
+ interval = Math.min(interval * 1.5, this.maxPollIntervalMs);
62
+ } catch {
63
+ await this.sleep(interval);
64
+ interval = Math.min(interval * 2, this.maxPollIntervalMs);
65
+ }
66
+ }
67
+ return {
68
+ approved: false,
69
+ status: "expired"
70
+ };
71
+ }
72
+ /**
73
+ * Fetch current approval status from the API
74
+ */
75
+ async fetchApprovalStatus(requestId) {
76
+ const response = await fetch(
77
+ `${this.baseUrl}/api/approvals/poll/${requestId}`,
78
+ {
79
+ headers: {
80
+ "Authorization": `Bearer ${this.apiKey}`
81
+ }
82
+ }
83
+ );
84
+ if (!response.ok) {
85
+ throw new Error(`Failed to fetch approval status: ${response.statusText}`);
86
+ }
87
+ return response.json();
88
+ }
89
+ sleep(ms) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms));
91
+ }
92
+ };
93
+
94
+ // src/client.ts
95
+ var DEFAULT_BASE_URL = "https://runplane.ai";
96
+ var DEFAULT_TIMEOUT_MS = 3e3;
97
+ var DEFAULT_FAIL_MODE = "closed";
98
+ var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
99
+ var DEFAULT_APPROVAL_POLL_INTERVAL_MS = 2e3;
100
+ var Runplane = class {
101
+ constructor(config) {
102
+ if (!config.apiKey) {
103
+ throw new RunplaneError(
104
+ "API key is required",
105
+ "INVALID_CONFIG"
106
+ );
107
+ }
108
+ this.apiKey = config.apiKey;
109
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
110
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
111
+ this.failMode = config.failMode ?? DEFAULT_FAIL_MODE;
112
+ this.approvalPoller = new ApprovalPoller({
113
+ baseUrl: this.baseUrl,
114
+ apiKey: this.apiKey,
115
+ timeoutMs: config.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
116
+ pollIntervalMs: config.approvalPollIntervalMs ?? DEFAULT_APPROVAL_POLL_INTERVAL_MS
117
+ });
118
+ }
119
+ /**
120
+ * Request a decision from the Runplane control plane.
121
+ *
122
+ * Call this before executing any sensitive action. The response will indicate
123
+ * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
124
+ *
125
+ * @param request - The action details to evaluate
126
+ * @returns The decision response
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const decision = await runplane.decide({
131
+ * actionType: "delete_record",
132
+ * target: "users.prod",
133
+ * context: { recordId: "usr_123", reason: "gdpr_request" }
134
+ * });
135
+ *
136
+ * switch (decision.decision) {
137
+ * case "ALLOW":
138
+ * await deleteRecord(recordId);
139
+ * break;
140
+ * case "BLOCK":
141
+ * console.error("Action blocked:", decision.reason);
142
+ * break;
143
+ * case "REQUIRE_APPROVAL":
144
+ * // Wait for human approval
145
+ * const result = await runplane.waitForApproval(decision.requestId);
146
+ * if (result.approved) {
147
+ * await deleteRecord(recordId);
148
+ * }
149
+ * break;
150
+ * }
151
+ * ```
152
+ */
153
+ async decide(request) {
154
+ const controller = new AbortController();
155
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
156
+ try {
157
+ const response = await fetch(`${this.baseUrl}/api/decide`, {
158
+ method: "POST",
159
+ headers: {
160
+ "Content-Type": "application/json",
161
+ "Authorization": `Bearer ${this.apiKey}`
162
+ },
163
+ body: JSON.stringify({
164
+ agentKey: this.apiKey,
165
+ actionType: request.actionType,
166
+ target: request.target,
167
+ context: request.context ?? {},
168
+ requestId: request.requestId
169
+ }),
170
+ signal: controller.signal
171
+ });
172
+ const json = await response.json();
173
+ if (!response.ok) {
174
+ return this.handleFailure(
175
+ `API error: ${json.error ?? response.statusText}`,
176
+ request
177
+ );
178
+ }
179
+ return {
180
+ decision: json.decision,
181
+ reason: json.reason,
182
+ requestId: json.requestId,
183
+ matchedRule: json.matchedRule,
184
+ riskScore: json.riskScore,
185
+ severity: json.severity
186
+ };
187
+ } catch (error) {
188
+ const message = error instanceof Error ? error.message : "Unknown error";
189
+ return this.handleFailure(`Network failure: ${message}`, request);
190
+ } finally {
191
+ clearTimeout(timeout);
192
+ }
193
+ }
194
+ /**
195
+ * Wait for an approval decision on a pending request.
196
+ *
197
+ * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
198
+ * the approval endpoint until the request is approved, denied, or times out.
199
+ *
200
+ * @param requestId - The requestId from the decide() response
201
+ * @param options - Optional configuration for polling behavior
202
+ * @returns The approval result
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * const decision = await runplane.decide({ ... });
207
+ *
208
+ * if (decision.decision === "REQUIRE_APPROVAL") {
209
+ * console.log("Waiting for approval...");
210
+ *
211
+ * const result = await runplane.waitForApproval(decision.requestId, {
212
+ * timeoutMs: 600000, // 10 minutes
213
+ * onPoll: (status, elapsed) => {
214
+ * console.log(`Still waiting... ${elapsed}ms elapsed`);
215
+ * }
216
+ * });
217
+ *
218
+ * if (result.approved) {
219
+ * console.log("Approved by:", result.resolvedBy);
220
+ * } else {
221
+ * console.log("Denied:", result.comment);
222
+ * }
223
+ * }
224
+ * ```
225
+ */
226
+ async waitForApproval(requestId, options) {
227
+ return this.approvalPoller.poll(requestId, options);
228
+ }
229
+ /**
230
+ * Guard a function with containment evaluation.
231
+ *
232
+ * This is a convenience method that wraps decide() and waitForApproval()
233
+ * into a single call. If the action is blocked or denied, it throws.
234
+ * If approved, it executes the provided function.
235
+ *
236
+ * @param actionType - Type of action being performed
237
+ * @param target - Target resource
238
+ * @param context - Additional context
239
+ * @param fn - Function to execute if allowed
240
+ * @returns The result of fn()
241
+ * @throws RunplaneError if blocked, denied, or times out (in closed mode)
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const result = await runplane.guard(
246
+ * "payment_transfer",
247
+ * "external_bank",
248
+ * { amount: 50000, currency: "USD" },
249
+ * async () => {
250
+ * return await paymentService.transfer(amount, recipient);
251
+ * }
252
+ * );
253
+ * ```
254
+ */
255
+ async guard(actionType, target, context, fn) {
256
+ const response = await this.decide({
257
+ actionType,
258
+ target,
259
+ context: context ?? void 0
260
+ });
261
+ if (response.decision === "BLOCK") {
262
+ throw new RunplaneError(
263
+ `Action blocked: ${response.reason}`,
264
+ "BLOCKED",
265
+ response.requestId
266
+ );
267
+ }
268
+ if (response.decision === "ALLOW") {
269
+ return fn();
270
+ }
271
+ const approval = await this.waitForApproval(response.requestId);
272
+ if (approval.approved) {
273
+ return fn();
274
+ }
275
+ if (approval.status === "denied") {
276
+ throw new RunplaneError(
277
+ `Action denied: ${approval.comment || "No reason provided"}`,
278
+ "DENIED",
279
+ response.requestId
280
+ );
281
+ }
282
+ if (this.failMode === "open") {
283
+ return fn();
284
+ }
285
+ throw new RunplaneError(
286
+ "Approval timeout - action blocked (fail-closed)",
287
+ "TIMEOUT",
288
+ response.requestId
289
+ );
290
+ }
291
+ /**
292
+ * Handle API failures according to failMode
293
+ */
294
+ handleFailure(reason, request) {
295
+ const requestId = request.requestId ?? crypto.randomUUID();
296
+ if (this.failMode === "open") {
297
+ return {
298
+ decision: "ALLOW",
299
+ reason: `${reason} (fail-open)`,
300
+ requestId
301
+ };
302
+ }
303
+ return {
304
+ decision: "BLOCK",
305
+ reason: `${reason} (fail-closed)`,
306
+ requestId
307
+ };
308
+ }
309
+ };
310
+ export {
311
+ Runplane,
312
+ RunplaneError
313
+ };
package/package.json CHANGED
@@ -1 +1,59 @@
1
- {"name":"@runplane/runplane-sdk","version":"1.0.1","description":"Runplane AI Runtime Governance SDK","main":"dist/index.js","type":"commonjs","license":"MIT","files":["dist"]}
1
+ {
2
+ "name": "@runplane/runplane-sdk",
3
+ "version": "1.0.2",
4
+ "description": "Official SDK for the Runplane control plane - runtime governance for AI agent actions",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "lint": "eslint src/",
23
+ "test": "vitest",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "runplane",
28
+ "ai",
29
+ "agents",
30
+ "governance",
31
+ "security",
32
+ "containment",
33
+ "policy",
34
+ "llm",
35
+ "langchain",
36
+ "openai"
37
+ ],
38
+ "author": "Runplane <support@runplane.ai>",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/runplane/sdk.git"
43
+ },
44
+ "homepage": "https://runplane.ai/docs",
45
+ "bugs": {
46
+ "url": "https://github.com/runplane/sdk/issues"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^20.0.0",
53
+ "tsup": "^8.0.0",
54
+ "typescript": "^5.0.0",
55
+ "vitest": "^1.0.0"
56
+ }
57
+ }
58
+
59
+