@runplane/runplane-sdk 1.0.1 → 1.0.3

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,362 @@
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
+ const normalizedStatus = this.normalizeStatus(response);
77
+ if (onPoll) {
78
+ onPoll(normalizedStatus, elapsed);
79
+ }
80
+ if (normalizedStatus !== "pending") {
81
+ return {
82
+ approved: normalizedStatus === "approved",
83
+ status: normalizedStatus,
84
+ comment: response.comment || response.reason,
85
+ resolvedBy: response.resolvedBy || response.approvedBy
86
+ };
87
+ }
88
+ await this.sleep(interval);
89
+ interval = Math.min(interval * 1.5, this.maxPollIntervalMs);
90
+ } catch {
91
+ await this.sleep(interval);
92
+ interval = Math.min(interval * 2, this.maxPollIntervalMs);
93
+ }
94
+ }
95
+ return {
96
+ approved: false,
97
+ status: "expired"
98
+ };
99
+ }
100
+ /**
101
+ * Fetch current approval status from the API
102
+ */
103
+ async fetchApprovalStatus(requestId) {
104
+ const response = await fetch(
105
+ `${this.baseUrl}/api/approvals/poll/${requestId}`,
106
+ {
107
+ headers: {
108
+ Authorization: `Bearer ${this.apiKey}`
109
+ }
110
+ }
111
+ );
112
+ if (!response.ok) {
113
+ throw new Error(`Failed to fetch approval status: ${response.statusText}`);
114
+ }
115
+ return response.json();
116
+ }
117
+ /**
118
+ * Normalize server response → SDK canonical format
119
+ */
120
+ normalizeStatus(response) {
121
+ const raw = response.status || response.decision || response.decisionOutcome || "";
122
+ const normalized = String(raw).toLowerCase();
123
+ if (normalized === "approved" || normalized === "allow") {
124
+ return "approved";
125
+ }
126
+ if (normalized === "denied" || normalized === "deny" || normalized === "block") {
127
+ return "denied";
128
+ }
129
+ if (normalized === "pending") {
130
+ return "pending";
131
+ }
132
+ if (normalized === "expired") {
133
+ return "expired";
134
+ }
135
+ return "pending";
136
+ }
137
+ sleep(ms) {
138
+ return new Promise((resolve) => setTimeout(resolve, ms));
139
+ }
140
+ };
141
+
142
+ // src/client.ts
143
+ var DEFAULT_BASE_URL = "https://runplane.ai";
144
+ var DEFAULT_TIMEOUT_MS = 3e3;
145
+ var DEFAULT_FAIL_MODE = "closed";
146
+ var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
147
+ var DEFAULT_APPROVAL_POLL_INTERVAL_MS = 2e3;
148
+ var Runplane = class {
149
+ constructor(config) {
150
+ if (!config.apiKey) {
151
+ throw new RunplaneError(
152
+ "API key is required",
153
+ "INVALID_CONFIG"
154
+ );
155
+ }
156
+ this.apiKey = config.apiKey;
157
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
158
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
159
+ this.failMode = config.failMode ?? DEFAULT_FAIL_MODE;
160
+ this.approvalPoller = new ApprovalPoller({
161
+ baseUrl: this.baseUrl,
162
+ apiKey: this.apiKey,
163
+ timeoutMs: config.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
164
+ pollIntervalMs: config.approvalPollIntervalMs ?? DEFAULT_APPROVAL_POLL_INTERVAL_MS
165
+ });
166
+ }
167
+ /**
168
+ * Request a decision from the Runplane control plane.
169
+ *
170
+ * Call this before executing any sensitive action. The response will indicate
171
+ * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
172
+ *
173
+ * @param request - The action details to evaluate
174
+ * @returns The decision response
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * const decision = await runplane.decide({
179
+ * actionType: "delete_record",
180
+ * target: "users.prod",
181
+ * context: { recordId: "usr_123", reason: "gdpr_request" }
182
+ * });
183
+ *
184
+ * switch (decision.decision) {
185
+ * case "ALLOW":
186
+ * await deleteRecord(recordId);
187
+ * break;
188
+ * case "BLOCK":
189
+ * console.error("Action blocked:", decision.reason);
190
+ * break;
191
+ * case "REQUIRE_APPROVAL":
192
+ * // Wait for human approval
193
+ * const result = await runplane.waitForApproval(decision.requestId);
194
+ * if (result.approved) {
195
+ * await deleteRecord(recordId);
196
+ * }
197
+ * break;
198
+ * }
199
+ * ```
200
+ */
201
+ async decide(request) {
202
+ const controller = new AbortController();
203
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
204
+ try {
205
+ const response = await fetch(`${this.baseUrl}/api/decide`, {
206
+ method: "POST",
207
+ headers: {
208
+ "Content-Type": "application/json",
209
+ "Authorization": `Bearer ${this.apiKey}`
210
+ },
211
+ body: JSON.stringify({
212
+ agentKey: this.apiKey,
213
+ actionType: request.actionType,
214
+ target: request.target,
215
+ context: request.context ?? {},
216
+ requestId: request.requestId
217
+ }),
218
+ signal: controller.signal
219
+ });
220
+ const json = await response.json();
221
+ if (!response.ok) {
222
+ return this.handleFailure(
223
+ `API error: ${json.error ?? response.statusText}`,
224
+ request
225
+ );
226
+ }
227
+ return {
228
+ decision: json.decision,
229
+ reason: json.reason,
230
+ requestId: json.requestId,
231
+ matchedRule: json.matchedRule,
232
+ riskScore: json.riskScore,
233
+ severity: json.severity
234
+ };
235
+ } catch (error) {
236
+ const message = error instanceof Error ? error.message : "Unknown error";
237
+ return this.handleFailure(`Network failure: ${message}`, request);
238
+ } finally {
239
+ clearTimeout(timeout);
240
+ }
241
+ }
242
+ /**
243
+ * Wait for an approval decision on a pending request.
244
+ *
245
+ * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
246
+ * the approval endpoint until the request is approved, denied, or times out.
247
+ *
248
+ * @param requestId - The requestId from the decide() response
249
+ * @param options - Optional configuration for polling behavior
250
+ * @returns The approval result
251
+ *
252
+ * @example
253
+ * ```typescript
254
+ * const decision = await runplane.decide({ ... });
255
+ *
256
+ * if (decision.decision === "REQUIRE_APPROVAL") {
257
+ * console.log("Waiting for approval...");
258
+ *
259
+ * const result = await runplane.waitForApproval(decision.requestId, {
260
+ * timeoutMs: 600000, // 10 minutes
261
+ * onPoll: (status, elapsed) => {
262
+ * console.log(`Still waiting... ${elapsed}ms elapsed`);
263
+ * }
264
+ * });
265
+ *
266
+ * if (result.approved) {
267
+ * console.log("Approved by:", result.resolvedBy);
268
+ * } else {
269
+ * console.log("Denied:", result.comment);
270
+ * }
271
+ * }
272
+ * ```
273
+ */
274
+ async waitForApproval(requestId, options) {
275
+ return this.approvalPoller.poll(requestId, options);
276
+ }
277
+ /**
278
+ * Guard a function with containment evaluation.
279
+ *
280
+ * This is a convenience method that wraps decide() and waitForApproval()
281
+ * into a single call. If the action is blocked or denied, it throws.
282
+ * If approved, it executes the provided function.
283
+ *
284
+ * @param actionType - Type of action being performed
285
+ * @param target - Target resource
286
+ * @param context - Additional context
287
+ * @param fn - Function to execute if allowed
288
+ * @returns The result of fn()
289
+ * @throws RunplaneError if blocked, denied, or times out (in closed mode)
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * const result = await runplane.guard(
294
+ * "payment_transfer",
295
+ * "external_bank",
296
+ * { amount: 50000, currency: "USD" },
297
+ * async () => {
298
+ * return await paymentService.transfer(amount, recipient);
299
+ * }
300
+ * );
301
+ * ```
302
+ */
303
+ async guard(actionType, target, context, fn) {
304
+ const response = await this.decide({
305
+ actionType,
306
+ target,
307
+ context: context ?? void 0
308
+ });
309
+ if (response.decision === "BLOCK") {
310
+ throw new RunplaneError(
311
+ `Action blocked: ${response.reason}`,
312
+ "BLOCKED",
313
+ response.requestId
314
+ );
315
+ }
316
+ if (response.decision === "ALLOW") {
317
+ return fn();
318
+ }
319
+ const approval = await this.waitForApproval(response.requestId);
320
+ if (approval.approved) {
321
+ return fn();
322
+ }
323
+ if (approval.status === "denied") {
324
+ throw new RunplaneError(
325
+ `Action denied: ${approval.comment || "No reason provided"}`,
326
+ "DENIED",
327
+ response.requestId
328
+ );
329
+ }
330
+ if (this.failMode === "open") {
331
+ return fn();
332
+ }
333
+ throw new RunplaneError(
334
+ "Approval timeout - action blocked (fail-closed)",
335
+ "TIMEOUT",
336
+ response.requestId
337
+ );
338
+ }
339
+ /**
340
+ * Handle API failures according to failMode
341
+ */
342
+ handleFailure(reason, request) {
343
+ const requestId = request.requestId ?? crypto.randomUUID();
344
+ if (this.failMode === "open") {
345
+ return {
346
+ decision: "ALLOW",
347
+ reason: `${reason} (fail-open)`,
348
+ requestId
349
+ };
350
+ }
351
+ return {
352
+ decision: "BLOCK",
353
+ reason: `${reason} (fail-closed)`,
354
+ requestId
355
+ };
356
+ }
357
+ };
358
+ // Annotate the CommonJS export names for ESM import in node:
359
+ 0 && (module.exports = {
360
+ Runplane,
361
+ RunplaneError
362
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,334 @@
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
+ const normalizedStatus = this.normalizeStatus(response);
50
+ if (onPoll) {
51
+ onPoll(normalizedStatus, elapsed);
52
+ }
53
+ if (normalizedStatus !== "pending") {
54
+ return {
55
+ approved: normalizedStatus === "approved",
56
+ status: normalizedStatus,
57
+ comment: response.comment || response.reason,
58
+ resolvedBy: response.resolvedBy || response.approvedBy
59
+ };
60
+ }
61
+ await this.sleep(interval);
62
+ interval = Math.min(interval * 1.5, this.maxPollIntervalMs);
63
+ } catch {
64
+ await this.sleep(interval);
65
+ interval = Math.min(interval * 2, this.maxPollIntervalMs);
66
+ }
67
+ }
68
+ return {
69
+ approved: false,
70
+ status: "expired"
71
+ };
72
+ }
73
+ /**
74
+ * Fetch current approval status from the API
75
+ */
76
+ async fetchApprovalStatus(requestId) {
77
+ const response = await fetch(
78
+ `${this.baseUrl}/api/approvals/poll/${requestId}`,
79
+ {
80
+ headers: {
81
+ Authorization: `Bearer ${this.apiKey}`
82
+ }
83
+ }
84
+ );
85
+ if (!response.ok) {
86
+ throw new Error(`Failed to fetch approval status: ${response.statusText}`);
87
+ }
88
+ return response.json();
89
+ }
90
+ /**
91
+ * Normalize server response → SDK canonical format
92
+ */
93
+ normalizeStatus(response) {
94
+ const raw = response.status || response.decision || response.decisionOutcome || "";
95
+ const normalized = String(raw).toLowerCase();
96
+ if (normalized === "approved" || normalized === "allow") {
97
+ return "approved";
98
+ }
99
+ if (normalized === "denied" || normalized === "deny" || normalized === "block") {
100
+ return "denied";
101
+ }
102
+ if (normalized === "pending") {
103
+ return "pending";
104
+ }
105
+ if (normalized === "expired") {
106
+ return "expired";
107
+ }
108
+ return "pending";
109
+ }
110
+ sleep(ms) {
111
+ return new Promise((resolve) => setTimeout(resolve, ms));
112
+ }
113
+ };
114
+
115
+ // src/client.ts
116
+ var DEFAULT_BASE_URL = "https://runplane.ai";
117
+ var DEFAULT_TIMEOUT_MS = 3e3;
118
+ var DEFAULT_FAIL_MODE = "closed";
119
+ var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
120
+ var DEFAULT_APPROVAL_POLL_INTERVAL_MS = 2e3;
121
+ var Runplane = class {
122
+ constructor(config) {
123
+ if (!config.apiKey) {
124
+ throw new RunplaneError(
125
+ "API key is required",
126
+ "INVALID_CONFIG"
127
+ );
128
+ }
129
+ this.apiKey = config.apiKey;
130
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
131
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
132
+ this.failMode = config.failMode ?? DEFAULT_FAIL_MODE;
133
+ this.approvalPoller = new ApprovalPoller({
134
+ baseUrl: this.baseUrl,
135
+ apiKey: this.apiKey,
136
+ timeoutMs: config.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS,
137
+ pollIntervalMs: config.approvalPollIntervalMs ?? DEFAULT_APPROVAL_POLL_INTERVAL_MS
138
+ });
139
+ }
140
+ /**
141
+ * Request a decision from the Runplane control plane.
142
+ *
143
+ * Call this before executing any sensitive action. The response will indicate
144
+ * whether the action should be ALLOWED, BLOCKED, or REQUIRE_APPROVAL.
145
+ *
146
+ * @param request - The action details to evaluate
147
+ * @returns The decision response
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const decision = await runplane.decide({
152
+ * actionType: "delete_record",
153
+ * target: "users.prod",
154
+ * context: { recordId: "usr_123", reason: "gdpr_request" }
155
+ * });
156
+ *
157
+ * switch (decision.decision) {
158
+ * case "ALLOW":
159
+ * await deleteRecord(recordId);
160
+ * break;
161
+ * case "BLOCK":
162
+ * console.error("Action blocked:", decision.reason);
163
+ * break;
164
+ * case "REQUIRE_APPROVAL":
165
+ * // Wait for human approval
166
+ * const result = await runplane.waitForApproval(decision.requestId);
167
+ * if (result.approved) {
168
+ * await deleteRecord(recordId);
169
+ * }
170
+ * break;
171
+ * }
172
+ * ```
173
+ */
174
+ async decide(request) {
175
+ const controller = new AbortController();
176
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
177
+ try {
178
+ const response = await fetch(`${this.baseUrl}/api/decide`, {
179
+ method: "POST",
180
+ headers: {
181
+ "Content-Type": "application/json",
182
+ "Authorization": `Bearer ${this.apiKey}`
183
+ },
184
+ body: JSON.stringify({
185
+ agentKey: this.apiKey,
186
+ actionType: request.actionType,
187
+ target: request.target,
188
+ context: request.context ?? {},
189
+ requestId: request.requestId
190
+ }),
191
+ signal: controller.signal
192
+ });
193
+ const json = await response.json();
194
+ if (!response.ok) {
195
+ return this.handleFailure(
196
+ `API error: ${json.error ?? response.statusText}`,
197
+ request
198
+ );
199
+ }
200
+ return {
201
+ decision: json.decision,
202
+ reason: json.reason,
203
+ requestId: json.requestId,
204
+ matchedRule: json.matchedRule,
205
+ riskScore: json.riskScore,
206
+ severity: json.severity
207
+ };
208
+ } catch (error) {
209
+ const message = error instanceof Error ? error.message : "Unknown error";
210
+ return this.handleFailure(`Network failure: ${message}`, request);
211
+ } finally {
212
+ clearTimeout(timeout);
213
+ }
214
+ }
215
+ /**
216
+ * Wait for an approval decision on a pending request.
217
+ *
218
+ * Use this when decide() returns REQUIRE_APPROVAL. This method will poll
219
+ * the approval endpoint until the request is approved, denied, or times out.
220
+ *
221
+ * @param requestId - The requestId from the decide() response
222
+ * @param options - Optional configuration for polling behavior
223
+ * @returns The approval result
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * const decision = await runplane.decide({ ... });
228
+ *
229
+ * if (decision.decision === "REQUIRE_APPROVAL") {
230
+ * console.log("Waiting for approval...");
231
+ *
232
+ * const result = await runplane.waitForApproval(decision.requestId, {
233
+ * timeoutMs: 600000, // 10 minutes
234
+ * onPoll: (status, elapsed) => {
235
+ * console.log(`Still waiting... ${elapsed}ms elapsed`);
236
+ * }
237
+ * });
238
+ *
239
+ * if (result.approved) {
240
+ * console.log("Approved by:", result.resolvedBy);
241
+ * } else {
242
+ * console.log("Denied:", result.comment);
243
+ * }
244
+ * }
245
+ * ```
246
+ */
247
+ async waitForApproval(requestId, options) {
248
+ return this.approvalPoller.poll(requestId, options);
249
+ }
250
+ /**
251
+ * Guard a function with containment evaluation.
252
+ *
253
+ * This is a convenience method that wraps decide() and waitForApproval()
254
+ * into a single call. If the action is blocked or denied, it throws.
255
+ * If approved, it executes the provided function.
256
+ *
257
+ * @param actionType - Type of action being performed
258
+ * @param target - Target resource
259
+ * @param context - Additional context
260
+ * @param fn - Function to execute if allowed
261
+ * @returns The result of fn()
262
+ * @throws RunplaneError if blocked, denied, or times out (in closed mode)
263
+ *
264
+ * @example
265
+ * ```typescript
266
+ * const result = await runplane.guard(
267
+ * "payment_transfer",
268
+ * "external_bank",
269
+ * { amount: 50000, currency: "USD" },
270
+ * async () => {
271
+ * return await paymentService.transfer(amount, recipient);
272
+ * }
273
+ * );
274
+ * ```
275
+ */
276
+ async guard(actionType, target, context, fn) {
277
+ const response = await this.decide({
278
+ actionType,
279
+ target,
280
+ context: context ?? void 0
281
+ });
282
+ if (response.decision === "BLOCK") {
283
+ throw new RunplaneError(
284
+ `Action blocked: ${response.reason}`,
285
+ "BLOCKED",
286
+ response.requestId
287
+ );
288
+ }
289
+ if (response.decision === "ALLOW") {
290
+ return fn();
291
+ }
292
+ const approval = await this.waitForApproval(response.requestId);
293
+ if (approval.approved) {
294
+ return fn();
295
+ }
296
+ if (approval.status === "denied") {
297
+ throw new RunplaneError(
298
+ `Action denied: ${approval.comment || "No reason provided"}`,
299
+ "DENIED",
300
+ response.requestId
301
+ );
302
+ }
303
+ if (this.failMode === "open") {
304
+ return fn();
305
+ }
306
+ throw new RunplaneError(
307
+ "Approval timeout - action blocked (fail-closed)",
308
+ "TIMEOUT",
309
+ response.requestId
310
+ );
311
+ }
312
+ /**
313
+ * Handle API failures according to failMode
314
+ */
315
+ handleFailure(reason, request) {
316
+ const requestId = request.requestId ?? crypto.randomUUID();
317
+ if (this.failMode === "open") {
318
+ return {
319
+ decision: "ALLOW",
320
+ reason: `${reason} (fail-open)`,
321
+ requestId
322
+ };
323
+ }
324
+ return {
325
+ decision: "BLOCK",
326
+ reason: `${reason} (fail-closed)`,
327
+ requestId
328
+ };
329
+ }
330
+ };
331
+ export {
332
+ Runplane,
333
+ RunplaneError
334
+ };
package/package.json CHANGED
@@ -1 +1,57 @@
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.3",
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
+ }