@shardflux/sdk 0.5.0 → 0.6.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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Lifecycle timing and progress. A slow open (0.8 s one day, 34 s the next) should say where the time went without a
3
+ * packet capture: waiting for a host, the cell booting or restoring the VM, the network between the caller and the API,
4
+ * the first tool token, or retries.
5
+ *
6
+ * A Trace follows one SDK call (open, a lifecycle call with `wait`, a wake, a token fetch). It records two clocks and
7
+ * never mixes them:
8
+ *
9
+ * - client phases, on the caller's monotonic clock: the request, then each operation state the SDK observed while
10
+ * waiting (`queued`, `capacity_pending`, `running`, with the server's `state_reason`), then reading the view and
11
+ * issuing the first token;
12
+ * - server timing, from the operation's own timestamps (one database clock: created_at, started_at, completed_at) and
13
+ * what the cell reported in its result (start path, boot-to-ready, host restore steps).
14
+ *
15
+ * `outsideServerMs` is the difference: time the caller spent that the operation did not (network, TLS, client queues,
16
+ * polling latency, view and token). Listeners get the phases live; the finished call returns a LifecycleTiming.
17
+ */
18
+ import type { components } from './generated/app-api.js';
19
+ type Operation = components['schemas']['Operation'];
20
+ /** The SDK call a trace follows. `wait` is a direct waitForOperation(); `token` a tool token fetched for tool calls. */
21
+ export type LifecycleAction = 'open' | 'suspend' | 'resume' | 'snapshot' | 'fork' | 'delete' | 'close' | 'reset' | 'wake' | 'wait' | 'token';
22
+ /**
23
+ * - `request`: an API request that starts or joins the operation. A held open (`Prefer: wait`) spends the server's
24
+ * hold here (reason `held`), so the states inside it show only in the server timing.
25
+ * - `queued`, `capacity_pending`, `running`: operation states as the SDK observed them while waiting, with the
26
+ * operation's `state_reason` (e.g. `no_ready_host`, `template_downloading`).
27
+ * - `view`: reading the workspace after the operation. `token`: issuing a tool token (concurrent with `view` in open()).
28
+ * - `busy`: a tool call waiting out `workspace_busy` (only in `tool` events).
29
+ */
30
+ export type LifecyclePhase = 'request' | 'queued' | 'capacity_pending' | 'running' | 'view' | 'token' | 'busy';
31
+ export interface TimingPhase {
32
+ phase: LifecyclePhase;
33
+ /** The server's state_reason, or why the SDK entered the phase (`held`, `initial`, `expiring`, `invalidated`). */
34
+ reason: string | null;
35
+ operationId: string | null;
36
+ /** Milliseconds from the start of the call (client monotonic clock). */
37
+ startMs: number;
38
+ durationMs: number;
39
+ }
40
+ export interface RetryRecord {
41
+ atMs: number;
42
+ /** Method and path, e.g. `POST /v1/workspaces/open`. */
43
+ request: string;
44
+ /** 1 for the first retry. */
45
+ attempt: number;
46
+ /** `HTTP 503 unavailable`, `timeout after 30000 ms`, `network: ECONNRESET`, `conflict operation_in_progress`, `stale_epoch`. */
47
+ cause: string;
48
+ /** Backoff before the retry. */
49
+ delayMs: number;
50
+ }
51
+ /** The operation's own account (database clock and cell-reported results); independent of the caller's network. */
52
+ export interface ServerTiming {
53
+ operationId: string;
54
+ kind: Operation['kind'];
55
+ state: Operation['state'];
56
+ /**
57
+ * created_at → started_at: waiting until the cell began running the operation, including any wait for a host with
58
+ * capacity (started_at is set when the operation first enters `running`). Null when the API does not report
59
+ * started_at or the operation has not run yet. An operation that finished without running (e.g. a failed dependency)
60
+ * has started_at = completed_at, so all of its time shows here.
61
+ */
62
+ queuedMs: number | null;
63
+ /** started_at → completed_at: the cell executing it (placement, boot or restore, guest readiness). Null until finished. */
64
+ runMs: number | null;
65
+ /** created_at → completed_at. Null until finished. */
66
+ totalMs: number | null;
67
+ /** `result.start_path`: `warm` (cloned from a warm snapshot) or `boot`. */
68
+ startPath: string | null;
69
+ /** `result.warm_fallback`: why a start that could be warm booted instead. */
70
+ warmFallback: string | null;
71
+ /** `result.resume_path`: `local_cache` or `download` (the checkpoint had to be fetched first). */
72
+ resumePath: string | null;
73
+ /** `result.boot_to_ready_ms`: VM start until the guest agent answered. */
74
+ bootToReadyMs: number | null;
75
+ /** `result.host_timings_ms`: the host's own steps (restore: load, after_restore, ready, …). */
76
+ hostTimingsMs: Record<string, number> | null;
77
+ }
78
+ export type TimingOutcome = 'succeeded' | 'failed' | 'timed_out' | 'error';
79
+ export interface LifecycleTiming {
80
+ action: LifecycleAction;
81
+ outcome: TimingOutcome;
82
+ workspaceId: string | null;
83
+ operationId: string | null;
84
+ /** Wall time of the call on the caller's monotonic clock. */
85
+ totalMs: number;
86
+ /** Client phases in start order. They follow each other, except `view` and `token` at the end of open(), which run together. */
87
+ phases: TimingPhase[];
88
+ retries: RetryRecord[];
89
+ /** Null when the call involved no operation (e.g. open() of a workspace that was already running). */
90
+ server: ServerTiming | null;
91
+ /**
92
+ * totalMs − server.totalMs: time the caller spent outside the operation (network and TLS, the held request's
93
+ * transit, polling latency, the view read and token). Null when the operation did not finish, or began before this
94
+ * call (a joined open or resume).
95
+ */
96
+ outsideServerMs: number | null;
97
+ }
98
+ interface EventBase {
99
+ action: LifecycleAction | 'tool';
100
+ workspaceId: string | null;
101
+ operationId: string | null;
102
+ /** Milliseconds since the call began. */
103
+ atMs: number;
104
+ }
105
+ /**
106
+ * - `phase`: a phase began (live progress: "capacity_pending: no_ready_host"). `tool` phases come from tool calls.
107
+ * - `retry`: a request is retried after a transient failure (also emitted for tool calls, action `tool`).
108
+ * - `done`: the call ended, successfully or not, with its full timing.
109
+ */
110
+ export type ProgressEvent = (EventBase & {
111
+ type: 'phase';
112
+ phase: LifecyclePhase;
113
+ reason: string | null;
114
+ }) | (EventBase & {
115
+ type: 'retry';
116
+ retry: RetryRecord;
117
+ }) | (EventBase & {
118
+ type: 'done';
119
+ timing: LifecycleTiming;
120
+ });
121
+ export type ProgressListener = (event: ProgressEvent) => void;
122
+ /** Calls every listener; a listener that throws never breaks the SDK call. */
123
+ export declare function emitTo(listeners: ReadonlyArray<ProgressListener | undefined>, event: ProgressEvent): void;
124
+ /** Combines listeners (client-level and per call) into one; undefined when there are none. */
125
+ export declare function combineListeners(...listeners: Array<ProgressListener | undefined>): ProgressListener | undefined;
126
+ /** Server timing from an operation as GET /v1/operations/{id} returns it. */
127
+ export declare function serverTiming(op: Operation): ServerTiming;
128
+ /** Why a request failed, in one short phrase (for retry records). */
129
+ export declare function describeFailure(err: unknown, timeoutMs?: number): string;
130
+ export declare class Trace {
131
+ #private;
132
+ readonly action: LifecycleAction;
133
+ workspaceId: string | null;
134
+ operationId: string | null;
135
+ constructor(action: LifecycleAction, listener: ProgressListener | undefined, ids?: {
136
+ workspaceId?: string | null;
137
+ operationId?: string | null;
138
+ });
139
+ /** Milliseconds since the call began. */
140
+ now(): number;
141
+ get finished(): LifecycleTiming | null;
142
+ /** Enters a sequential phase (closing the current one). The same phase and reason again is not a new phase. */
143
+ phase(phase: LifecyclePhase, reason?: string | null): void;
144
+ /** Runs `fn` as a phase that may overlap others (open() reads the view and issues the token together). */
145
+ span<T>(phase: LifecyclePhase, fn: () => Promise<T>, reason?: string | null): Promise<T>;
146
+ /** Records an operation snapshot: its ids, the observed state as a phase, and the server timing. */
147
+ observe(op: Operation): void;
148
+ retry(r: Omit<RetryRecord, 'atMs'>): void;
149
+ /** An `onRetry` hook for HttpClient requests made on behalf of this call. */
150
+ get onRetry(): (r: Omit<RetryRecord, 'atMs'>) => void;
151
+ /** Ends the call (idempotent) and emits `done`. Pass the error when it failed. */
152
+ end(error?: unknown): LifecycleTiming;
153
+ }
154
+ /** Runs `fn` under `trace` and ends the trace either way (the error keeps its timing). */
155
+ export declare function traced<T>(trace: Trace, fn: () => Promise<T>): Promise<T>;
156
+ /**
157
+ * A human-readable account of a timing, for logs and bug reports:
158
+ *
159
+ * open 34.18 s, succeeded (workspace <id>, operation <id>)
160
+ * client: request 20.01 s (held) → capacity_pending 13.52 s (no_ready_host) → running 0.59 s → view 42 ms ∥ token 61 ms
161
+ * server: queued 33.40 s, ran 0.62 s, total 34.02 s; start warm, boot to ready 79 ms
162
+ * outside the server: 0.16 s
163
+ * retries: 1 (POST /v1/workspaces/open, HTTP 503 unavailable, after 200 ms)
164
+ */
165
+ export declare function formatTiming(t: LifecycleTiming): string;
166
+ export {};
@@ -0,0 +1,238 @@
1
+ /** Calls every listener; a listener that throws never breaks the SDK call. */
2
+ export function emitTo(listeners, event) {
3
+ for (const l of listeners) {
4
+ if (!l)
5
+ continue;
6
+ try {
7
+ l(event);
8
+ }
9
+ catch {
10
+ // Listener errors are the listener's problem.
11
+ }
12
+ }
13
+ }
14
+ /** Combines listeners (client-level and per call) into one; undefined when there are none. */
15
+ export function combineListeners(...listeners) {
16
+ const ls = listeners.filter((l) => l !== undefined);
17
+ if (ls.length === 0)
18
+ return undefined;
19
+ if (ls.length === 1)
20
+ return ls[0];
21
+ return (e) => emitTo(ls, e);
22
+ }
23
+ const round = (ms) => Math.round(ms * 10) / 10;
24
+ const TERMINAL = new Set(['succeeded', 'failed', 'canceled']);
25
+ function diffMs(from, to) {
26
+ if (!from || !to)
27
+ return null;
28
+ const d = Date.parse(to) - Date.parse(from);
29
+ return Number.isFinite(d) ? d : null;
30
+ }
31
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : null);
32
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
33
+ /** Server timing from an operation as GET /v1/operations/{id} returns it. */
34
+ export function serverTiming(op) {
35
+ const r = (op.result ?? {});
36
+ const host = r.host_timings_ms;
37
+ let hostTimingsMs = null;
38
+ if (typeof host === 'object' && host !== null) {
39
+ hostTimingsMs = {};
40
+ for (const [k, v] of Object.entries(host))
41
+ if (num(v) !== null)
42
+ hostTimingsMs[k] = v;
43
+ }
44
+ const startedAt = op.started_at ?? null;
45
+ return {
46
+ operationId: op.id,
47
+ kind: op.kind,
48
+ state: op.state,
49
+ queuedMs: diffMs(op.created_at, startedAt),
50
+ runMs: diffMs(startedAt, op.completed_at),
51
+ totalMs: diffMs(op.created_at, op.completed_at),
52
+ startPath: str(r.start_path),
53
+ warmFallback: str(r.warm_fallback),
54
+ resumePath: str(r.resume_path),
55
+ bootToReadyMs: num(r.boot_to_ready_ms),
56
+ hostTimingsMs,
57
+ };
58
+ }
59
+ function outcomeOf(err) {
60
+ const name = err instanceof Error ? err.name : '';
61
+ if (name === 'OperationFailedError')
62
+ return 'failed';
63
+ if (name === 'OperationTimeoutError')
64
+ return 'timed_out';
65
+ return 'error';
66
+ }
67
+ /** Why a request failed, in one short phrase (for retry records). */
68
+ export function describeFailure(err, timeoutMs) {
69
+ if (err instanceof Error) {
70
+ if (err.name === 'TimeoutError')
71
+ return timeoutMs !== undefined ? `timeout after ${timeoutMs} ms` : 'timeout';
72
+ const e = err;
73
+ if (e.name === 'ShardfluxApiError' || e.name === 'ShardfluxProtocolError')
74
+ return `HTTP ${e.status}${e.code ? ` ${e.code}` : ''}`;
75
+ return `network: ${e.cause?.code ?? e.cause?.message ?? e.message}`;
76
+ }
77
+ return 'network error';
78
+ }
79
+ export class Trace {
80
+ action;
81
+ workspaceId;
82
+ operationId;
83
+ #listener;
84
+ #t0;
85
+ #phases = [];
86
+ #retries = [];
87
+ #current = null;
88
+ #server = null;
89
+ #timing = null;
90
+ constructor(action, listener, ids = {}) {
91
+ this.action = action;
92
+ this.#listener = listener;
93
+ this.workspaceId = ids.workspaceId ?? null;
94
+ this.operationId = ids.operationId ?? null;
95
+ this.#t0 = performance.now();
96
+ }
97
+ /** Milliseconds since the call began. */
98
+ now() {
99
+ return round(performance.now() - this.#t0);
100
+ }
101
+ get finished() {
102
+ return this.#timing;
103
+ }
104
+ #emit(event) {
105
+ if (this.#listener)
106
+ emitTo([this.#listener], event);
107
+ }
108
+ #base() {
109
+ return { action: this.action, workspaceId: this.workspaceId, operationId: this.operationId, atMs: this.now() };
110
+ }
111
+ #close(at) {
112
+ if (this.#current)
113
+ this.#current.durationMs = round(at - this.#current.startMs);
114
+ this.#current = null;
115
+ }
116
+ /** Enters a sequential phase (closing the current one). The same phase and reason again is not a new phase. */
117
+ phase(phase, reason = null) {
118
+ if (this.#timing)
119
+ return;
120
+ if (this.#current && this.#current.phase === phase && this.#current.reason === reason)
121
+ return;
122
+ const at = this.now();
123
+ this.#close(at);
124
+ this.#current = { phase, reason, operationId: this.operationId, startMs: at, durationMs: 0 };
125
+ this.#phases.push(this.#current);
126
+ this.#emit({ ...this.#base(), type: 'phase', phase, reason });
127
+ }
128
+ /** Runs `fn` as a phase that may overlap others (open() reads the view and issues the token together). */
129
+ async span(phase, fn, reason = null) {
130
+ this.#close(this.now());
131
+ const entry = { phase, reason, operationId: this.operationId, startMs: this.now(), durationMs: 0 };
132
+ this.#phases.push(entry);
133
+ this.#emit({ ...this.#base(), type: 'phase', phase, reason });
134
+ try {
135
+ return await fn();
136
+ }
137
+ finally {
138
+ entry.durationMs = round(this.now() - entry.startMs);
139
+ }
140
+ }
141
+ /** Records an operation snapshot: its ids, the observed state as a phase, and the server timing. */
142
+ observe(op) {
143
+ // The latest operation wins: a wake that meets a conflict joins the active operation instead of its own.
144
+ this.operationId = op.id;
145
+ this.workspaceId ??= op.workspace_id;
146
+ this.#server = serverTiming(op);
147
+ if (!TERMINAL.has(op.state))
148
+ this.phase(op.state, op.state_reason ?? null);
149
+ }
150
+ retry(r) {
151
+ if (this.#timing)
152
+ return;
153
+ const record = { atMs: this.now(), ...r };
154
+ this.#retries.push(record);
155
+ this.#emit({ ...this.#base(), type: 'retry', retry: record });
156
+ }
157
+ /** An `onRetry` hook for HttpClient requests made on behalf of this call. */
158
+ get onRetry() {
159
+ return (r) => this.retry(r);
160
+ }
161
+ /** Ends the call (idempotent) and emits `done`. Pass the error when it failed. */
162
+ end(error) {
163
+ if (this.#timing)
164
+ return this.#timing;
165
+ const totalMs = this.now();
166
+ this.#close(totalMs);
167
+ const server = this.#server;
168
+ let outsideServerMs = null;
169
+ if (server?.totalMs !== null && server?.totalMs !== undefined && server.totalMs <= totalMs)
170
+ outsideServerMs = round(totalMs - server.totalMs);
171
+ this.#timing = {
172
+ action: this.action,
173
+ outcome: error === undefined ? 'succeeded' : outcomeOf(error),
174
+ workspaceId: this.workspaceId,
175
+ operationId: this.operationId,
176
+ totalMs,
177
+ phases: this.#phases.map((p) => ({ ...p })),
178
+ retries: [...this.#retries],
179
+ server,
180
+ outsideServerMs,
181
+ };
182
+ if (error instanceof Error && 'timing' in error)
183
+ error.timing ??= this.#timing;
184
+ this.#emit({ ...this.#base(), type: 'done', timing: this.#timing });
185
+ return this.#timing;
186
+ }
187
+ }
188
+ /** Runs `fn` under `trace` and ends the trace either way (the error keeps its timing). */
189
+ export async function traced(trace, fn) {
190
+ try {
191
+ const out = await fn();
192
+ trace.end();
193
+ return out;
194
+ }
195
+ catch (err) {
196
+ trace.end(err);
197
+ throw err;
198
+ }
199
+ }
200
+ const fmt = (ms) => (ms === null ? '?' : ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(2)} s`);
201
+ /**
202
+ * A human-readable account of a timing, for logs and bug reports:
203
+ *
204
+ * open 34.18 s, succeeded (workspace <id>, operation <id>)
205
+ * client: request 20.01 s (held) → capacity_pending 13.52 s (no_ready_host) → running 0.59 s → view 42 ms ∥ token 61 ms
206
+ * server: queued 33.40 s, ran 0.62 s, total 34.02 s; start warm, boot to ready 79 ms
207
+ * outside the server: 0.16 s
208
+ * retries: 1 (POST /v1/workspaces/open, HTTP 503 unavailable, after 200 ms)
209
+ */
210
+ export function formatTiming(t) {
211
+ const ids = [t.workspaceId ? `workspace ${t.workspaceId}` : null, t.operationId ? `operation ${t.operationId}` : null].filter(Boolean).join(', ');
212
+ const lines = [`${t.action} ${fmt(t.totalMs)}, ${t.outcome.replace('_', ' ')}${ids ? ` (${ids})` : ''}`];
213
+ let client = '';
214
+ t.phases.forEach((p, i) => {
215
+ const prev = t.phases[i - 1];
216
+ // A phase that starts before the previous one ended ran alongside it (view and token at the end of open()).
217
+ const sep = i === 0 ? '' : prev && p.startMs < prev.startMs + prev.durationMs ? ' ∥ ' : ' → ';
218
+ client += `${sep}${p.phase} ${fmt(p.durationMs)}${p.reason ? ` (${p.reason})` : ''}`;
219
+ });
220
+ if (client)
221
+ lines.push(` client: ${client}`);
222
+ const s = t.server;
223
+ if (s) {
224
+ const parts = [s.queuedMs !== null ? `queued ${fmt(s.queuedMs)}` : null, s.runMs !== null ? `ran ${fmt(s.runMs)}` : null, s.totalMs !== null ? `total ${fmt(s.totalMs)}` : `still ${s.state}`];
225
+ const how = [
226
+ s.startPath ? `start ${s.startPath}${s.warmFallback ? ` (warm fallback: ${s.warmFallback})` : ''}` : null,
227
+ s.resumePath ? `resume from ${s.resumePath}` : null,
228
+ s.bootToReadyMs !== null ? `boot to ready ${fmt(s.bootToReadyMs)}` : null,
229
+ s.hostTimingsMs ? `host ${Object.entries(s.hostTimingsMs).map(([k, v]) => `${k} ${fmt(v)}`).join(', ')}` : null,
230
+ ].filter(Boolean);
231
+ lines.push(` server: ${parts.filter(Boolean).join(', ')}${how.length ? `; ${how.join(', ')}` : ''}`);
232
+ }
233
+ if (t.outsideServerMs !== null)
234
+ lines.push(` outside the server: ${fmt(t.outsideServerMs)}`);
235
+ if (t.retries.length)
236
+ lines.push(` retries: ${t.retries.length} (${t.retries.map((r) => `${r.request}, ${r.cause}, after ${fmt(r.delayMs)}`).join('; ')})`);
237
+ return lines.join('\n');
238
+ }
package/dist/secrets.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Customer secrets over the application API (/v1). Types are written by hand.
2
+ * Customer secrets over the application API (/v1). Types are written by hand
3
+ * (checked against the generated contract in type-checks.ts).
3
4
  *
4
5
  * A project API key manages the secrets of its own project. Values are
5
6
  * write-only: no method here returns one. The cell gateway obtains values at
6
- * exec/PTY session start through POST /v1/internal/secret-resolutions with the
7
- * caller's tool token; that endpoint is not part of the customer SDK.
8
- * Organization-wide secrets and access logs are managed by owners/admins in the
9
- * browser (/api/v1).
7
+ * exec/PTY session start with the caller's tool token, for the names bound to
8
+ * the workspace (`workspace.secrets`, `open({ secrets })`) together with the
9
+ * call's own `secretRefs`. Organization-wide secrets and access logs are managed
10
+ * by owners/admins (the API answers 403 to project API keys for those calls).
10
11
  */
11
12
  import type { ClientContext, Page } from './client.js';
12
13
  import type { ToolName } from './tokens.js';
@@ -63,14 +64,64 @@ export interface CreateSecretParams {
63
64
  /** Defaults to a fresh key per call so transport retries replay instead of duplicating. */
64
65
  idempotencyKey?: string;
65
66
  }
67
+ export interface CreateOrganizationSecretParams extends CreateSecretParams {
68
+ /** Projects whose workspaces may use it: default [] (nowhere until granted); null = every project. */
69
+ allowedProjectIds?: string[] | null;
70
+ }
66
71
  export interface UpdateSecretParams {
67
72
  description?: string;
73
+ /** Organization secrets only. */
74
+ allowedProjectIds?: string[] | null;
68
75
  allowedWorkspaceIds?: string[] | null;
69
76
  allowedTools?: ToolName[];
70
77
  }
78
+ /** One session-time resolution that matched a secret (granted, denied or failed). Never a value. */
79
+ export interface SecretAccessEvent {
80
+ id: string;
81
+ secret_id: string | null;
82
+ secret_version: number | null;
83
+ requested_name: string;
84
+ workspace_id: string;
85
+ project_id: string;
86
+ agent_session_id: string | null;
87
+ principal: {
88
+ type: 'user' | 'api_key';
89
+ id: string;
90
+ };
91
+ tool: ToolName;
92
+ outcome: 'granted' | 'denied' | 'failed';
93
+ /** Closed code for denied/failed: not_found, project_not_allowed, workspace_not_allowed, tool_not_allowed, ... */
94
+ reason: string | null;
95
+ ownership_epoch: number | null;
96
+ request_id: string | null;
97
+ cell_session_id: string | null;
98
+ created_at: string;
99
+ }
100
+ /** Status of a bound name: available (injected at the next start), not_allowed (starts are refused with 403), deleted. */
101
+ export type BoundSecretStatus = 'available' | 'not_allowed' | 'deleted';
102
+ /** GET/PUT /v1/workspaces/{id}/secrets: the names bound to a workspace (never values). */
103
+ export interface WorkspaceSecretBindings {
104
+ workspace_id: string;
105
+ /** The bound names, in the order they were given. */
106
+ names: string[];
107
+ secrets: Array<{
108
+ name: string;
109
+ status: BoundSecretStatus;
110
+ secret_id: string | null;
111
+ scope: SecretScope | null;
112
+ }>;
113
+ }
71
114
  export declare class SecretsApi {
72
115
  #private;
73
116
  constructor(ctx: () => ClientContext);
117
+ /** Creates an organization-wide secret (owners/admins; project API keys get 403). */
118
+ createOrganization(organizationId: string, params: CreateOrganizationSecretParams): Promise<Secret>;
119
+ /** Lists organization-wide secrets (metadata only; owners/admins/members, project API keys get 403). */
120
+ listOrganization(organizationId: string, opts?: {
121
+ limit?: number;
122
+ cursor?: string;
123
+ includeDeleted?: boolean;
124
+ }): Promise<Page<Secret>>;
74
125
  /** Creates a project secret (the key's own project). */
75
126
  create(projectId: string, params: CreateSecretParams): Promise<Secret>;
76
127
  /** Lists a project's secrets (metadata only). */
@@ -95,6 +146,32 @@ export declare class SecretsApi {
95
146
  limit?: number;
96
147
  cursor?: string;
97
148
  }): Promise<Page<SecretVersion>>;
98
- /** Deletes (tombstones) the secret and erases its values; resolution stops at once. */
149
+ /**
150
+ * Deletes (tombstones) the secret and erases its values; resolution stops at once and the name is removed
151
+ * from every workspace binding that referred to it.
152
+ */
99
153
  delete(secretId: string): Promise<void>;
154
+ /** Access log of a secret, newest first (owners/admins; project API keys get 403). */
155
+ accessEvents(secretId: string, opts?: {
156
+ limit?: number;
157
+ cursor?: string;
158
+ outcome?: SecretAccessEvent['outcome'];
159
+ workspaceId?: string;
160
+ }): Promise<Page<SecretAccessEvent>>;
161
+ }
162
+ /**
163
+ * The secret names bound to one workspace (`workspace.secrets`): injected as environment variables into every
164
+ * exec and PTY start of the workspace, together with the call's own `secretRefs`. Never values.
165
+ */
166
+ export declare class WorkspaceSecrets {
167
+ #private;
168
+ constructor(ctx: ClientContext, workspaceId: string);
169
+ /** The bound names with their status (available | not_allowed | deleted). */
170
+ get(): Promise<WorkspaceSecretBindings>;
171
+ /**
172
+ * Replaces the binding (`[]` clears it); applies from the next exec/PTY start. Every name must be a secret this
173
+ * workspace may use, else ShardfluxApiError 422 (details.reason `secret_not_available`, details.names) and
174
+ * nothing changes.
175
+ */
176
+ set(names: readonly string[]): Promise<WorkspaceSecretBindings>;
100
177
  }
package/dist/secrets.js CHANGED
@@ -8,6 +8,25 @@ export class SecretsApi {
8
8
  get #auth() {
9
9
  return this.#ctx().authorization;
10
10
  }
11
+ /** Creates an organization-wide secret (owners/admins; project API keys get 403). */
12
+ async createOrganization(organizationId, params) {
13
+ return this.#ctx().http.json('POST', `/v1/organizations/${encodeURIComponent(organizationId)}/secrets`, {
14
+ json: {
15
+ name: params.name,
16
+ value: params.value,
17
+ ...(params.description !== undefined ? { description: params.description } : {}),
18
+ ...(params.allowedProjectIds !== undefined ? { allowed_project_ids: params.allowedProjectIds } : {}),
19
+ ...(params.allowedWorkspaceIds !== undefined ? { allowed_workspace_ids: params.allowedWorkspaceIds } : {}),
20
+ ...(params.allowedTools !== undefined ? { allowed_tools: params.allowedTools } : {}),
21
+ },
22
+ idempotencyKey: params.idempotencyKey ?? randomId('sdk-secret-'),
23
+ }, this.#auth);
24
+ }
25
+ /** Lists organization-wide secrets (metadata only; owners/admins/members, project API keys get 403). */
26
+ async listOrganization(organizationId, opts = {}) {
27
+ const raw = await this.#ctx().http.json('GET', `/v1/organizations/${encodeURIComponent(organizationId)}/secrets`, { query: { limit: opts.limit, cursor: opts.cursor, include_deleted: opts.includeDeleted } }, this.#auth);
28
+ return toPage(raw);
29
+ }
11
30
  /** Creates a project secret (the key's own project). */
12
31
  async create(projectId, params) {
13
32
  return this.#ctx().http.json('POST', `/v1/projects/${encodeURIComponent(projectId)}/secrets`, {
@@ -35,6 +54,7 @@ export class SecretsApi {
35
54
  return this.#ctx().http.json('PATCH', `/v1/secrets/${encodeURIComponent(secretId)}`, {
36
55
  json: {
37
56
  ...(params.description !== undefined ? { description: params.description } : {}),
57
+ ...(params.allowedProjectIds !== undefined ? { allowed_project_ids: params.allowedProjectIds } : {}),
38
58
  ...(params.allowedWorkspaceIds !== undefined ? { allowed_workspace_ids: params.allowedWorkspaceIds } : {}),
39
59
  ...(params.allowedTools !== undefined ? { allowed_tools: params.allowedTools } : {}),
40
60
  },
@@ -49,8 +69,40 @@ export class SecretsApi {
49
69
  const raw = await this.#ctx().http.json('GET', `/v1/secrets/${encodeURIComponent(secretId)}/versions`, { query: { limit: opts.limit, cursor: opts.cursor } }, this.#auth);
50
70
  return toPage(raw);
51
71
  }
52
- /** Deletes (tombstones) the secret and erases its values; resolution stops at once. */
72
+ /**
73
+ * Deletes (tombstones) the secret and erases its values; resolution stops at once and the name is removed
74
+ * from every workspace binding that referred to it.
75
+ */
53
76
  async delete(secretId) {
54
77
  await this.#ctx().http.json('DELETE', `/v1/secrets/${encodeURIComponent(secretId)}`, {}, this.#auth);
55
78
  }
79
+ /** Access log of a secret, newest first (owners/admins; project API keys get 403). */
80
+ async accessEvents(secretId, opts = {}) {
81
+ const raw = await this.#ctx().http.json('GET', `/v1/secrets/${encodeURIComponent(secretId)}/access-events`, { query: { limit: opts.limit, cursor: opts.cursor, outcome: opts.outcome, workspace_id: opts.workspaceId } }, this.#auth);
82
+ return toPage(raw);
83
+ }
84
+ }
85
+ /**
86
+ * The secret names bound to one workspace (`workspace.secrets`): injected as environment variables into every
87
+ * exec and PTY start of the workspace, together with the call's own `secretRefs`. Never values.
88
+ */
89
+ export class WorkspaceSecrets {
90
+ #ctx;
91
+ #workspaceId;
92
+ constructor(ctx, workspaceId) {
93
+ this.#ctx = ctx;
94
+ this.#workspaceId = workspaceId;
95
+ }
96
+ /** The bound names with their status (available | not_allowed | deleted). */
97
+ get() {
98
+ return this.#ctx.http.json('GET', `/v1/workspaces/${encodeURIComponent(this.#workspaceId)}/secrets`, {}, this.#ctx.authorization);
99
+ }
100
+ /**
101
+ * Replaces the binding (`[]` clears it); applies from the next exec/PTY start. Every name must be a secret this
102
+ * workspace may use, else ShardfluxApiError 422 (details.reason `secret_not_available`, details.names) and
103
+ * nothing changes.
104
+ */
105
+ set(names) {
106
+ return this.#ctx.http.json('PUT', `/v1/workspaces/${encodeURIComponent(this.#workspaceId)}/secrets`, { json: { names: [...names] } }, this.#ctx.authorization);
107
+ }
56
108
  }