@actana/sdk 0.2.2 → 0.3.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,94 @@
1
+ import type { CoreLinkTlsMaterial } from "./core-link-socket.ts";
2
+ /** The refusal codes the Core's file routes answer with (`docs/external-api.md`). */
3
+ export type CoreFilesErrorCode = "unauthorized" | "not-found" | "project-not-found" | "method-not-allowed" | "bad-request" | "absolute-path" | "dot-dot-segment" | "outside-project-root" | "malformed-path" | "transfer-in-progress" | "insufficient-storage" | "corrupt-archive" | "absolute-entry-path" | "dot-dot-entry-path" | "root-entry-path" | "entry-outside-root" | "unsupported-entry-type" | "hardlink-outside-root" | "symlink-outside-root" | "directory-in-the-way" | "write-failed" | "read-failed";
4
+ /** Base class for everything `project.files.*` throws, so one `catch` can name it. */
5
+ export declare class CoreFilesError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * This Core has no file surface (F9, #165's `files` capability).
10
+ *
11
+ * Thrown *before any request goes out*, which is the point: a client that
12
+ * called the route anyway would read a `404` off a Core that simply predates the
13
+ * surface and report it as an outage. The `reason` is prose for an operator
14
+ * because "unavailable" on its own sends somebody to check their network.
15
+ */
16
+ export declare class CoreFilesUnavailableError extends CoreFilesError {
17
+ readonly reason: string;
18
+ constructor(reason: string);
19
+ }
20
+ /** A refusal with a status line: the Core answered, and said no. */
21
+ export declare class CoreFilesRequestError extends CoreFilesError {
22
+ readonly status: number;
23
+ readonly code: CoreFilesErrorCode | string;
24
+ constructor(status: number, code: CoreFilesErrorCode | string, message: string);
25
+ }
26
+ /**
27
+ * The one-write-per-Project rule (F8), as an error and **never as a retry**.
28
+ *
29
+ * `409 transfer-in-progress` is a conflict a human has to resolve — some other
30
+ * transfer is running on this Project right now, possibly a multi-gigabyte one
31
+ * with twenty minutes left. A client that retried under the hood would convert
32
+ * the Core's clear, immediate refusal into a hang with no output, which is
33
+ * strictly worse than the error: the operator loses both the reason and the
34
+ * chance to decide. So nothing in this package sleeps, backs off or re-sends —
35
+ * this class *is* the handling, and the decision is the caller's.
36
+ *
37
+ * `directory-in-the-way` shares the status and the same rule: it is a refusal
38
+ * to delete a tree, and it will still be a directory on the next attempt.
39
+ */
40
+ export declare class CoreFilesConflictError extends CoreFilesRequestError {
41
+ constructor(status: number, code: CoreFilesErrorCode | string, message: string);
42
+ }
43
+ /**
44
+ * A failure that arrived as the **last line of a progress stream** rather than
45
+ * as a status code, because the `200` was spent on the first entry.
46
+ *
47
+ * This is why the progress stream is NDJSON and not a JSON document: a document
48
+ * would have to be well-formed to be read at all, and this one stops in the
49
+ * middle by design.
50
+ */
51
+ export declare class CoreFilesStreamError extends CoreFilesError {
52
+ readonly code: CoreFilesErrorCode | string;
53
+ constructor(code: CoreFilesErrorCode | string, message: string);
54
+ }
55
+ /** One request, in the terms this surface actually uses. */
56
+ export type CoreFilesRequest = {
57
+ method: "GET" | "HEAD" | "PUT";
58
+ url: string;
59
+ headers: Record<string, string>;
60
+ /** A stream, never a buffer — see {@link CoreFiles.upload}. */
61
+ body?: ReadableStream<Uint8Array> | null;
62
+ signal?: AbortSignal;
63
+ };
64
+ /**
65
+ * How a file request is actually sent.
66
+ *
67
+ * Injected rather than reached for, on the same terms as
68
+ * {@link CoreLinkSocketFactory}: it is what lets a suite drive this surface
69
+ * over a plain loopback server, and what lets a runtime with its own
70
+ * dispatcher — a proxy, a recorder, a browser — supply one. The default is
71
+ * {@link createCoreFilesFetch}.
72
+ */
73
+ export type CoreFilesFetch = (req: CoreFilesRequest) => Promise<Response>;
74
+ /**
75
+ * The default sender: `fetch`, with the client certificate carried in on a
76
+ * dispatcher.
77
+ *
78
+ * The `Agent` is built **once, lazily, and only when there is TLS material** —
79
+ * a `http://` loopback Core needs no dispatcher, and building one per request
80
+ * would open a fresh connection pool for every upload.
81
+ */
82
+ export declare function createCoreFilesFetch(tls: CoreLinkTlsMaterial | null): CoreFilesFetch;
83
+ /**
84
+ * Turn a non-2xx answer into the right error class, reading the `code` beside
85
+ * the prose rather than the prose.
86
+ *
87
+ * A client branches on `code` because a message is a sentence somebody will
88
+ * reword — and `transfer-in-progress` in particular *has* to stay
89
+ * distinguishable (F8). The body is read to completion here, and only here: a
90
+ * refusal body is a short JSON object, which is why buffering it does not
91
+ * violate the streaming rule the success paths keep.
92
+ */
93
+ export declare function refusalFrom(res: Response, what: string): Promise<CoreFilesRequestError>;
94
+ //# sourceMappingURL=core-files-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-files-http.d.ts","sourceRoot":"","sources":["../src/core-files-http.ts"],"names":[],"mappings":"AA2DA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,WAAW,GACX,mBAAmB,GACnB,oBAAoB,GACpB,aAAa,GACb,eAAe,GACf,iBAAiB,GACjB,sBAAsB,GACtB,gBAAgB,GAChB,sBAAsB,GACtB,sBAAsB,GACtB,iBAAiB,GACjB,qBAAqB,GACrB,oBAAoB,GACpB,iBAAiB,GACjB,oBAAoB,GACpB,wBAAwB,GACxB,uBAAuB,GACvB,sBAAsB,GACtB,sBAAsB,GACtB,cAAc,GACd,aAAa,CAAC;AAElB,sFAAsF;AACtF,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,qBAAa,yBAA0B,SAAQ,cAAc;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE,MAAM;CAK3B;AAED,oEAAoE;AACpE,qBAAa,qBAAsB,SAAQ,cAAc;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAAC;gBAE/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM;CAM/E;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,sBAAuB,SAAQ,qBAAqB;gBACnD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM;CAI/E;AAED;;;;;;;GAOG;AACH,qBAAa,oBAAqB,SAAQ,cAAc;IACtD,QAAQ,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAAC;gBAE/B,IAAI,EAAE,kBAAkB,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM;CAK/D;AAED,4DAA4D;AAC5D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IACzC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE1E;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,GAAG,cAAc,CA8BpF;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAe7F"}
@@ -0,0 +1,198 @@
1
+ // The SDK's first HTTPS surface (#167, F12) — how a file request reaches a
2
+ // Core, and what a refusal from one means.
3
+ //
4
+ // Everything in this package before this module spoke one protocol: the
5
+ // core-link WebSocket. File bytes deliberately do not cross it (ADR 0028), so
6
+ // `project.files.*` dials the *same* mTLS server on the *same* port with the
7
+ // *same* certificate and bearer, over HTTP instead of a WebSocket upgrade.
8
+ // `CoreConnection.httpsBaseUrl` has named that origin since phase 1 precisely
9
+ // so this module would not be the thing that changed a published shape.
10
+ //
11
+ // ## The `fetch` shape is frozen, not chosen here
12
+ //
13
+ // The phase-1 spike (#151, PR 189) settled how a client certificate reaches
14
+ // `fetch` and its verdict is reused verbatim rather than rediscovered:
15
+ //
16
+ // const agent = new Agent({ connect: { ca, cert, key } });
17
+ // await fetch(url, { dispatcher: agent });
18
+ //
19
+ // **`fetch` is undici and ignores `options.cert` / `options.key`.** There is no
20
+ // option bag on `fetch` that reaches TLS at all — the dispatcher is the only
21
+ // seam. A client built the `https.request` way silently makes an *anonymous*
22
+ // connection, which a Core with `requestCert: true, rejectUnauthorized: true`
23
+ // refuses at the handshake with an error that names no certificate; that is a
24
+ // long debugging session, and it is the whole reason the spike exists. The
25
+ // spike's leg 2b is the control that makes the shape mean anything: the same
26
+ // `fetch` through a CA-only `Agent` is refused at the TLS layer. D12 also held
27
+ // — both legs pass on Node 22 — so this package's `engines: ">=22"` stands.
28
+ //
29
+ // ### One thing the spike could not have found, and this had to
30
+ //
31
+ // **The `fetch` above is undici's own, not the global one**, and the two are
32
+ // not interchangeable. Node embeds a *copy* of undici for its global `fetch`,
33
+ // and a `Dispatcher` only satisfies the implementation it came from: handing a
34
+ // dispatcher from the `undici` package to Node 24's built-in `fetch` fails with
35
+ // `InvalidArgumentError: invalid onRequestStart method`, because undici 8
36
+ // changed the handler interface that the embedded copy still expects. The
37
+ // spike never met this — it loaded `ws` and `undici` as a matching pair from
38
+ // one directory and used the global `fetch`, which happened to agree at those
39
+ // versions.
40
+ //
41
+ // Importing both halves from the same package is what makes that class of skew
42
+ // impossible rather than merely absent today: this package's `fetch` and its
43
+ // `Agent` are the same implementation, whatever Node bundles underneath. The
44
+ // frozen finding is untouched — the dispatcher is still the only seam, and the
45
+ // CA-only control still has to be refused — and `core-files-mtls.test.ts` holds
46
+ // both against a real handshake.
47
+ //
48
+ // ## Why `undici` is this package's second dependency
49
+ //
50
+ // `ws` is here because Node's global `WebSocket` cannot present a client
51
+ // certificate. `undici` is here for the identical reason one layer up: Node's
52
+ // global `fetch` cannot either, and the `Agent` that lets it is not exposed by
53
+ // any builtin. It is a public package on the registry, so unlike `@actana/shared`
54
+ // it is resolvable by everyone who installs this SDK — the boundary ADR 0025 D4
55
+ // draws is around *private workspace* packages, not around dependencies as such.
56
+ // A caller who never dials `https://` never constructs an `Agent`: the dispatcher
57
+ // is built lazily, on the first request that needs one.
58
+ import { Agent, fetch as undiciFetch } from "undici";
59
+ /** Base class for everything `project.files.*` throws, so one `catch` can name it. */
60
+ export class CoreFilesError extends Error {
61
+ constructor(message) {
62
+ super(message);
63
+ this.name = "CoreFilesError";
64
+ }
65
+ }
66
+ /**
67
+ * This Core has no file surface (F9, #165's `files` capability).
68
+ *
69
+ * Thrown *before any request goes out*, which is the point: a client that
70
+ * called the route anyway would read a `404` off a Core that simply predates the
71
+ * surface and report it as an outage. The `reason` is prose for an operator
72
+ * because "unavailable" on its own sends somebody to check their network.
73
+ */
74
+ export class CoreFilesUnavailableError extends CoreFilesError {
75
+ reason;
76
+ constructor(reason) {
77
+ super(reason);
78
+ this.name = "CoreFilesUnavailableError";
79
+ this.reason = reason;
80
+ }
81
+ }
82
+ /** A refusal with a status line: the Core answered, and said no. */
83
+ export class CoreFilesRequestError extends CoreFilesError {
84
+ status;
85
+ code;
86
+ constructor(status, code, message) {
87
+ super(message);
88
+ this.name = "CoreFilesRequestError";
89
+ this.status = status;
90
+ this.code = code;
91
+ }
92
+ }
93
+ /**
94
+ * The one-write-per-Project rule (F8), as an error and **never as a retry**.
95
+ *
96
+ * `409 transfer-in-progress` is a conflict a human has to resolve — some other
97
+ * transfer is running on this Project right now, possibly a multi-gigabyte one
98
+ * with twenty minutes left. A client that retried under the hood would convert
99
+ * the Core's clear, immediate refusal into a hang with no output, which is
100
+ * strictly worse than the error: the operator loses both the reason and the
101
+ * chance to decide. So nothing in this package sleeps, backs off or re-sends —
102
+ * this class *is* the handling, and the decision is the caller's.
103
+ *
104
+ * `directory-in-the-way` shares the status and the same rule: it is a refusal
105
+ * to delete a tree, and it will still be a directory on the next attempt.
106
+ */
107
+ export class CoreFilesConflictError extends CoreFilesRequestError {
108
+ constructor(status, code, message) {
109
+ super(status, code, message);
110
+ this.name = "CoreFilesConflictError";
111
+ }
112
+ }
113
+ /**
114
+ * A failure that arrived as the **last line of a progress stream** rather than
115
+ * as a status code, because the `200` was spent on the first entry.
116
+ *
117
+ * This is why the progress stream is NDJSON and not a JSON document: a document
118
+ * would have to be well-formed to be read at all, and this one stops in the
119
+ * middle by design.
120
+ */
121
+ export class CoreFilesStreamError extends CoreFilesError {
122
+ code;
123
+ constructor(code, message) {
124
+ super(message);
125
+ this.name = "CoreFilesStreamError";
126
+ this.code = code;
127
+ }
128
+ }
129
+ /**
130
+ * The default sender: `fetch`, with the client certificate carried in on a
131
+ * dispatcher.
132
+ *
133
+ * The `Agent` is built **once, lazily, and only when there is TLS material** —
134
+ * a `http://` loopback Core needs no dispatcher, and building one per request
135
+ * would open a fresh connection pool for every upload.
136
+ */
137
+ export function createCoreFilesFetch(tls) {
138
+ let dispatcher = null;
139
+ return async (req) => {
140
+ if (tls && !dispatcher) {
141
+ dispatcher = new Agent({ connect: { ca: tls.ca, cert: tls.cert, key: tls.key } });
142
+ }
143
+ // `duplex: "half"` is mandatory for a streamed request body and is not a
144
+ // detail that can be left out: without it `fetch` rejects the body outright,
145
+ // which would take this surface straight back to buffering an upload in
146
+ // memory — the one thing #167 exists to not do.
147
+ //
148
+ // The casts are the type-level shadow of the same two-copies problem the
149
+ // header describes: `@types/node` declares the global `fetch` with its own
150
+ // bundled `undici-types`, and this module deliberately uses the real
151
+ // package's `fetch` instead. The two `Response` and `RequestInit`
152
+ // declarations are structurally near-identical and neither is wrong — they
153
+ // are simply not the same declaration. Everything this package reads off a
154
+ // response (`ok`, `status`, `headers.get`, `json`, `body`) is common to
155
+ // both, and the global `Response` is what callers and tests hold, so that
156
+ // is the type this surface promises.
157
+ const init = {
158
+ method: req.method,
159
+ headers: req.headers,
160
+ ...(req.body ? { body: req.body, duplex: "half" } : {}),
161
+ ...(req.signal ? { signal: req.signal } : {}),
162
+ ...(dispatcher ? { dispatcher } : {}),
163
+ };
164
+ const res = await undiciFetch(req.url, init);
165
+ return res;
166
+ };
167
+ }
168
+ /**
169
+ * Turn a non-2xx answer into the right error class, reading the `code` beside
170
+ * the prose rather than the prose.
171
+ *
172
+ * A client branches on `code` because a message is a sentence somebody will
173
+ * reword — and `transfer-in-progress` in particular *has* to stay
174
+ * distinguishable (F8). The body is read to completion here, and only here: a
175
+ * refusal body is a short JSON object, which is why buffering it does not
176
+ * violate the streaming rule the success paths keep.
177
+ */
178
+ export async function refusalFrom(res, what) {
179
+ let code = "";
180
+ let message = "";
181
+ try {
182
+ const body = (await res.json());
183
+ if (typeof body.code === "string")
184
+ code = body.code;
185
+ if (typeof body.error === "string")
186
+ message = body.error;
187
+ }
188
+ catch {
189
+ // A refusal that is not the Core's JSON shape — a proxy's HTML error page,
190
+ // a truncated body. Say what is actually known rather than inventing a code.
191
+ }
192
+ const described = message || `${what} was refused with HTTP ${res.status}`;
193
+ const withCode = code ? `${described} (${code})` : described;
194
+ if (res.status === 409)
195
+ return new CoreFilesConflictError(res.status, code || "conflict", withCode);
196
+ return new CoreFilesRequestError(res.status, code || `http-${res.status}`, withCode);
197
+ }
198
+ //# sourceMappingURL=core-files-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-files-http.js","sourceRoot":"","sources":["../src/core-files-http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,227 @@
1
+ import { type CoreFilesFetch } from "./core-files-http.ts";
2
+ /**
3
+ * Whether this Core's file surface may be used at all, and — when it may not —
4
+ * why, in words an operator can act on (F9).
5
+ */
6
+ export type CoreFilesAvailability = {
7
+ available: true;
8
+ } | {
9
+ available: false;
10
+ reason: string;
11
+ };
12
+ /** How a written entry landed. `overwritten` names a file that was already there (F5). */
13
+ export type CoreFileWriteResult = "written" | "overwritten";
14
+ /**
15
+ * One entry of a listing, in the manifest shape PR 215 established: `{path,
16
+ * size, mtime, mode, sha256}`.
17
+ *
18
+ * `path` is **Project-relative**, which is the address space F1 gives the
19
+ * operator — the same string that goes back to `download`.
20
+ *
21
+ * `sha256` is nullable on purpose, and the Core settled which way it means:
22
+ * hashing every entry of a large tree is expensive, so a listing computes
23
+ * digests **on request** ({@link CoreFileListOptions.sha256}) and a transfer
24
+ * computes them eagerly, the bytes being already in hand. So `null` reads as
25
+ * "no bytes" for a directory and "nobody asked" for anything else.
26
+ */
27
+ export type CoreFileEntry = {
28
+ path: string;
29
+ size: number;
30
+ mtime: number;
31
+ mode: number;
32
+ sha256: string | null;
33
+ /** Present when the listing distinguishes them; absent listings read as files. */
34
+ kind?: "file" | "directory" | "symlink";
35
+ };
36
+ /** One line of a write's NDJSON progress stream. */
37
+ export type CoreFileProgress = ({
38
+ type: "entry";
39
+ result: CoreFileWriteResult;
40
+ } & CoreFileEntry) | {
41
+ type: "done";
42
+ entries: number;
43
+ bytes: number;
44
+ };
45
+ /**
46
+ * What a read hands back: metadata, and **a stream**.
47
+ *
48
+ * There is no `bytes` or `buffer` field here and that absence is the feature.
49
+ * A gigabyte file must not materialise in memory, so the only way to get at the
50
+ * content is to consume `stream` — a caller who wants it whole has to write
51
+ * that themselves and own the decision. `size` is what the Core declared, which
52
+ * is `null` for a folder, whose tar length is not known until it has been walked.
53
+ */
54
+ export type CoreFileDownload = {
55
+ /** `file` for raw bytes, `tar` for a folder crossing as one archive (ADR 0029). */
56
+ kind: "file" | "tar";
57
+ size: number | null;
58
+ mode: number | null;
59
+ mtime: number | null;
60
+ stream: ReadableStream<Uint8Array>;
61
+ };
62
+ /**
63
+ * Anything a caller can upload.
64
+ *
65
+ * A Node `Readable` — `fs.createReadStream(…)` — is an async iterable of
66
+ * `Buffer`, so it is accepted directly and without this package importing
67
+ * `node:stream` to say so. A web `ReadableStream` passes through untouched.
68
+ */
69
+ export type CoreFileSource = ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | string>;
70
+ export type CoreFileUploadOptions = {
71
+ /** Project-relative destination. `""` is the Project root, which only a tar may target. */
72
+ path: string;
73
+ body: CoreFileSource;
74
+ /**
75
+ * `file` writes one file at `path`; `tar` unpacks an archive into it
76
+ * (ADR 0029). Default `file`.
77
+ */
78
+ kind?: "file" | "tar";
79
+ /** Unix permission bits for a single-file write. The Core defaults to `0o644`. */
80
+ mode?: number;
81
+ /** Modification time in epoch milliseconds, preserved across the wire. */
82
+ mtime?: number;
83
+ /**
84
+ * Declared body length, when the caller knows it.
85
+ *
86
+ * Worth passing: it is what lets the Core run its free-space precheck and
87
+ * refuse `507 insufficient-storage` before the transfer rather than fail with
88
+ * `ENOSPC` half-way through. Omitted, the upload is chunked and there is
89
+ * nothing to check against.
90
+ */
91
+ contentLength?: number;
92
+ signal?: AbortSignal;
93
+ };
94
+ export type CoreFileDownloadOptions = {
95
+ /** Project-relative path. A directory comes back as one streamed tar. */
96
+ path: string;
97
+ signal?: AbortSignal;
98
+ };
99
+ export type CoreFileListOptions = {
100
+ /** Subtree to list, Project-relative. Default: the whole Project. */
101
+ path?: string;
102
+ /** Maximum depth to descend. `1` is the immediate children. Default: the whole tree. */
103
+ depth?: number;
104
+ /**
105
+ * Ask the Core to compute {@link CoreFileEntry.sha256} for every file and
106
+ * symlink under the path. **Off by default, and not free**: a listing does not
107
+ * have the bytes in hand, so digests mean reading every one of them (ADR 0027
108
+ * D6). Left off, every `sha256` comes back `null`.
109
+ */
110
+ sha256?: boolean;
111
+ signal?: AbortSignal;
112
+ };
113
+ export type CoreFilesOptions = {
114
+ projectId: string;
115
+ /** The Core's HTTPS origin — `CoreConnection.httpsBaseUrl`. No trailing slash. */
116
+ baseUrl: string;
117
+ /** The same signed bearer the core link's `auth` frame presents, or null on a loopback rig. */
118
+ bearer: string | null;
119
+ /**
120
+ * Read at the top of every call rather than once at construction: a Core can
121
+ * be downgraded and a client survives reconnects, so the answer belongs to
122
+ * the *current* connection and a remembered one would send a caller at a route
123
+ * that is no longer there.
124
+ */
125
+ availability: () => CoreFilesAvailability;
126
+ fetch: CoreFilesFetch;
127
+ };
128
+ /**
129
+ * A Project's files, over the Core's HTTPS routes.
130
+ *
131
+ * Reached as `client.project(id).files` rather than constructed directly.
132
+ */
133
+ export declare class CoreFiles {
134
+ private readonly opts;
135
+ constructor(opts: CoreFilesOptions);
136
+ /**
137
+ * The Project's tree, one entry at a time.
138
+ *
139
+ * Reads the Core's real listing route — `GET …/files/list`, see
140
+ * {@link listUrl} — which streams `{path, kind, size, mtime, mode, sha256}`
141
+ * per entry as NDJSON. `core-files-list-contract.test.ts` drives this method
142
+ * against that route in process and runs in **both** packages' suites, so the
143
+ * two halves of the URL cannot drift apart again without a red test (#218).
144
+ *
145
+ * Both line shapes are accepted — a bare manifest entry and one wrapped as
146
+ * `{type: "entry", …}`, which is what the Core's listing and its write route
147
+ * both emit. `skipped` lines are passed over: a path the walk could not read
148
+ * is a fact about the tree, not an entry in it, and not a reason to stop.
149
+ */
150
+ list(opts?: CoreFileListOptions): AsyncGenerator<CoreFileEntry, void, undefined>;
151
+ /**
152
+ * Write a stream into the Project, and watch it land.
153
+ *
154
+ * The returned iterable is the Core's NDJSON progress stream, parsed: one
155
+ * `entry` line per file — each carrying `result: "written" | "overwritten"`,
156
+ * so **every overwrite is named** rather than inferred — then one `done` line
157
+ * with the totals.
158
+ *
159
+ * **Lazy, like everything else here.** Calling `upload` sends nothing; the
160
+ * request goes out on the first `next()`. A caller who does not care about
161
+ * progress still has to drain the iterable, and that is the honest shape: the
162
+ * alternative is a method that returns a promise and quietly buffers a
163
+ * gigabyte of progress nobody read.
164
+ *
165
+ * Throws {@link CoreFilesConflictError} when another write already holds this
166
+ * Project's lease (F8) — immediately, and without retrying. Throws
167
+ * {@link CoreFilesStreamError} when the write fails part-way through, which
168
+ * arrives as the stream's last line rather than as a status code, the `200`
169
+ * having been spent on the first entry.
170
+ */
171
+ upload(opts: CoreFileUploadOptions): AsyncGenerator<CoreFileProgress, void, undefined>;
172
+ /**
173
+ * Read a file — or a folder, as one streamed tar — **as a stream**.
174
+ *
175
+ * The response body is handed through untouched. Nothing on this path calls
176
+ * `arrayBuffer`, `text` or `blob`, so a gigabyte file crosses in whatever
177
+ * chunks the socket delivers and only ever occupies what the consumer has not
178
+ * yet read. `core-files-streaming.test.ts` holds that claim to a real
179
+ * gigabyte and watches the heap while it crosses.
180
+ *
181
+ * The metadata comes off headers the Core already had — `stat` on the file it
182
+ * was about to open — so nothing is read twice to produce it.
183
+ */
184
+ download(opts: CoreFileDownloadOptions): Promise<CoreFileDownload>;
185
+ /**
186
+ * `…/v1/projects/:projectId/files/list?path=<relative>` — a route of its own.
187
+ *
188
+ * This module used to send `?list=1` on the read route instead, and argued for
189
+ * it on cost: the Core's `parseRoute` matched `/v1/projects/:id/files` on an
190
+ * exact four-segment split, so a `…/files/list` leaf was a change to that
191
+ * parser and a query parameter was not. That cost is now paid — #216 shipped a
192
+ * parser that reads the fifth segment — and what is left is the Core's
193
+ * argument, which was never about cost: a listing and a read of the same
194
+ * folder answer with completely different things, one a manifest and one a
195
+ * tar. A query parameter that a proxy, a redirect or a hand-edited URL can
196
+ * drop turns "list this folder" into "download this folder", which for a
197
+ * `node_modules` is a mistake measured in gigabytes rather than in a 400. A
198
+ * path segment cannot be dropped silently. See issue 218.
199
+ *
200
+ * Still `protected`: a caller pinned to an older Core can subclass rather than
201
+ * wait for a release.
202
+ */
203
+ protected listUrl(opts: CoreFileListOptions): string;
204
+ /** `…/v1/projects/:projectId/files?path=<relative>` — the read and write route. */
205
+ protected fileUrl(filePath: string): string;
206
+ /**
207
+ * `<baseUrl>/v1/projects/:projectId/<leaf>`, with the Project id escaped.
208
+ *
209
+ * The two routes above differ by their leaf and by nothing else, and that is
210
+ * worth having in one place: the day this surface gains a third, the origin,
211
+ * the version prefix and the escaping should not be a third opportunity to get
212
+ * one of them subtly wrong.
213
+ */
214
+ private routeUrl;
215
+ private authHeaders;
216
+ /**
217
+ * The F9 gate, checked before every call and *before any byte leaves*.
218
+ *
219
+ * The reason is carried into the error rather than flattened to a boolean,
220
+ * because the two ways this fails want different actions from an operator: a
221
+ * Core that has not finished connecting is a wait, and a Core that announced
222
+ * no `files` capability is a Core that predates the surface — which is a
223
+ * supported state, not a fault and not a needs-update.
224
+ */
225
+ private requireAvailable;
226
+ }
227
+ //# sourceMappingURL=core-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-files.d.ts","sourceRoot":"","sources":["../src/core-files.ts"],"names":[],"mappings":"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"}