@shardflux/sdk 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +162 -0
- package/dist/audit.d.ts +74 -0
- package/dist/audit.js +49 -0
- package/dist/cell.d.ts +195 -0
- package/dist/cell.js +408 -0
- package/dist/client.d.ts +212 -0
- package/dist/client.js +238 -0
- package/dist/egress.d.ts +152 -0
- package/dist/egress.js +37 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.js +73 -0
- package/dist/generated/app-api.d.ts +23479 -0
- package/dist/generated/app-api.js +5 -0
- package/dist/generated/cell-api.d.ts +1832 -0
- package/dist/generated/cell-api.js +5 -0
- package/dist/http.d.ts +39 -0
- package/dist/http.js +121 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +13 -0
- package/dist/secrets.d.ts +100 -0
- package/dist/secrets.js +56 -0
- package/dist/templates.d.ts +328 -0
- package/dist/templates.js +117 -0
- package/dist/tokens.d.ts +35 -0
- package/dist/tokens.js +61 -0
- package/dist/tools.d.ts +91 -0
- package/dist/tools.js +313 -0
- package/dist/usage.d.ts +60 -0
- package/dist/usage.js +45 -0
- package/dist/volumes.d.ts +108 -0
- package/dist/volumes.js +120 -0
- package/dist/workspace.d.ts +70 -0
- package/dist/workspace.js +113 -0
- package/package.json +56 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shardflux SDK entry point (TEMPLATES.md "Developer interface"):
|
|
3
|
+
*
|
|
4
|
+
* const cloud = new Shardflux({ apiKey, baseUrl });
|
|
5
|
+
* const workspace = await cloud.workspaces.open({ key, template });
|
|
6
|
+
* await agent.run({ input, tools: workspaceTools(workspace) });
|
|
7
|
+
*
|
|
8
|
+
* Types are generated from the application API and cell gateway OpenAPI documents.
|
|
9
|
+
*/
|
|
10
|
+
import type { components, operations } from './generated/app-api.js';
|
|
11
|
+
import { ShardfluxApiError } from './errors.js';
|
|
12
|
+
import { HttpClient } from './http.js';
|
|
13
|
+
import type { ToolName } from './tokens.js';
|
|
14
|
+
import { Workspace } from './workspace.js';
|
|
15
|
+
import { AuditApi } from './audit.js';
|
|
16
|
+
import { EgressPolicyApi } from './egress.js';
|
|
17
|
+
import { SecretsApi } from './secrets.js';
|
|
18
|
+
import { TemplatesApi } from './templates.js';
|
|
19
|
+
import { UsageApi } from './usage.js';
|
|
20
|
+
import { VolumesApi } from './volumes.js';
|
|
21
|
+
export type WorkspaceView = components['schemas']['Workspace'];
|
|
22
|
+
export type Operation = components['schemas']['Operation'];
|
|
23
|
+
type JsonOf<R> = R extends {
|
|
24
|
+
content: {
|
|
25
|
+
'application/json': infer T;
|
|
26
|
+
};
|
|
27
|
+
} ? T : never;
|
|
28
|
+
type Ok<Op> = Op extends {
|
|
29
|
+
responses: infer R;
|
|
30
|
+
} ? {
|
|
31
|
+
[K in keyof R]: K extends 200 | 201 | 202 ? JsonOf<R[K]> : never;
|
|
32
|
+
}[keyof R] : never;
|
|
33
|
+
export type OpenResponse = Ok<operations['postV1WorkspacesOpen']>;
|
|
34
|
+
export type AgentSession = Ok<operations['getV1WorkspacesWorkspaceIdAgentSessions']>['data'][number];
|
|
35
|
+
export type Entitlements = Ok<operations['getV1OrganizationsOrganizationIdEntitlements']>;
|
|
36
|
+
export type Me = Ok<operations['getV1Me']>;
|
|
37
|
+
/** The versioned plan catalog admission enforces (public). */
|
|
38
|
+
export type BillingCatalog = Ok<operations['getV1BillingCatalog']>;
|
|
39
|
+
/** Billing state, subscription and effective plan of an organization. */
|
|
40
|
+
export type BillingSubscription = Ok<operations['getV1OrganizationsOrganizationIdBillingSubscription']>;
|
|
41
|
+
/** Browser (session) surface, for first-party frontends: Checkout intent, portal link and invoices. */
|
|
42
|
+
export type CheckoutSession = Ok<operations['postApiV1OrganizationsOrganizationIdBillingCheckoutSessions']>;
|
|
43
|
+
export type PortalSession = Ok<operations['postApiV1OrganizationsOrganizationIdBillingPortalSessions']>;
|
|
44
|
+
export type InvoicePage = Ok<operations['getApiV1OrganizationsOrganizationIdBillingInvoices']>;
|
|
45
|
+
export type Invoice = InvoicePage['data'][number];
|
|
46
|
+
export interface ShardfluxOptions {
|
|
47
|
+
/** Project API key: sfk_<key_id>_<secret>. */
|
|
48
|
+
apiKey: string;
|
|
49
|
+
/** Default https://api.shardflux.dev (override with `baseUrl`). */
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
fetch?: typeof fetch;
|
|
52
|
+
userAgent?: string;
|
|
53
|
+
/** Per-request timeout (ms), default 30 s. */
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
/** Retries of safe/idempotent requests on transient failures, default 2. */
|
|
56
|
+
maxRetries?: number;
|
|
57
|
+
/** Injected for tests; defaults to setTimeout. */
|
|
58
|
+
sleep?: (ms: number) => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export interface Caps {
|
|
61
|
+
cpu_millis?: number;
|
|
62
|
+
memory_mib?: number;
|
|
63
|
+
disk_gib?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface WaitOptions {
|
|
66
|
+
/** Give up waiting after this long (default 300 000 ms); the operation continues server side. */
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
/** First poll delay (default 250 ms); doubles up to `maxPollIntervalMs` with jitter. */
|
|
69
|
+
pollIntervalMs?: number;
|
|
70
|
+
maxPollIntervalMs?: number;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}
|
|
73
|
+
export interface OpenParams {
|
|
74
|
+
key: string;
|
|
75
|
+
template: string;
|
|
76
|
+
caps?: Caps;
|
|
77
|
+
/** Browser/session principals only; API keys use their own project. */
|
|
78
|
+
projectId?: string;
|
|
79
|
+
/** Attribution label for tool tokens obtained through this workspace object. */
|
|
80
|
+
agentLabel?: string;
|
|
81
|
+
/** Tools to request in tool tokens (subset of the key's permissions); default all permitted. */
|
|
82
|
+
tools?: ToolName[];
|
|
83
|
+
/** `false`: return immediately (possibly not ready). Default: wait until ready. */
|
|
84
|
+
wait?: false | WaitOptions;
|
|
85
|
+
/** Defaults to a fresh key per open() call so transport retries replay instead of duplicating. */
|
|
86
|
+
idempotencyKey?: string;
|
|
87
|
+
}
|
|
88
|
+
export interface ListParams {
|
|
89
|
+
state?: WorkspaceView['observed_state'];
|
|
90
|
+
desiredState?: WorkspaceView['desired_state'];
|
|
91
|
+
keyPrefix?: string;
|
|
92
|
+
projectId?: string;
|
|
93
|
+
organizationId?: string;
|
|
94
|
+
includeDeleted?: boolean;
|
|
95
|
+
limit?: number;
|
|
96
|
+
cursor?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface Page<T> {
|
|
99
|
+
data: T[];
|
|
100
|
+
nextCursor: string | null;
|
|
101
|
+
}
|
|
102
|
+
/** Internal context shared with Workspace objects. */
|
|
103
|
+
export interface ClientContext {
|
|
104
|
+
http: HttpClient;
|
|
105
|
+
authorization: string;
|
|
106
|
+
fetch: typeof fetch;
|
|
107
|
+
userAgent: string;
|
|
108
|
+
sleep: (ms: number) => Promise<void>;
|
|
109
|
+
workspaces: WorkspacesApi;
|
|
110
|
+
}
|
|
111
|
+
export declare class WorkspacesApi {
|
|
112
|
+
#private;
|
|
113
|
+
constructor(ctx: () => ClientContext);
|
|
114
|
+
/**
|
|
115
|
+
* Opens a workspace by key: creates it from the template's latest published version on first
|
|
116
|
+
* use, reconnects (or resumes) afterwards; never resets an existing workspace. Waits until it is
|
|
117
|
+
* ready unless `wait: false`; on timeout throws OperationTimeoutError carrying the operation id.
|
|
118
|
+
*/
|
|
119
|
+
open(params: OpenParams): Promise<Workspace>;
|
|
120
|
+
/**
|
|
121
|
+
* Polls GET /v1/operations/{id} with bounded exponential backoff (+-20 % jitter) until it
|
|
122
|
+
* succeeds (resolves), fails or is canceled (OperationFailedError), or `timeoutMs` passes
|
|
123
|
+
* (OperationTimeoutError; the operation keeps running and can be awaited again).
|
|
124
|
+
*/
|
|
125
|
+
waitForOperation(operationId: string, opts?: WaitOptions): Promise<Operation>;
|
|
126
|
+
/** One operation (GET /v1/operations/{id}); lifecycle operations stay pollable after a workspace is deleted. */
|
|
127
|
+
getOperation(operationId: string, opts?: {
|
|
128
|
+
signal?: AbortSignal;
|
|
129
|
+
}): Promise<Operation>;
|
|
130
|
+
get(workspaceId: string, opts?: {
|
|
131
|
+
agentLabel?: string;
|
|
132
|
+
tools?: ToolName[];
|
|
133
|
+
}): Promise<Workspace>;
|
|
134
|
+
list(params?: ListParams): Promise<Page<Workspace>>;
|
|
135
|
+
/** Iterates every page. */
|
|
136
|
+
listAll(params?: Omit<ListParams, 'cursor'>): AsyncGenerator<Workspace>;
|
|
137
|
+
/** Tombstones the workspace now (tool access revoked); storage cleanup happens asynchronously. */
|
|
138
|
+
delete(workspaceId: string, opts?: {
|
|
139
|
+
idempotencyKey?: string;
|
|
140
|
+
}): Promise<Operation>;
|
|
141
|
+
suspend(workspaceId: string, opts?: {
|
|
142
|
+
idempotencyKey?: string;
|
|
143
|
+
}): Promise<Operation>;
|
|
144
|
+
resume(workspaceId: string, opts?: {
|
|
145
|
+
idempotencyKey?: string;
|
|
146
|
+
}): Promise<Operation>;
|
|
147
|
+
snapshot(workspaceId: string, opts?: {
|
|
148
|
+
label?: string;
|
|
149
|
+
idempotencyKey?: string;
|
|
150
|
+
}): Promise<Operation>;
|
|
151
|
+
fork(workspaceId: string, target: {
|
|
152
|
+
key: string;
|
|
153
|
+
caps?: Caps;
|
|
154
|
+
}, opts?: {
|
|
155
|
+
idempotencyKey?: string;
|
|
156
|
+
}): Promise<{
|
|
157
|
+
operation: Operation;
|
|
158
|
+
workspace: Workspace;
|
|
159
|
+
}>;
|
|
160
|
+
operations(workspaceId: string, params?: {
|
|
161
|
+
limit?: number;
|
|
162
|
+
cursor?: string;
|
|
163
|
+
state?: Operation['state'];
|
|
164
|
+
kind?: Operation['kind'];
|
|
165
|
+
}): Promise<Page<Operation>>;
|
|
166
|
+
agentSessions(workspaceId: string, params?: {
|
|
167
|
+
limit?: number;
|
|
168
|
+
cursor?: string;
|
|
169
|
+
}): Promise<Page<AgentSession>>;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Read-only billing for API keys: the plan catalog and the organization's subscription/billing
|
|
173
|
+
* state. Purchases and the Stripe portal are owner/billing-member actions in the browser
|
|
174
|
+
* (session) surface; an open or resume refused after a payment grace period throws
|
|
175
|
+
* ShardfluxApiError { code: 'entitlement_required', details.reason: 'payment_past_due' | 'unpaid' }.
|
|
176
|
+
*/
|
|
177
|
+
export declare class BillingApi {
|
|
178
|
+
#private;
|
|
179
|
+
constructor(ctx: () => ClientContext);
|
|
180
|
+
catalog(): Promise<BillingCatalog>;
|
|
181
|
+
subscription(organizationId: string): Promise<BillingSubscription>;
|
|
182
|
+
}
|
|
183
|
+
/** The public plan catalog without an API key (pricing pages, CLIs before sign-up). */
|
|
184
|
+
export declare function fetchBillingCatalog(opts?: {
|
|
185
|
+
baseUrl?: string;
|
|
186
|
+
fetch?: typeof fetch;
|
|
187
|
+
timeoutMs?: number;
|
|
188
|
+
}): Promise<BillingCatalog>;
|
|
189
|
+
export declare class Shardflux {
|
|
190
|
+
#private;
|
|
191
|
+
readonly workspaces: WorkspacesApi;
|
|
192
|
+
readonly billing: BillingApi;
|
|
193
|
+
/** Usage, allowances, estimates, grants/leases and spend (Phase 9). */
|
|
194
|
+
readonly usage: UsageApi;
|
|
195
|
+
/** Template registry and custom template builds. */
|
|
196
|
+
readonly templates: TemplatesApi;
|
|
197
|
+
/** Customer secrets (metadata only; values are write-only and resolved by the cell at session start). */
|
|
198
|
+
readonly secrets: SecretsApi;
|
|
199
|
+
/** Project/workspace outbound allowlists (stored and versioned; host enforcement state is reported). */
|
|
200
|
+
readonly egress: EgressPolicyApi;
|
|
201
|
+
/** Organization audit trail (owner/admin; API keys are refused with 403). */
|
|
202
|
+
readonly audit: AuditApi;
|
|
203
|
+
/** Shared volumes: persistent storage attached to workspaces at a mount path (contracts §15). */
|
|
204
|
+
readonly volumes: VolumesApi;
|
|
205
|
+
constructor(opts: ShardfluxOptions);
|
|
206
|
+
/** The authenticated principal (the API key, its organization and project). */
|
|
207
|
+
me(): Promise<Me>;
|
|
208
|
+
entitlements(organizationId: string): Promise<Entitlements>;
|
|
209
|
+
/** Raw access to any /v1 endpoint with the SDK's authentication and error handling. */
|
|
210
|
+
request<T>(method: string, path: string, init?: Parameters<HttpClient['json']>[2]): Promise<T>;
|
|
211
|
+
}
|
|
212
|
+
export { ShardfluxApiError };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { OperationFailedError, OperationTimeoutError, ShardfluxApiError } from "./errors.js";
|
|
2
|
+
import { HttpClient, SDK_VERSION, defaultSleep, randomId } from "./http.js";
|
|
3
|
+
import { Workspace } from "./workspace.js";
|
|
4
|
+
import { AuditApi } from "./audit.js";
|
|
5
|
+
import { EgressPolicyApi } from "./egress.js";
|
|
6
|
+
import { SecretsApi } from "./secrets.js";
|
|
7
|
+
import { TemplatesApi } from "./templates.js";
|
|
8
|
+
import { UsageApi } from "./usage.js";
|
|
9
|
+
import { VolumesApi } from "./volumes.js";
|
|
10
|
+
const TERMINAL = new Set(['succeeded', 'failed', 'canceled']);
|
|
11
|
+
/** Races the injected sleep against the signal (the injected sleep itself may not be abortable). */
|
|
12
|
+
function abortableSleep(sleep, ms, signal) {
|
|
13
|
+
if (signal.aborted)
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const done = () => {
|
|
17
|
+
signal.removeEventListener('abort', done);
|
|
18
|
+
resolve();
|
|
19
|
+
};
|
|
20
|
+
signal.addEventListener('abort', done, { once: true });
|
|
21
|
+
void sleep(ms).then(done, done);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export class WorkspacesApi {
|
|
25
|
+
#ctx;
|
|
26
|
+
constructor(ctx) {
|
|
27
|
+
this.#ctx = ctx;
|
|
28
|
+
}
|
|
29
|
+
get #http() {
|
|
30
|
+
return this.#ctx().http;
|
|
31
|
+
}
|
|
32
|
+
get #auth() {
|
|
33
|
+
return this.#ctx().authorization;
|
|
34
|
+
}
|
|
35
|
+
#wrap(view, opts = {}) {
|
|
36
|
+
return new Workspace(this.#ctx(), view, opts);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Opens a workspace by key: creates it from the template's latest published version on first
|
|
40
|
+
* use, reconnects (or resumes) afterwards; never resets an existing workspace. Waits until it is
|
|
41
|
+
* ready unless `wait: false`; on timeout throws OperationTimeoutError carrying the operation id.
|
|
42
|
+
*/
|
|
43
|
+
async open(params) {
|
|
44
|
+
const body = { key: params.key, template: params.template };
|
|
45
|
+
if (params.caps !== undefined)
|
|
46
|
+
body.caps = params.caps;
|
|
47
|
+
if (params.projectId !== undefined)
|
|
48
|
+
body.project_id = params.projectId;
|
|
49
|
+
if (params.agentLabel !== undefined)
|
|
50
|
+
body.agent_label = params.agentLabel;
|
|
51
|
+
if (params.tools !== undefined)
|
|
52
|
+
body.tools = params.tools;
|
|
53
|
+
const res = await this.#http.jsonWithStatus('POST', '/v1/workspaces/open', { json: body, idempotencyKey: params.idempotencyKey ?? randomId('open-') }, this.#auth);
|
|
54
|
+
const wrapOpts = { agentLabel: params.agentLabel, tools: params.tools, token: res.body.tool_token };
|
|
55
|
+
if (res.status === 200 || params.wait === false || res.body.operation === null)
|
|
56
|
+
return this.#wrap(res.body.workspace, wrapOpts);
|
|
57
|
+
await this.waitForOperation(res.body.operation.id, params.wait ?? {});
|
|
58
|
+
return this.#wrap(await this.#getView(res.body.workspace.id), { agentLabel: params.agentLabel, tools: params.tools });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Polls GET /v1/operations/{id} with bounded exponential backoff (+-20 % jitter) until it
|
|
62
|
+
* succeeds (resolves), fails or is canceled (OperationFailedError), or `timeoutMs` passes
|
|
63
|
+
* (OperationTimeoutError; the operation keeps running and can be awaited again).
|
|
64
|
+
*/
|
|
65
|
+
async waitForOperation(operationId, opts = {}) {
|
|
66
|
+
const { sleep } = this.#ctx();
|
|
67
|
+
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
68
|
+
const maxInterval = opts.maxPollIntervalMs ?? 5_000;
|
|
69
|
+
let interval = opts.pollIntervalMs ?? 250;
|
|
70
|
+
const started = Date.now();
|
|
71
|
+
const aborted = () => (opts.signal?.reason instanceof Error ? opts.signal.reason : new Error('aborted'));
|
|
72
|
+
for (;;) {
|
|
73
|
+
if (opts.signal?.aborted)
|
|
74
|
+
throw aborted();
|
|
75
|
+
const operation = await this.getOperation(operationId, opts.signal ? { signal: opts.signal } : {});
|
|
76
|
+
if (operation.state === 'succeeded')
|
|
77
|
+
return operation;
|
|
78
|
+
if (TERMINAL.has(operation.state))
|
|
79
|
+
throw new OperationFailedError(operation);
|
|
80
|
+
const waited = Date.now() - started;
|
|
81
|
+
if (waited >= timeoutMs)
|
|
82
|
+
throw new OperationTimeoutError(operation, waited);
|
|
83
|
+
const jitter = interval * 0.2 * (Math.random() * 2 - 1);
|
|
84
|
+
const delay = Math.max(10, Math.min(interval + jitter, timeoutMs - waited));
|
|
85
|
+
// The wait itself is abortable: a signal ends it at once instead of after the sleep.
|
|
86
|
+
await (opts.signal ? abortableSleep(sleep, delay, opts.signal) : sleep(delay));
|
|
87
|
+
if (opts.signal?.aborted)
|
|
88
|
+
throw aborted();
|
|
89
|
+
interval = Math.min(maxInterval, interval * 2);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** One operation (GET /v1/operations/{id}); lifecycle operations stay pollable after a workspace is deleted. */
|
|
93
|
+
async getOperation(operationId, opts = {}) {
|
|
94
|
+
const { operation } = await this.#http.json('GET', `/v1/operations/${encodeURIComponent(operationId)}`, opts.signal ? { signal: opts.signal } : {}, this.#auth);
|
|
95
|
+
return operation;
|
|
96
|
+
}
|
|
97
|
+
async #getView(workspaceId) {
|
|
98
|
+
return this.#http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}`, {}, this.#auth);
|
|
99
|
+
}
|
|
100
|
+
async get(workspaceId, opts = {}) {
|
|
101
|
+
return this.#wrap(await this.#getView(workspaceId), opts);
|
|
102
|
+
}
|
|
103
|
+
async list(params = {}) {
|
|
104
|
+
const page = await this.#http.json('GET', '/v1/workspaces', {
|
|
105
|
+
query: {
|
|
106
|
+
state: params.state,
|
|
107
|
+
desired_state: params.desiredState,
|
|
108
|
+
key_prefix: params.keyPrefix,
|
|
109
|
+
project_id: params.projectId,
|
|
110
|
+
organization_id: params.organizationId,
|
|
111
|
+
include_deleted: params.includeDeleted,
|
|
112
|
+
limit: params.limit,
|
|
113
|
+
cursor: params.cursor,
|
|
114
|
+
},
|
|
115
|
+
}, this.#auth);
|
|
116
|
+
return { data: page.data.map((v) => this.#wrap(v)), nextCursor: page.next_cursor };
|
|
117
|
+
}
|
|
118
|
+
/** Iterates every page. */
|
|
119
|
+
async *listAll(params = {}) {
|
|
120
|
+
let cursor;
|
|
121
|
+
do {
|
|
122
|
+
const page = await this.list({ ...params, ...(cursor ? { cursor } : {}) });
|
|
123
|
+
yield* page.data;
|
|
124
|
+
cursor = page.nextCursor ?? undefined;
|
|
125
|
+
} while (cursor);
|
|
126
|
+
}
|
|
127
|
+
async #lifecycle(method, path, json, idempotencyKey) {
|
|
128
|
+
return this.#http.json(method, path, { ...(json === undefined ? {} : { json }), idempotencyKey: idempotencyKey ?? randomId('op-') }, this.#auth);
|
|
129
|
+
}
|
|
130
|
+
/** Tombstones the workspace now (tool access revoked); storage cleanup happens asynchronously. */
|
|
131
|
+
async delete(workspaceId, opts = {}) {
|
|
132
|
+
return (await this.#lifecycle('DELETE', `/v1/workspaces/${encodeURIComponent(workspaceId)}`, undefined, opts.idempotencyKey)).operation;
|
|
133
|
+
}
|
|
134
|
+
async suspend(workspaceId, opts = {}) {
|
|
135
|
+
return (await this.#lifecycle('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/suspend`, undefined, opts.idempotencyKey)).operation;
|
|
136
|
+
}
|
|
137
|
+
async resume(workspaceId, opts = {}) {
|
|
138
|
+
return (await this.#lifecycle('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/resume`, undefined, opts.idempotencyKey)).operation;
|
|
139
|
+
}
|
|
140
|
+
async snapshot(workspaceId, opts = {}) {
|
|
141
|
+
return (await this.#lifecycle('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/snapshot`, opts.label === undefined ? {} : { label: opts.label }, opts.idempotencyKey)).operation;
|
|
142
|
+
}
|
|
143
|
+
async fork(workspaceId, target, opts = {}) {
|
|
144
|
+
const out = await this.#lifecycle('POST', `/v1/workspaces/${encodeURIComponent(workspaceId)}/fork`, target, opts.idempotencyKey);
|
|
145
|
+
return { operation: out.operation, workspace: this.#wrap(out.workspace) };
|
|
146
|
+
}
|
|
147
|
+
async operations(workspaceId, params = {}) {
|
|
148
|
+
const page = await this.#http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}/operations`, { query: { limit: params.limit, cursor: params.cursor, state: params.state, kind: params.kind } }, this.#auth);
|
|
149
|
+
return { data: page.data, nextCursor: page.next_cursor };
|
|
150
|
+
}
|
|
151
|
+
async agentSessions(workspaceId, params = {}) {
|
|
152
|
+
const page = await this.#http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}/agent-sessions`, { query: { limit: params.limit, cursor: params.cursor } }, this.#auth);
|
|
153
|
+
return { data: page.data, nextCursor: page.next_cursor };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Read-only billing for API keys: the plan catalog and the organization's subscription/billing
|
|
158
|
+
* state. Purchases and the Stripe portal are owner/billing-member actions in the browser
|
|
159
|
+
* (session) surface; an open or resume refused after a payment grace period throws
|
|
160
|
+
* ShardfluxApiError { code: 'entitlement_required', details.reason: 'payment_past_due' | 'unpaid' }.
|
|
161
|
+
*/
|
|
162
|
+
export class BillingApi {
|
|
163
|
+
#ctx;
|
|
164
|
+
constructor(ctx) {
|
|
165
|
+
this.#ctx = ctx;
|
|
166
|
+
}
|
|
167
|
+
catalog() {
|
|
168
|
+
return this.#ctx().http.json('GET', '/v1/billing/catalog', {}, this.#ctx().authorization);
|
|
169
|
+
}
|
|
170
|
+
subscription(organizationId) {
|
|
171
|
+
return this.#ctx().http.json('GET', `/v1/organizations/${encodeURIComponent(organizationId)}/billing/subscription`, {}, this.#ctx().authorization);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** The public plan catalog without an API key (pricing pages, CLIs before sign-up). */
|
|
175
|
+
export async function fetchBillingCatalog(opts = {}) {
|
|
176
|
+
const http = new HttpClient({
|
|
177
|
+
baseUrl: opts.baseUrl ?? 'https://api.shardflux.dev',
|
|
178
|
+
fetch: opts.fetch ?? fetch,
|
|
179
|
+
userAgent: `shardflux-sdk-ts/${SDK_VERSION}`,
|
|
180
|
+
timeoutMs: opts.timeoutMs ?? 30_000,
|
|
181
|
+
maxRetries: 2,
|
|
182
|
+
source: 'api',
|
|
183
|
+
sleep: defaultSleep,
|
|
184
|
+
});
|
|
185
|
+
return http.json('GET', '/v1/billing/catalog');
|
|
186
|
+
}
|
|
187
|
+
export class Shardflux {
|
|
188
|
+
workspaces;
|
|
189
|
+
billing;
|
|
190
|
+
/** Usage, allowances, estimates, grants/leases and spend (Phase 9). */
|
|
191
|
+
usage;
|
|
192
|
+
/** Template registry and custom template builds. */
|
|
193
|
+
templates;
|
|
194
|
+
/** Customer secrets (metadata only; values are write-only and resolved by the cell at session start). */
|
|
195
|
+
secrets;
|
|
196
|
+
/** Project/workspace outbound allowlists (stored and versioned; host enforcement state is reported). */
|
|
197
|
+
egress;
|
|
198
|
+
/** Organization audit trail (owner/admin; API keys are refused with 403). */
|
|
199
|
+
audit;
|
|
200
|
+
/** Shared volumes: persistent storage attached to workspaces at a mount path (contracts §15). */
|
|
201
|
+
volumes;
|
|
202
|
+
#ctx;
|
|
203
|
+
constructor(opts) {
|
|
204
|
+
if (!/^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/.test(opts.apiKey))
|
|
205
|
+
throw new Error('apiKey must be a Shardflux project key (sfk_<key_id>_<secret>)');
|
|
206
|
+
const f = opts.fetch ?? fetch;
|
|
207
|
+
const userAgent = opts.userAgent ?? `shardflux-sdk-ts/${SDK_VERSION}`;
|
|
208
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
209
|
+
this.workspaces = new WorkspacesApi(() => this.#ctx);
|
|
210
|
+
this.billing = new BillingApi(() => this.#ctx);
|
|
211
|
+
this.usage = new UsageApi(() => this.#ctx);
|
|
212
|
+
this.templates = new TemplatesApi(() => this.#ctx);
|
|
213
|
+
this.secrets = new SecretsApi(() => this.#ctx);
|
|
214
|
+
this.egress = new EgressPolicyApi(() => this.#ctx);
|
|
215
|
+
this.audit = new AuditApi(() => this.#ctx);
|
|
216
|
+
this.volumes = new VolumesApi(() => this.#ctx);
|
|
217
|
+
this.#ctx = {
|
|
218
|
+
http: new HttpClient({ baseUrl: opts.baseUrl ?? 'https://api.shardflux.dev', fetch: f, userAgent, timeoutMs: opts.timeoutMs ?? 30_000, maxRetries: opts.maxRetries ?? 2, source: 'api', sleep }),
|
|
219
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
220
|
+
fetch: f,
|
|
221
|
+
userAgent,
|
|
222
|
+
sleep,
|
|
223
|
+
workspaces: this.workspaces,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** The authenticated principal (the API key, its organization and project). */
|
|
227
|
+
me() {
|
|
228
|
+
return this.#ctx.http.json('GET', '/v1/me', {}, this.#ctx.authorization);
|
|
229
|
+
}
|
|
230
|
+
entitlements(organizationId) {
|
|
231
|
+
return this.#ctx.http.json('GET', `/v1/organizations/${encodeURIComponent(organizationId)}/entitlements`, {}, this.#ctx.authorization);
|
|
232
|
+
}
|
|
233
|
+
/** Raw access to any /v1 endpoint with the SDK's authentication and error handling. */
|
|
234
|
+
request(method, path, init = {}) {
|
|
235
|
+
return this.#ctx.http.json(method, path, init, this.#ctx.authorization);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export { ShardfluxApiError };
|
package/dist/egress.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress (outbound network) policies over the application API (/v1). Types are
|
|
3
|
+
* written by hand.
|
|
4
|
+
*
|
|
5
|
+
* Policies are stored and versioned by the API; the host enforces them. Always
|
|
6
|
+
* check `enforcement.state` on a workspace: `not_enforced` until the cell/host
|
|
7
|
+
* acknowledges the current effective version for the workspace's current
|
|
8
|
+
* ownership epoch. Pass `ifMatch` (the `version` you read) to avoid overwriting
|
|
9
|
+
* a concurrent change; a mismatch throws ShardfluxApiError 409 `conflict` with
|
|
10
|
+
* details.reason = 'version_mismatch'.
|
|
11
|
+
*/
|
|
12
|
+
import type { ClientContext, Page } from './client.js';
|
|
13
|
+
export type EgressMode = 'allow_all' | 'allowlist' | 'deny_all';
|
|
14
|
+
export interface EgressRuleInput {
|
|
15
|
+
/** Exact FQDN or `*.` + FQDN (one extra label). IP literals and localhost-like names are rejected. */
|
|
16
|
+
host: string;
|
|
17
|
+
/** Default [443]. */
|
|
18
|
+
ports?: number[];
|
|
19
|
+
/** Only 'tcp' (default); udp/quic are rejected. */
|
|
20
|
+
protocols?: Array<'tcp'>;
|
|
21
|
+
}
|
|
22
|
+
export interface EgressPolicyInput {
|
|
23
|
+
mode: EgressMode;
|
|
24
|
+
/** allowlist only; at most 256. */
|
|
25
|
+
rules?: EgressRuleInput[];
|
|
26
|
+
/** allowlist only; public ranges only; at most 64. */
|
|
27
|
+
cidrs?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface EgressRule {
|
|
30
|
+
host: string;
|
|
31
|
+
ports: number[];
|
|
32
|
+
protocols: Array<'tcp'>;
|
|
33
|
+
}
|
|
34
|
+
export interface EgressPolicyVersion {
|
|
35
|
+
id: string;
|
|
36
|
+
scope: 'project' | 'workspace';
|
|
37
|
+
project_id: string;
|
|
38
|
+
workspace_id: string | null;
|
|
39
|
+
version: number;
|
|
40
|
+
/** cleared: the workspace override was removed. */
|
|
41
|
+
kind: 'policy' | 'cleared';
|
|
42
|
+
mode: EgressMode | null;
|
|
43
|
+
rules: EgressRule[];
|
|
44
|
+
cidrs: string[];
|
|
45
|
+
policy_sha256: string | null;
|
|
46
|
+
created_by: {
|
|
47
|
+
type: 'user' | 'api_key';
|
|
48
|
+
id: string;
|
|
49
|
+
};
|
|
50
|
+
created_at: string;
|
|
51
|
+
}
|
|
52
|
+
export interface EffectiveEgressPolicy {
|
|
53
|
+
/** organization: an organization egress override (contracts §18) wins over every policy (mode deny_all, no version). */
|
|
54
|
+
source: 'organization' | 'workspace' | 'project' | 'platform_default';
|
|
55
|
+
policy_version_id: string | null;
|
|
56
|
+
policy_version: number | null;
|
|
57
|
+
mode: EgressMode;
|
|
58
|
+
rules: EgressRule[];
|
|
59
|
+
cidrs: string[];
|
|
60
|
+
policy_sha256: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Active organization egress override (contracts §18): the organization used its whole outbound transfer allowance,
|
|
64
|
+
* so outbound internet traffic of every workspace is blocked until `lifts_at` (period end) or an upgrade/purchase.
|
|
65
|
+
* Stored policies are kept and apply again when it lifts.
|
|
66
|
+
*/
|
|
67
|
+
export interface OrganizationEgressOverride {
|
|
68
|
+
mode: 'deny_all';
|
|
69
|
+
reason: 'transfer_allowance_exhausted';
|
|
70
|
+
set_at: string;
|
|
71
|
+
lifts_at: string;
|
|
72
|
+
}
|
|
73
|
+
export interface EgressEnforcement {
|
|
74
|
+
state: 'not_enforced' | 'pending' | 'enforced' | 'failed';
|
|
75
|
+
reason: 'not_acknowledged' | 'other_version_acknowledged' | 'stale_epoch' | null;
|
|
76
|
+
/** Version number of the policy the cell/host last acknowledged for this workspace (null: none, or the platform default). */
|
|
77
|
+
applied_version: number | null;
|
|
78
|
+
/** Version number of the current effective policy (null: platform default). */
|
|
79
|
+
effective_version: number | null;
|
|
80
|
+
acknowledged: {
|
|
81
|
+
policy_version_id: string | null;
|
|
82
|
+
policy_version: number | null;
|
|
83
|
+
policy_sha256: string;
|
|
84
|
+
ownership_epoch: number;
|
|
85
|
+
state: 'pending' | 'enforced' | 'failed';
|
|
86
|
+
host_id: string | null;
|
|
87
|
+
error_code: string | null;
|
|
88
|
+
result: Record<string, unknown>;
|
|
89
|
+
acknowledged_at: string;
|
|
90
|
+
applied_at: string | null;
|
|
91
|
+
updated_at: string;
|
|
92
|
+
} | null;
|
|
93
|
+
}
|
|
94
|
+
export interface ProjectEgressPolicy {
|
|
95
|
+
organization_id: string;
|
|
96
|
+
project_id: string;
|
|
97
|
+
/** Send as ifMatch (0 = no project policy yet). */
|
|
98
|
+
version: number;
|
|
99
|
+
policy: EgressPolicyVersion | null;
|
|
100
|
+
effective: EffectiveEgressPolicy;
|
|
101
|
+
/** Active organization override (null: none). */
|
|
102
|
+
organization_override: OrganizationEgressOverride | null;
|
|
103
|
+
/** How far the cell has applied the current project policy to the live workspaces inheriting it. */
|
|
104
|
+
propagation: {
|
|
105
|
+
workspaces: number;
|
|
106
|
+
enforced: number;
|
|
107
|
+
pending: number;
|
|
108
|
+
failed: number;
|
|
109
|
+
not_enforced: number;
|
|
110
|
+
truncated: boolean;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export interface WorkspaceEgressPolicy {
|
|
114
|
+
organization_id: string;
|
|
115
|
+
project_id: string;
|
|
116
|
+
workspace_id: string;
|
|
117
|
+
/** Version of the workspace override history; send as ifMatch. */
|
|
118
|
+
version: number;
|
|
119
|
+
override: EgressPolicyVersion | null;
|
|
120
|
+
project_policy: EgressPolicyVersion | null;
|
|
121
|
+
effective: EffectiveEgressPolicy;
|
|
122
|
+
/** Active organization override (null: none); when set, `effective.source` is 'organization'. */
|
|
123
|
+
organization_override: OrganizationEgressOverride | null;
|
|
124
|
+
enforcement: EgressEnforcement;
|
|
125
|
+
}
|
|
126
|
+
export declare class EgressPolicyApi {
|
|
127
|
+
#private;
|
|
128
|
+
constructor(ctx: () => ClientContext);
|
|
129
|
+
getProject(projectId: string): Promise<ProjectEgressPolicy>;
|
|
130
|
+
/** Replaces the project policy (new immutable version). */
|
|
131
|
+
putProject(projectId: string, policy: EgressPolicyInput, opts?: {
|
|
132
|
+
ifMatch?: number | '*';
|
|
133
|
+
}): Promise<ProjectEgressPolicy>;
|
|
134
|
+
projectVersions(projectId: string, opts?: {
|
|
135
|
+
limit?: number;
|
|
136
|
+
cursor?: string;
|
|
137
|
+
}): Promise<Page<EgressPolicyVersion>>;
|
|
138
|
+
/** Effective policy (workspace > project > platform default) and its enforcement state. */
|
|
139
|
+
getWorkspace(workspaceId: string): Promise<WorkspaceEgressPolicy>;
|
|
140
|
+
/** Sets the workspace override (new immutable version). */
|
|
141
|
+
putWorkspace(workspaceId: string, policy: EgressPolicyInput, opts?: {
|
|
142
|
+
ifMatch?: number | '*';
|
|
143
|
+
}): Promise<WorkspaceEgressPolicy>;
|
|
144
|
+
/** Removes the workspace override (falls back to the project policy). */
|
|
145
|
+
clearWorkspace(workspaceId: string, opts?: {
|
|
146
|
+
ifMatch?: number | '*';
|
|
147
|
+
}): Promise<void>;
|
|
148
|
+
workspaceVersions(workspaceId: string, opts?: {
|
|
149
|
+
limit?: number;
|
|
150
|
+
cursor?: string;
|
|
151
|
+
}): Promise<Page<EgressPolicyVersion>>;
|
|
152
|
+
}
|
package/dist/egress.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const ifMatchHeader = (ifMatch) => ifMatch === undefined ? {} : { 'if-match': ifMatch === '*' ? '*' : `"${ifMatch}"` };
|
|
2
|
+
export class EgressPolicyApi {
|
|
3
|
+
#ctx;
|
|
4
|
+
constructor(ctx) {
|
|
5
|
+
this.#ctx = ctx;
|
|
6
|
+
}
|
|
7
|
+
get #auth() {
|
|
8
|
+
return this.#ctx().authorization;
|
|
9
|
+
}
|
|
10
|
+
getProject(projectId) {
|
|
11
|
+
return this.#ctx().http.json('GET', `/v1/projects/${encodeURIComponent(projectId)}/egress-policy`, {}, this.#auth);
|
|
12
|
+
}
|
|
13
|
+
/** Replaces the project policy (new immutable version). */
|
|
14
|
+
putProject(projectId, policy, opts = {}) {
|
|
15
|
+
return this.#ctx().http.json('PUT', `/v1/projects/${encodeURIComponent(projectId)}/egress-policy`, { json: policy, headers: ifMatchHeader(opts.ifMatch) }, this.#auth);
|
|
16
|
+
}
|
|
17
|
+
async projectVersions(projectId, opts = {}) {
|
|
18
|
+
const raw = await this.#ctx().http.json('GET', `/v1/projects/${encodeURIComponent(projectId)}/egress-policy/versions`, { query: { limit: opts.limit, cursor: opts.cursor } }, this.#auth);
|
|
19
|
+
return { data: raw.data, nextCursor: raw.next_cursor };
|
|
20
|
+
}
|
|
21
|
+
/** Effective policy (workspace > project > platform default) and its enforcement state. */
|
|
22
|
+
getWorkspace(workspaceId) {
|
|
23
|
+
return this.#ctx().http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}/egress-policy`, {}, this.#auth);
|
|
24
|
+
}
|
|
25
|
+
/** Sets the workspace override (new immutable version). */
|
|
26
|
+
putWorkspace(workspaceId, policy, opts = {}) {
|
|
27
|
+
return this.#ctx().http.json('PUT', `/v1/workspaces/${encodeURIComponent(workspaceId)}/egress-policy`, { json: policy, headers: ifMatchHeader(opts.ifMatch) }, this.#auth);
|
|
28
|
+
}
|
|
29
|
+
/** Removes the workspace override (falls back to the project policy). */
|
|
30
|
+
async clearWorkspace(workspaceId, opts = {}) {
|
|
31
|
+
await this.#ctx().http.json('DELETE', `/v1/workspaces/${encodeURIComponent(workspaceId)}/egress-policy`, { headers: ifMatchHeader(opts.ifMatch) }, this.#auth);
|
|
32
|
+
}
|
|
33
|
+
async workspaceVersions(workspaceId, opts = {}) {
|
|
34
|
+
const raw = await this.#ctx().http.json('GET', `/v1/workspaces/${encodeURIComponent(workspaceId)}/egress-policy/versions`, { query: { limit: opts.limit, cursor: opts.cursor } }, this.#auth);
|
|
35
|
+
return { data: raw.data, nextCursor: raw.next_cursor };
|
|
36
|
+
}
|
|
37
|
+
}
|