@shardflux/sdk 0.6.1 → 0.7.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/dist/http.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { RetryRecord } from './progress.js';
2
- export declare const SDK_VERSION = "0.6.1";
2
+ export declare const SDK_VERSION = "0.7.0";
3
3
  export interface RequestOptions {
4
4
  query?: Record<string, string | number | boolean | undefined | null>;
5
5
  json?: unknown;
package/dist/http.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { ShardfluxApiError, ShardfluxProtocolError, isErrorBody } from "./errors.js";
8
8
  import { describeFailure } from "./progress.js";
9
- export const SDK_VERSION = '0.6.1';
9
+ export const SDK_VERSION = '0.7.0';
10
10
  export const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
11
11
  /**
12
12
  * The fetch the SDK uses when none is given. On runtimes whose bundled undici is 8.x (Node 26) it sends
package/dist/index.d.ts CHANGED
@@ -12,14 +12,18 @@
12
12
  export type { components, operations, paths } from './generated/app-api.js';
13
13
  export type { components as CellComponents, paths as CellPaths } from './generated/cell-api.js';
14
14
  export { BillingApi, Shardflux, WorkspacesApi, fetchBillingCatalog, pickByKey } from './client.js';
15
- export type { AgentSession, BillingCatalog, BillingSubscription, Caps, CheckoutSession, DiskLayout, Entitlements, FindByKeyOptions, ForkTarget, Invoice, InvoicePage, LifetimeFilter, ListParams, Me, OpenParams, OpenResponse, Operation, Page, PortalSession, PurposeFilter, ResetWorkspaceBody, ShardfluxOptions, UpdatePolicy, WaitOptions, WorkspaceLifetime, WorkspaceOrigin, WorkspacePurpose, WorkspaceView, } from './client.js';
15
+ export type { AgentSession, BillingCatalog, BillingSubscription, Caps, CheckoutSession, DiskLayout, Entitlements, FindByKeyOptions, ForkTarget, Invoice, InvoicePage, LifetimeFilter, ListParams, Me, OpenParams, OpenResponse, Operation, Page, PortalSession, PurposeFilter, ResetWorkspaceBody, ShardfluxOptions, UpdatePolicy, WaitOptions, WorkspaceInputs, WorkspaceLifetime, WorkspaceOrigin, WorkspacePurpose, WorkspaceView, } from './client.js';
16
16
  export type { FinishedOperation, LifecycleOptions, WaitedLifecycleOptions } from './lifecycle.js';
17
17
  export { formatTiming } from './progress.js';
18
18
  export type { LifecycleAction, LifecyclePhase, LifecycleTiming, ProgressEvent, ProgressListener, RetryRecord, ServerTiming, TimingOutcome, TimingPhase, } from './progress.js';
19
19
  export { UsageApi } from './usage.js';
20
20
  export type { Grants, Spend, SpendPolicy, UsageEstimate, UsageMeter, UsageSeries, UsageSeriesParams, UsageSummary } from './usage.js';
21
- export { TemplateBuildTimeoutError, TemplateBuildsApi, TemplateDraftApi, TemplatesApi, buildSettled, saveAsTemplateBody } from './templates.js';
22
- export type { BuilderAvailability, CreateDraftBody, CreateDraftParams, CreateTemplateBuildParams, CreateTestInstanceBody, DraftOpened, DraftState, OpenTestInstanceParams, OrgTemplateStorage, PublishDraftBody, PublishDraftParams, SaveAsTemplateBody, SaveAsTemplateParams, SaveAsTemplateResponse, TemplateBuild, TemplateBuildLogUrl, TemplateBuildRegistrationState, TemplateBuildState, TemplateDefaults, TemplateDefaultsInput, TemplateDetail, TemplateDiffChange, TemplateDiffEntry, TemplateDiffPage, TemplateDiffParams, TemplateDraft, TemplateDraftSummary, TemplateFileEntry, TemplateFilePage, TemplateFilesParams, TemplateFilesSummary, TemplateOwner, TemplateRecipe, TemplateSource, TemplateStorage, TemplateStorageWarning, TemplateSummary, TemplateVersion, TemplateVersionState, WaitForBuildOptions, } from './templates.js';
21
+ export { TemplateBuildTimeoutError, TemplateBuildsApi, TemplateDraftApi, TemplatePackagesApi, TemplateUploadError, TemplateUploadsApi, TemplateVersionTestInstancesApi, TemplateVersionsApi, TemplatesApi, buildSettled, saveAsTemplateBody, } from './templates.js';
22
+ export { TemplateFileError, packDirectory, parseTemplateText, readTemplateFile } from './template-file.js';
23
+ export type { PackedFile, YamlParser } from './template-file.js';
24
+ export { tarEnd, tarHeader, tarPadding } from './tar.js';
25
+ export type { TarEntry, TarEntryType } from './tar.js';
26
+ export type { BuildFromFileEvent, BuildFromFileOptions, BuildFromFileResult, BuildFromRecipeOptions, BuilderAvailability, CreateDraftBody, CreateDraftParams, CreateTemplateBuildParams, CreateTestInstanceBody, CreateVersionTestInstanceBody, CreateVersionTestInstanceParams, DraftOpened, LocalUpload, DraftState, OpenTestInstanceParams, OrgTemplateStorage, PublishDraftBody, PublishDraftParams, PutUploadOptions, SaveAsTemplateBody, SaveAsTemplateParams, SaveAsTemplateResponse, TemplateBuild, TemplateBuildLogUrl, TemplateBuildRecipeV2, TemplateBuildRegistrationState, TemplateBuildState, TemplateCategory, TemplateDefaults, TemplateDefaultsInput, TemplateDetail, TemplateDiffChange, TemplateDiffEntry, TemplateDiffPage, TemplateDiffParams, TemplateDraft, TemplateDraftSummary, TemplateEgressDefault, TemplateFileEntry, TemplateFilePage, TemplateFilesParams, TemplateFilesSummary, TemplateInput, TemplateLanguage, TemplateLanguages, TemplateOwner, TemplatePackage, TemplatePackageEcosystem, TemplatePackagePage, TemplateRecipe, TemplateRecipeV2, TemplateRecipeV2File, TemplateService, TemplateSettings, TemplateSettingsInput, TemplateSource, TemplateStartCommand, TemplateStorage, TemplateStorageWarning, TemplateSummary, TemplateUpload, TemplateUploadRequest, TemplateUploadResponse, TemplateUploadResult, TemplateVersion, TemplateVersionRecipe, TemplateVersionState, UploadData, WaitForBuildOptions, WorkspaceStartup, } from './templates.js';
23
27
  export { SecretsApi, WorkspaceSecrets } from './secrets.js';
24
28
  export type { BoundSecretStatus, CreateOrganizationSecretParams, CreateSecretParams, Secret, SecretAccessEvent, SecretPermissions, SecretScope, SecretVersion, UpdateSecretParams, WorkspaceSecretBindings, } from './secrets.js';
25
29
  export { EgressPolicyApi } from './egress.js';
@@ -36,6 +40,9 @@ export { ToolTokenManager } from './tokens.js';
36
40
  export type { ToolName, ToolToken, ToolTokenOptions } from './tokens.js';
37
41
  export { ToolArgumentError, executeToolCall, toAnthropicTools, toOpenAITools, validateArgs, workspaceTools } from './tools.js';
38
42
  export type { JsonSchema, WorkspaceTool, WorkspaceToolsOptions } from './tools.js';
43
+ export { CaptureError, ToolCallCapture, captureTool } from './capture.js';
44
+ export type { CallLike, CallRef, CaptureCall, CaptureErrorKind, CaptureEvent, CaptureFlushResult, CapturePart, CaptureSelector, CaptureSource, CaptureStats, CaptureStatus, DropReason, ToolCallCaptureOptions, WrapOptions, } from './capture.js';
45
+ export type { AiSdkAdapter, AiSdkToolEndEvent, AnthropicAdapter, ClaudeAdapter, ClaudeCaptureHooks, ClaudeHookCallback, ClaudeHookMatcher, LangChainAdapter, LangChainToolHandler, MastraAdapter, MastraAfterToolCallContext, McpAdapter, OpenAIAgentsAdapter, } from './capture-adapters.js';
39
46
  export { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from './errors.js';
40
47
  export type { AppErrorCode, CellErrorCode, ErrorCode, ErrorReason, KnownErrorReason } from './errors.js';
41
48
  export { SDK_VERSION } from './http.js';
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  export { BillingApi, Shardflux, WorkspacesApi, fetchBillingCatalog, pickByKey } from "./client.js";
2
2
  export { formatTiming } from "./progress.js";
3
3
  export { UsageApi } from "./usage.js";
4
- export { TemplateBuildTimeoutError, TemplateBuildsApi, TemplateDraftApi, TemplatesApi, buildSettled, saveAsTemplateBody } from "./templates.js";
4
+ export { TemplateBuildTimeoutError, TemplateBuildsApi, TemplateDraftApi, TemplatePackagesApi, TemplateUploadError, TemplateUploadsApi, TemplateVersionTestInstancesApi, TemplateVersionsApi, TemplatesApi, buildSettled, saveAsTemplateBody, } from "./templates.js";
5
+ export { TemplateFileError, packDirectory, parseTemplateText, readTemplateFile } from "./template-file.js";
6
+ export { tarEnd, tarHeader, tarPadding } from "./tar.js";
5
7
  export { SecretsApi, WorkspaceSecrets } from "./secrets.js";
6
8
  export { EgressPolicyApi } from "./egress.js";
7
9
  export { VolumesApi } from "./volumes.js";
@@ -10,5 +12,6 @@ export { Workspace } from "./workspace.js";
10
12
  export { CellClient, DEFAULT_TRANSITION_TIMEOUT_MS, cellPath, ndjson } from "./cell.js";
11
13
  export { ToolTokenManager } from "./tokens.js";
12
14
  export { ToolArgumentError, executeToolCall, toAnthropicTools, toOpenAITools, validateArgs, workspaceTools } from "./tools.js";
15
+ export { CaptureError, ToolCallCapture, captureTool } from "./capture.js";
13
16
  export { OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError } from "./errors.js";
14
17
  export { SDK_VERSION } from "./http.js";
@@ -20,7 +20,8 @@ export interface LifecycleOptions {
20
20
  /**
21
21
  * Default (false): resolve once the change is requested; the returned operation is usually still `queued`.
22
22
  * `true` or WaitOptions: resolve once it has finished (the operation `succeeded`); throws OperationFailedError when it
23
- * fails and OperationTimeoutError after `timeoutMs` (default 5 minutes; the operation continues server side).
23
+ * fails and OperationTimeoutError after `timeoutMs` (default 5 minutes; the operation continues server side, a start
24
+ * waiting for capacity until its deadline, then it fails with `capacity_unavailable`, retryable).
24
25
  */
25
26
  wait?: boolean | WaitOptions;
26
27
  /** Progress events of this call (phases, retries, and `done` with its timing). */
@@ -45,4 +46,6 @@ export declare function waitOptionsOf(opts: LifecycleOptions): WaitOptions | nul
45
46
  * Starts a lifecycle operation with `start` and, when `opts.wait` asks for it, waits for it to finish. The trace covers
46
47
  * the request, every observed state, and `AFTER_WAIT`; it ends with `done` either way.
47
48
  */
48
- export declare function runLifecycle(ctx: ClientContext, kind: LifecycleAction, workspaceId: string, start: (init: Pick<RequestOptions, 'onRetry'>) => Promise<Operation>, opts: InternalLifecycleOptions): Promise<Operation>;
49
+ export declare function runLifecycle(ctx: ClientContext, kind: LifecycleAction, workspaceId: string, start: (init: Pick<RequestOptions, 'onRetry'>) => Promise<Operation>, opts: InternalLifecycleOptions, capture?: {
50
+ settle?: boolean;
51
+ }): Promise<Operation>;
package/dist/lifecycle.js CHANGED
@@ -13,10 +13,14 @@ export function waitOptionsOf(opts) {
13
13
  * Starts a lifecycle operation with `start` and, when `opts.wait` asks for it, waits for it to finish. The trace covers
14
14
  * the request, every observed state, and `AFTER_WAIT`; it ends with `done` either way.
15
15
  */
16
- export async function runLifecycle(ctx, kind, workspaceId, start, opts) {
16
+ export async function runLifecycle(ctx, kind, workspaceId, start, opts, capture = {}) {
17
17
  const waitOpts = waitOptionsOf(opts);
18
18
  const trace = new Trace(kind, combineListeners(ctx.onProgress, opts.onProgress, waitOpts?.onProgress), { workspaceId });
19
19
  return traced(trace, async () => {
20
+ // Tool-call capture writes recorded before this call land first (snapshot, fork, suspend and close include them).
21
+ const pending = capture.settle ? ctx.captures.settle(workspaceId) : undefined;
22
+ if (pending)
23
+ await trace.span('capture_flush', () => pending);
20
24
  trace.phase('request');
21
25
  const operation = await start({ onRetry: trace.onRetry });
22
26
  trace.observe(operation);
@@ -26,8 +26,10 @@ export type LifecycleAction = 'open' | 'suspend' | 'resume' | 'snapshot' | 'fork
26
26
  * operation's `state_reason` (e.g. `no_ready_host`, `template_downloading`).
27
27
  * - `view`: reading the workspace after the operation. `token`: issuing a tool token (concurrent with `view` in open()).
28
28
  * - `busy`: a tool call waiting out `workspace_busy` (only in `tool` events).
29
+ * - `capture_flush` (0.7.0+): a lifecycle call waiting for tool-call capture writes recorded before it (only when some
30
+ * were pending).
29
31
  */
30
- export type LifecyclePhase = 'request' | 'queued' | 'capacity_pending' | 'running' | 'view' | 'token' | 'busy';
32
+ export type LifecyclePhase = 'request' | 'queued' | 'capacity_pending' | 'running' | 'view' | 'token' | 'busy' | 'capture_flush';
31
33
  export interface TimingPhase {
32
34
  phase: LifecyclePhase;
33
35
  /** The server's state_reason, or why the SDK entered the phase (`held`, `initial`, `expiring`, `invalidated`). */
@@ -104,6 +106,8 @@ interface EventBase {
104
106
  }
105
107
  /**
106
108
  * - `phase`: a phase began (live progress: "capacity_pending: no_ready_host"). `tool` phases come from tool calls.
109
+ * A `capacity_pending` phase carries `deadlineAt` (0.6.2+, when the API reports it): when the start gives up waiting
110
+ * for a host and fails with `capacity_unavailable` (retryable; nothing was started).
107
111
  * - `retry`: a request is retried after a transient failure (also emitted for tool calls, action `tool`).
108
112
  * - `done`: the call ended, successfully or not, with its full timing.
109
113
  */
@@ -111,6 +115,7 @@ export type ProgressEvent = (EventBase & {
111
115
  type: 'phase';
112
116
  phase: LifecyclePhase;
113
117
  reason: string | null;
118
+ deadlineAt?: string;
114
119
  }) | (EventBase & {
115
120
  type: 'retry';
116
121
  retry: RetryRecord;
@@ -123,6 +128,12 @@ export type ProgressListener = (event: ProgressEvent) => void;
123
128
  export declare function emitTo(listeners: ReadonlyArray<ProgressListener | undefined>, event: ProgressEvent): void;
124
129
  /** Combines listeners (client-level and per call) into one; undefined when there are none. */
125
130
  export declare function combineListeners(...listeners: Array<ProgressListener | undefined>): ProgressListener | undefined;
131
+ /**
132
+ * When a start waiting in `capacity_pending` gives up: the operation's `error.details.deadline_at` (RFC 3339). A start no
133
+ * host admits by then fails with `capacity_unavailable` (retryable; nothing was started). Null in any other state, or
134
+ * from an API that does not report it.
135
+ */
136
+ export declare function capacityDeadlineOf(op: Operation): string | null;
126
137
  /** Server timing from an operation as GET /v1/operations/{id} returns it. */
127
138
  export declare function serverTiming(op: Operation): ServerTiming;
128
139
  /** Why a request failed, in one short phrase (for retry records). */
@@ -139,8 +150,11 @@ export declare class Trace {
139
150
  /** Milliseconds since the call began. */
140
151
  now(): number;
141
152
  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;
153
+ /**
154
+ * Enters a sequential phase (closing the current one). The same phase and reason again is not a new phase.
155
+ * `deadlineAt`: when a `capacity_pending` start gives up (added to the event only).
156
+ */
157
+ phase(phase: LifecyclePhase, reason?: string | null, deadlineAt?: string | null): void;
144
158
  /** Runs `fn` as a phase that may overlap others (open() reads the view and issues the token together). */
145
159
  span<T>(phase: LifecyclePhase, fn: () => Promise<T>, reason?: string | null): Promise<T>;
146
160
  /** Records an operation snapshot: its ids, the observed state as a phase, and the server timing. */
package/dist/progress.js CHANGED
@@ -30,6 +30,17 @@ function diffMs(from, to) {
30
30
  }
31
31
  const str = (v) => (typeof v === 'string' && v.length > 0 ? v : null);
32
32
  const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
33
+ /**
34
+ * When a start waiting in `capacity_pending` gives up: the operation's `error.details.deadline_at` (RFC 3339). A start no
35
+ * host admits by then fails with `capacity_unavailable` (retryable; nothing was started). Null in any other state, or
36
+ * from an API that does not report it.
37
+ */
38
+ export function capacityDeadlineOf(op) {
39
+ if (op.state !== 'capacity_pending')
40
+ return null;
41
+ const details = op.error?.details;
42
+ return typeof details === 'object' && details !== null ? str(details.deadline_at) : null;
43
+ }
33
44
  /** Server timing from an operation as GET /v1/operations/{id} returns it. */
34
45
  export function serverTiming(op) {
35
46
  const r = (op.result ?? {});
@@ -113,8 +124,11 @@ export class Trace {
113
124
  this.#current.durationMs = round(at - this.#current.startMs);
114
125
  this.#current = null;
115
126
  }
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) {
127
+ /**
128
+ * Enters a sequential phase (closing the current one). The same phase and reason again is not a new phase.
129
+ * `deadlineAt`: when a `capacity_pending` start gives up (added to the event only).
130
+ */
131
+ phase(phase, reason = null, deadlineAt = null) {
118
132
  if (this.#timing)
119
133
  return;
120
134
  if (this.#current && this.#current.phase === phase && this.#current.reason === reason)
@@ -123,7 +137,7 @@ export class Trace {
123
137
  this.#close(at);
124
138
  this.#current = { phase, reason, operationId: this.operationId, startMs: at, durationMs: 0 };
125
139
  this.#phases.push(this.#current);
126
- this.#emit({ ...this.#base(), type: 'phase', phase, reason });
140
+ this.#emit({ ...this.#base(), type: 'phase', phase, reason, ...(deadlineAt ? { deadlineAt } : {}) });
127
141
  }
128
142
  /** Runs `fn` as a phase that may overlap others (open() reads the view and issues the token together). */
129
143
  async span(phase, fn, reason = null) {
@@ -145,7 +159,7 @@ export class Trace {
145
159
  this.workspaceId ??= op.workspace_id;
146
160
  this.#server = serverTiming(op);
147
161
  if (!TERMINAL.has(op.state))
148
- this.phase(op.state, op.state_reason ?? null);
162
+ this.phase(op.state, op.state_reason ?? null, capacityDeadlineOf(op));
149
163
  }
150
164
  retry(r) {
151
165
  if (this.#timing)
package/dist/tar.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A small tar writer for build uploads of folders (contracts §24.2: uncompressed ustar/pax, extracted by the host's
3
+ * static tool). Pure: no Node imports, so the browser bundle can carry it.
4
+ *
5
+ * The bytes equal CPython's `tarfile.open(mode="w", format=tarfile.PAX_FORMAT)` with every member added by hand (the
6
+ * Python SDK's writer), so both SDKs upload the same bytes for the same folder and a template built from either has the
7
+ * same recipe_sha256:
8
+ * - a pax `x` header (`././@PaxHeader`) precedes a member whose name or link target is non-ASCII or longer than 100
9
+ * characters (`path`, `linkpath` records), or whose size does not fit the 11 octal digits (`size`);
10
+ * - the ustar header then holds the name encoded as ASCII with `?` for each non-ASCII code point, truncated to 100 bytes;
11
+ * - the archive ends with two zero blocks and is padded to a multiple of 10240 bytes (tarfile's RECORDSIZE).
12
+ */
13
+ export declare const TAR_BLOCK = 512;
14
+ export declare const TAR_RECORD_SIZE: number;
15
+ export type TarEntryType = 'file' | 'dir' | 'symlink';
16
+ export interface TarEntry {
17
+ /** Relative path with `/` separators; directories get their trailing `/` from the writer. */
18
+ path: string;
19
+ type: TarEntryType;
20
+ /** Permission bits (masked to 0o7777 as tarfile does). */
21
+ mode: number;
22
+ /** Bytes of a regular file (0 for directories and symlinks). */
23
+ size: number;
24
+ /** Symlink target. */
25
+ linkname?: string;
26
+ mtime?: number;
27
+ uid?: number;
28
+ gid?: number;
29
+ uname?: string;
30
+ gname?: string;
31
+ }
32
+ /** Zero padding after `size` bytes of member data. */
33
+ export declare function tarPadding(size: number): Uint8Array;
34
+ /**
35
+ * The header block(s) of one member: an optional pax extended header with its records, then the ustar header.
36
+ * Member data (a file's bytes) follows, then tarPadding(size).
37
+ */
38
+ export declare function tarHeader(entry: TarEntry): Uint8Array;
39
+ /** The end of an archive written so far (`offset` bytes): two zero blocks, then zeros up to a multiple of 10240. */
40
+ export declare function tarEnd(offset: number): Uint8Array;
package/dist/tar.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * A small tar writer for build uploads of folders (contracts §24.2: uncompressed ustar/pax, extracted by the host's
3
+ * static tool). Pure: no Node imports, so the browser bundle can carry it.
4
+ *
5
+ * The bytes equal CPython's `tarfile.open(mode="w", format=tarfile.PAX_FORMAT)` with every member added by hand (the
6
+ * Python SDK's writer), so both SDKs upload the same bytes for the same folder and a template built from either has the
7
+ * same recipe_sha256:
8
+ * - a pax `x` header (`././@PaxHeader`) precedes a member whose name or link target is non-ASCII or longer than 100
9
+ * characters (`path`, `linkpath` records), or whose size does not fit the 11 octal digits (`size`);
10
+ * - the ustar header then holds the name encoded as ASCII with `?` for each non-ASCII code point, truncated to 100 bytes;
11
+ * - the archive ends with two zero blocks and is padded to a multiple of 10240 bytes (tarfile's RECORDSIZE).
12
+ */
13
+ export const TAR_BLOCK = 512;
14
+ export const TAR_RECORD_SIZE = 20 * TAR_BLOCK;
15
+ const TYPEFLAG = { file: 0x30, dir: 0x35, symlink: 0x32 };
16
+ const utf8 = new TextEncoder();
17
+ function isAscii(s) {
18
+ for (let i = 0; i < s.length; i += 1)
19
+ if (s.charCodeAt(i) > 0x7f)
20
+ return false;
21
+ return true;
22
+ }
23
+ /** Python's `s.encode("ascii", "replace")`: one `?` per non-ASCII code point (not per UTF-16 unit). */
24
+ function asciiReplace(s) {
25
+ const out = [];
26
+ for (const ch of s)
27
+ out.push(ch.codePointAt(0) > 0x7f ? 0x3f : ch.charCodeAt(0));
28
+ return Uint8Array.from(out);
29
+ }
30
+ /** tarfile's stn(): the bytes truncated to `length` and NUL-padded. */
31
+ function putString(buf, offset, length, bytes) {
32
+ buf.set(bytes.subarray(0, length), offset);
33
+ }
34
+ /** tarfile's itn() for the POSIX range: `%0*o` with digits-1 digits and a NUL. */
35
+ function putOctal(buf, offset, digits, value) {
36
+ const text = `${value.toString(8).padStart(digits - 1, '0')}\0`;
37
+ buf.set(utf8.encode(text), offset);
38
+ }
39
+ /** Python's len(str): code points. */
40
+ function codePoints(s) {
41
+ return Array.from(s).length;
42
+ }
43
+ /** tarfile's _create_header(info, USTAR_FORMAT, "ascii", "replace"). */
44
+ function ustarHeader(info) {
45
+ const h = new Uint8Array(TAR_BLOCK);
46
+ putString(h, 0, 100, asciiReplace(info.name));
47
+ putOctal(h, 100, 8, info.mode & 0o7777);
48
+ putOctal(h, 108, 8, info.uid);
49
+ putOctal(h, 116, 8, info.gid);
50
+ putOctal(h, 124, 12, info.size);
51
+ putOctal(h, 136, 12, info.mtime);
52
+ h.fill(0x20, 148, 156);
53
+ h[156] = info.type;
54
+ putString(h, 157, 100, asciiReplace(info.linkname));
55
+ h.set(utf8.encode('ustar\x0000'), 257);
56
+ putString(h, 265, 32, asciiReplace(info.uname));
57
+ putString(h, 297, 32, asciiReplace(info.gname));
58
+ // devmajor, devminor and prefix stay NUL (tarfile writes empty strings for non-device members).
59
+ let sum = 0;
60
+ for (const b of h)
61
+ sum += b;
62
+ h.set(utf8.encode(`${sum.toString(8).padStart(6, '0')}\0`), 148);
63
+ return h;
64
+ }
65
+ /** Zero padding after `size` bytes of member data. */
66
+ export function tarPadding(size) {
67
+ const rest = size % TAR_BLOCK;
68
+ return new Uint8Array(rest === 0 ? 0 : TAR_BLOCK - rest);
69
+ }
70
+ /** tarfile's pax record: "%d %s=%s\n" where the length counts itself. */
71
+ function paxRecord(keyword, value) {
72
+ const kv = utf8.encode(`${keyword}=${value}\n`);
73
+ const l = kv.length + 1; // + ' '
74
+ let p = 0;
75
+ for (;;) {
76
+ const n = l + String(p).length;
77
+ if (n === p)
78
+ break;
79
+ p = n;
80
+ }
81
+ const head = utf8.encode(`${p} `);
82
+ const out = new Uint8Array(head.length + kv.length);
83
+ out.set(head, 0);
84
+ out.set(kv, head.length);
85
+ return out;
86
+ }
87
+ /**
88
+ * The header block(s) of one member: an optional pax extended header with its records, then the ustar header.
89
+ * Member data (a file's bytes) follows, then tarPadding(size).
90
+ */
91
+ export function tarHeader(entry) {
92
+ const name = entry.type === 'dir' && !entry.path.endsWith('/') ? `${entry.path}/` : entry.path;
93
+ const info = {
94
+ name,
95
+ mode: entry.mode,
96
+ uid: entry.uid ?? 0,
97
+ gid: entry.gid ?? 0,
98
+ size: entry.type === 'file' ? entry.size : 0,
99
+ mtime: entry.mtime ?? 0,
100
+ type: TYPEFLAG[entry.type],
101
+ linkname: entry.linkname ?? '',
102
+ uname: entry.uname ?? '',
103
+ gname: entry.gname ?? '',
104
+ };
105
+ const records = [];
106
+ for (const [field, keyword, length] of [
107
+ ['name', 'path', 100],
108
+ ['linkname', 'linkpath', 100],
109
+ ['uname', 'uname', 32],
110
+ ['gname', 'gname', 32],
111
+ ]) {
112
+ const value = info[field];
113
+ if (!isAscii(value) || codePoints(value) > length)
114
+ records.push(paxRecord(keyword, value));
115
+ }
116
+ for (const [field, digits] of [
117
+ ['uid', 8],
118
+ ['gid', 8],
119
+ ['size', 12],
120
+ ['mtime', 12],
121
+ ]) {
122
+ const value = info[field];
123
+ if (!(value >= 0 && value < 8 ** (digits - 1))) {
124
+ records.push(paxRecord(field, String(value)));
125
+ info[field] = 0;
126
+ }
127
+ }
128
+ const header = ustarHeader(info);
129
+ if (records.length === 0)
130
+ return header;
131
+ const total = records.reduce((n, r) => n + r.length, 0);
132
+ const payload = new Uint8Array(total + tarPadding(total).length);
133
+ let at = 0;
134
+ for (const r of records) {
135
+ payload.set(r, at);
136
+ at += r.length;
137
+ }
138
+ const pax = ustarHeader({ name: '././@PaxHeader', mode: 0, uid: 0, gid: 0, size: total, mtime: 0, type: 0x78, linkname: '', uname: '', gname: '' });
139
+ const out = new Uint8Array(pax.length + payload.length + header.length);
140
+ out.set(pax, 0);
141
+ out.set(payload, pax.length);
142
+ out.set(header, pax.length + payload.length);
143
+ return out;
144
+ }
145
+ /** The end of an archive written so far (`offset` bytes): two zero blocks, then zeros up to a multiple of 10240. */
146
+ export function tarEnd(offset) {
147
+ const end = offset + 2 * TAR_BLOCK;
148
+ const rest = end % TAR_RECORD_SIZE;
149
+ return new Uint8Array(2 * TAR_BLOCK + (rest === 0 ? 0 : TAR_RECORD_SIZE - rest));
150
+ }
@@ -0,0 +1,92 @@
1
+ type Fs = typeof import('node:fs');
2
+ type FsPromises = typeof import('node:fs/promises');
3
+ type Path = typeof import('node:path');
4
+ type Os = typeof import('node:os');
5
+ type Crypto = typeof import('node:crypto');
6
+ type Stream = {
7
+ Readable: typeof import('node:stream').Readable;
8
+ };
9
+ interface NodeModules {
10
+ fs: Fs;
11
+ fsp: FsPromises;
12
+ path: Path;
13
+ os: Os;
14
+ crypto: Crypto;
15
+ stream: Stream;
16
+ }
17
+ /** The Node modules this file needs, loaded once (throws outside Node). */
18
+ export declare function nodeModules(): Promise<NodeModules>;
19
+ /** A template file that cannot be read, parsed or packed (before any request is made). */
20
+ export declare class TemplateFileError extends Error {
21
+ /** The template file or local path concerned, when there is one. */
22
+ readonly path: string | undefined;
23
+ constructor(message: string, path?: string);
24
+ }
25
+ /** Largest single upload (contracts §24.2: 5 GiB, one presigned PUT). */
26
+ export declare const UPLOAD_BYTES_MAX = 5368709120;
27
+ /** Most entries a folder tar may have (the host's extraction limit, §24.2). */
28
+ export declare const TAR_ENTRIES_MAX = 200000;
29
+ export declare const RECIPE_V2_SCHEMA = "shardflux.template-recipe.v2";
30
+ /** Parses YAML text: the optional `yaml` package's parse() unless the caller passes its own. */
31
+ export type YamlParser = (text: string) => unknown;
32
+ /**
33
+ * The document of a template file: `.json` is JSON, anything else YAML (JSON is valid YAML too). YAML needs the
34
+ * optional `yaml` package (`npm install yaml`) or `parseYaml`; without either, a YAML file that is not valid JSON fails.
35
+ */
36
+ export declare function parseTemplateText(text: string, fileName: string, parseYaml?: YamlParser): Promise<Record<string, unknown>>;
37
+ /** The recipe v2 document: a mapping whose `schema` is recipe v2 (filled in when absent). */
38
+ export declare function normalizeTemplateDocument(doc: unknown, where?: string): Record<string, unknown>;
39
+ /** Reads and parses a template file (template.yaml or .json). */
40
+ export declare function readTemplateFile(file: string, opts?: {
41
+ parseYaml?: YamlParser;
42
+ }): Promise<Record<string, unknown>>;
43
+ export interface PackedFile {
44
+ sha256: string;
45
+ size: number;
46
+ /** Tar members (folders), null for a file uploaded as it is. */
47
+ entries: number | null;
48
+ }
49
+ /** SHA-256 (lower-case hex) and size of a local file, streamed. */
50
+ export declare function hashFile(file: string): Promise<{
51
+ sha256: string;
52
+ size: number;
53
+ }>;
54
+ /**
55
+ * Packs the folder `root` into `dest` as the reproducible tar (tar.ts; byte-identical to the Python SDK's): members in
56
+ * UTF-8 path order, mtime 0, uid/gid 0 without names, permission bits kept (symlinks 0777). Returns its sha256, size and
57
+ * member count.
58
+ */
59
+ export declare function packDirectory(root: string, dest: string): Promise<PackedFile>;
60
+ /** Refuses `target` when it resolves, after symlinks, outside `root` (an agent-facing server confines local reads). */
61
+ export declare function assertInside(root: string, target: string, what: string): Promise<void>;
62
+ /** One `from` entry resolved on the disk: what gets uploaded and how. */
63
+ export interface LocalSource {
64
+ /** `from` as written. */
65
+ from: string;
66
+ /** The resolved local path. */
67
+ path: string;
68
+ kind: 'file' | 'tar';
69
+ /** The bytes to upload: the file itself, or the packed tar in a temporary directory. */
70
+ uploadPath: string;
71
+ sha256: string;
72
+ size: number;
73
+ entries: number | null;
74
+ }
75
+ /**
76
+ * Resolves `from` (relative to `baseDir`), checks it against `root` when given (after symlinks), and packs or hashes
77
+ * it. `kind` omitted: a folder is `tar`, a file is `file`. A folder with kind `file` is refused; a file with kind `tar`
78
+ * is a prepared (uncompressed) archive.
79
+ */
80
+ export declare function resolveLocalSource(from: string, kind: 'file' | 'tar' | undefined, opts: {
81
+ baseDir: string;
82
+ root?: string | undefined;
83
+ tmpDir: () => Promise<string>;
84
+ }): Promise<LocalSource>;
85
+ /** A fetch body that streams a local file (replayable: each call opens it again). */
86
+ export declare function fileBody(file: string): Promise<ReadableStream<Uint8Array>>;
87
+ /** A private temporary directory, created on first use; `cleanup()` removes it. */
88
+ export declare function tempDir(): {
89
+ get: () => Promise<string>;
90
+ cleanup: () => Promise<void>;
91
+ };
92
+ export {};