@actana/sdk 0.2.2 → 0.3.2

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,123 @@
1
+ // The refusal vocabulary of the Core's file surface — one definition, for both
2
+ // ends of the wire (#224).
3
+ //
4
+ // ## Why this file exists rather than a union in each package
5
+ //
6
+ // This union was written out twice, verbatim and in the same order: once in
7
+ // `packages/core/src/core-files-routes.ts`, which is the side that *sends* the
8
+ // codes, and once in `packages/sdk/src/core-files-http.ts`, which is the side
9
+ // that *reads* them. The two agreed, member for member, the whole time they
10
+ // existed — so nothing here is a repair. What made it worth a ticket is that
11
+ // the two copies were not equally load-bearing, and the asymmetry ran the wrong
12
+ // way round:
13
+ //
14
+ // - The Core's copy is **checked**. `Refusal` types every refusal the routes
15
+ // send, so a code outside the union is a compile error, and the two narrower
16
+ // lists inside that package — `TarRefusalCode` in `files-tar.ts` and
17
+ // `FileConfinementRefusal` in `files-confinement.ts` — are pinned as subsets
18
+ // by assignment. Inside the Core, a copy cannot drift.
19
+ // - The SDK's copy was **not checked, deliberately**. Every use site widens it
20
+ // to `CoreFilesErrorCode | string`, because a client that meets a Core newer
21
+ // than itself has to survive a code it has never heard of. That widening is
22
+ // correct and it survives this change (see `core-files-http.ts`) — but it
23
+ // also means a member missing from the SDK's list was a type error nowhere.
24
+ //
25
+ // So the one place the copy could drift was the one place nothing would notice,
26
+ // and the drift would not surface as a break. It would surface as two processes
27
+ // disagreeing on a wire while each typechecks cleanly: the Core answers a code
28
+ // the client cannot name, every consumer's `catch` still compiles because of the
29
+ // `| string`, and the operator gets a refusal their tooling has no word for.
30
+ //
31
+ // That is not a hypothetical about this seam. #218/#219 is the same seam doing
32
+ // exactly this: the SDK targeted `?list=1`, the Core served `/files/list`, each
33
+ // side documented its choice at length, neither knew, and **both suites passed
34
+ // the whole time** — because each proved itself against its own idea of the
35
+ // other. And the review that raised this ticket produced the maintenance cost
36
+ // while it was still running: the commit that added `root-entry-path`, the 22nd
37
+ // member, had to edit both packages to do it, with nothing pointing the author
38
+ // at the second file.
39
+ //
40
+ // ## Why the SDK owns it, and why that arrow is not upside-down
41
+ //
42
+ // The Core is the *server* answering these codes and it now imports their
43
+ // definition from the *client* package. That reads backwards and is the same
44
+ // trade [ADR 0025](../../../docs/adr/0025-the-protocol-ships-with-the-client.md)
45
+ // already made for the core-link frames, for the same reason: **what the Core
46
+ // depends on is the protocol, not the client.** D2 rules that arrow is not a
47
+ // layering violation, and D3 states the rule this file exists to satisfy —
48
+ // "there is exactly one definition of every frame type, and a mirror is never
49
+ // the answer", because a mirror "does not fail; it disagrees, at runtime, on a
50
+ // wire, between two processes that each believe they are correct."
51
+ //
52
+ // ADR 0025 D1 scoped that record to `core-link-frames.ts`, so this union sat
53
+ // just outside its subject. #224 moves the scope rather than re-deriving the
54
+ // argument: D2's rule now names this module too, and the ADR's consequences say
55
+ // so, so the next reviewer meets the widened rule where the rule lives.
56
+ //
57
+ // ## Why a module of its own, and not `core-files-http.ts`
58
+ //
59
+ // `core-files-http.ts` is a transport: it imports `undici` and builds
60
+ // dispatchers. An `import type` erases, so parking the union there would have
61
+ // created no runtime edge for the Core either — but it would have pointed the
62
+ // Core's source at a file full of client machinery to read a vocabulary, and
63
+ // D2's rule is only reviewable if what the Core may import is a module a
64
+ // reviewer can see holds nothing else. This file imports nothing, exactly as
65
+ // `core-link-frames.ts` imports nothing, and that is the property that makes it
66
+ // safe to depend on from the server.
67
+ //
68
+ // **It is public API.** `packages/sdk/package.json` exports `./*`, so this is
69
+ // the published subpath `@actana/sdk/core-files-error-codes` on the next
70
+ // publish. That is a release decision and it is taken here deliberately rather
71
+ // than fallen into: the refusal vocabulary is precisely the thing a third party
72
+ // writing a `catch` wants to type against, so it is better named than hidden.
73
+ // `core-files-http.ts` re-exports the type as well, so the specifier that
74
+ // already worked keeps working and nothing published moves.
75
+ //
76
+ // ## Why the codes are a value and not only a type
77
+ //
78
+ // A union of string literals erases at compile time, which leaves nothing for a
79
+ // test to read. The list below is therefore a `const` tuple and the type is
80
+ // derived from it, so the vocabulary is enumerable at runtime and the *third*
81
+ // copy of it — the operator-facing table in `docs/external-api.md` — can be
82
+ // pinned to this one by a test rather than by somebody remembering.
83
+ // `core-files-error-codes.test.ts` is that test. Nothing shipped imports the
84
+ // array, and nothing has to: `import type` erases in the Core's bundle, so the
85
+ // server still carries no runtime edge into this package.
86
+ /**
87
+ * Every machine-readable refusal code the Core's file routes can answer with.
88
+ *
89
+ * Order is not meaning — nothing reads these by index. Keep it grouped as it is
90
+ * (request, then path, then transfer, then tar entry, then I/O) because that is
91
+ * the order `docs/external-api.md` explains them in, and two lists a reader
92
+ * compares by eye are easier to compare when they run the same way.
93
+ *
94
+ * **Adding one is a one-file change**: append it here, and the Core's `Refusal`
95
+ * type and the SDK's error classes both accept it with no further edit. The
96
+ * docs table is the one copy left, and it is not a copy you can forget —
97
+ * `core-files-error-codes.test.ts` goes red until it names the new code too.
98
+ */
99
+ export const CORE_FILES_ERROR_CODES = [
100
+ "unauthorized",
101
+ "not-found",
102
+ "project-not-found",
103
+ "method-not-allowed",
104
+ "bad-request",
105
+ "absolute-path",
106
+ "dot-dot-segment",
107
+ "outside-project-root",
108
+ "malformed-path",
109
+ "transfer-in-progress",
110
+ "insufficient-storage",
111
+ "corrupt-archive",
112
+ "absolute-entry-path",
113
+ "dot-dot-entry-path",
114
+ "root-entry-path",
115
+ "entry-outside-root",
116
+ "unsupported-entry-type",
117
+ "hardlink-outside-root",
118
+ "symlink-outside-root",
119
+ "directory-in-the-way",
120
+ "write-failed",
121
+ "read-failed",
122
+ ];
123
+ //# sourceMappingURL=core-files-error-codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-files-error-codes.js","sourceRoot":"","sources":["../src/core-files-error-codes.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,2BAA2B;AAC3B,EAAE;AACF,8DAA8D;AAC9D,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,gFAAgF;AAChF,aAAa;AACb,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,uEAAuE;AACvE,+EAA+E;AAC/E,yDAAyD;AACzD,+EAA+E;AAC/E,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,8EAA8E;AAC9E,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,iFAAiF;AACjF,6EAA6E;AAC7E,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,+EAA+E;AAC/E,4EAA4E;AAC5E,8EAA8E;AAC9E,gFAAgF;AAChF,+EAA+E;AAC/E,sBAAsB;AACtB,EAAE;AACF,gEAAgE;AAChE,EAAE;AACF,0EAA0E;AAC1E,6EAA6E;AAC7E,iFAAiF;AACjF,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,+EAA+E;AAC/E,mEAAmE;AACnE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,gFAAgF;AAChF,wEAAwE;AACxE,EAAE;AACF,2DAA2D;AAC3D,EAAE;AACF,sEAAsE;AACtE,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,yEAAyE;AACzE,6EAA6E;AAC7E,gFAAgF;AAChF,qCAAqC;AACrC,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,+EAA+E;AAC/E,gFAAgF;AAChF,8EAA8E;AAC9E,0EAA0E;AAC1E,4DAA4D;AAC5D,EAAE;AACF,mDAAmD;AACnD,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,8EAA8E;AAC9E,4EAA4E;AAC5E,oEAAoE;AACpE,6EAA6E;AAC7E,+EAA+E;AAC/E,0DAA0D;AAE1D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,cAAc;IACd,WAAW;IACX,mBAAmB;IACnB,oBAAoB;IACpB,aAAa;IACb,eAAe;IACf,iBAAiB;IACjB,sBAAsB;IACtB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;IACtB,iBAAiB;IACjB,qBAAqB;IACrB,oBAAoB;IACpB,iBAAiB;IACjB,oBAAoB;IACpB,wBAAwB;IACxB,uBAAuB;IACvB,sBAAsB;IACtB,sBAAsB;IACtB,cAAc;IACd,aAAa;CACL,CAAC"}
@@ -0,0 +1,119 @@
1
+ import type { CoreFilesErrorCode } from "./core-files-error-codes.ts";
2
+ import type { CoreLinkTlsMaterial } from "./core-link-socket.ts";
3
+ export type { CoreFilesErrorCode } from "./core-files-error-codes.ts";
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
+ /**
21
+ * A refusal with a status line: the Core answered, and said no.
22
+ *
23
+ * ## `CoreFilesErrorCode | string` is the type, and it stays that way
24
+ *
25
+ * Stated here rather than left to be inferred, because it looks like a loose
26
+ * type somebody forgot to tighten and it is the opposite (#224). A client is
27
+ * routinely older than the Core it dials: this package is published, an
28
+ * operator upgrades a Core, and the next release of that Core may answer a
29
+ * refusal code this build of the SDK has never heard of. Typing `code` as the
30
+ * closed union would make the honest handling of that — reading the code the
31
+ * Core actually sent — a cast at every call site, and would invite a client to
32
+ * `switch` exhaustively over a vocabulary it does not own the far end of.
33
+ *
34
+ * So the union is autocomplete and documentation, and `| string` is the truth
35
+ * about the wire. Narrowing this later is a **decision** — it means declaring
36
+ * that a client may refuse to name what its Core told it — and not a cleanup
37
+ * for a passing reader to do.
38
+ *
39
+ * Note what this therefore does *not* buy, so the guarantee is not overstated:
40
+ * one definition (see `core-files-error-codes.ts`) removes the drift where the
41
+ * SDK lists fewer codes than the Core sends. It cannot remove the widening's
42
+ * own blind spot, because that is not drift — it is the wire being older or
43
+ * newer than the reader, which is a fact about deployments rather than about
44
+ * this repository.
45
+ */
46
+ export declare class CoreFilesRequestError extends CoreFilesError {
47
+ readonly status: number;
48
+ readonly code: CoreFilesErrorCode | string;
49
+ constructor(status: number, code: CoreFilesErrorCode | string, message: string);
50
+ }
51
+ /**
52
+ * The one-write-per-Project rule (F8), as an error and **never as a retry**.
53
+ *
54
+ * `409 transfer-in-progress` is a conflict a human has to resolve — some other
55
+ * transfer is running on this Project right now, possibly a multi-gigabyte one
56
+ * with twenty minutes left. A client that retried under the hood would convert
57
+ * the Core's clear, immediate refusal into a hang with no output, which is
58
+ * strictly worse than the error: the operator loses both the reason and the
59
+ * chance to decide. So nothing in this package sleeps, backs off or re-sends —
60
+ * this class *is* the handling, and the decision is the caller's.
61
+ *
62
+ * `directory-in-the-way` shares the status and the same rule: it is a refusal
63
+ * to delete a tree, and it will still be a directory on the next attempt.
64
+ */
65
+ export declare class CoreFilesConflictError extends CoreFilesRequestError {
66
+ constructor(status: number, code: CoreFilesErrorCode | string, message: string);
67
+ }
68
+ /**
69
+ * A failure that arrived as the **last line of a progress stream** rather than
70
+ * as a status code, because the `200` was spent on the first entry.
71
+ *
72
+ * This is why the progress stream is NDJSON and not a JSON document: a document
73
+ * would have to be well-formed to be read at all, and this one stops in the
74
+ * middle by design.
75
+ */
76
+ export declare class CoreFilesStreamError extends CoreFilesError {
77
+ readonly code: CoreFilesErrorCode | string;
78
+ constructor(code: CoreFilesErrorCode | string, message: string);
79
+ }
80
+ /** One request, in the terms this surface actually uses. */
81
+ export type CoreFilesRequest = {
82
+ method: "GET" | "HEAD" | "PUT";
83
+ url: string;
84
+ headers: Record<string, string>;
85
+ /** A stream, never a buffer — see {@link CoreFiles.upload}. */
86
+ body?: ReadableStream<Uint8Array> | null;
87
+ signal?: AbortSignal;
88
+ };
89
+ /**
90
+ * How a file request is actually sent.
91
+ *
92
+ * Injected rather than reached for, on the same terms as
93
+ * {@link CoreLinkSocketFactory}: it is what lets a suite drive this surface
94
+ * over a plain loopback server, and what lets a runtime with its own
95
+ * dispatcher — a proxy, a recorder, a browser — supply one. The default is
96
+ * {@link createCoreFilesFetch}.
97
+ */
98
+ export type CoreFilesFetch = (req: CoreFilesRequest) => Promise<Response>;
99
+ /**
100
+ * The default sender: `fetch`, with the client certificate carried in on a
101
+ * dispatcher.
102
+ *
103
+ * The `Agent` is built **once, lazily, and only when there is TLS material** —
104
+ * a `http://` loopback Core needs no dispatcher, and building one per request
105
+ * would open a fresh connection pool for every upload.
106
+ */
107
+ export declare function createCoreFilesFetch(tls: CoreLinkTlsMaterial | null): CoreFilesFetch;
108
+ /**
109
+ * Turn a non-2xx answer into the right error class, reading the `code` beside
110
+ * the prose rather than the prose.
111
+ *
112
+ * A client branches on `code` because a message is a sentence somebody will
113
+ * reword — and `transfer-in-progress` in particular *has* to stay
114
+ * distinguishable (F8). The body is read to completion here, and only here: a
115
+ * refusal body is a short JSON object, which is why buffering it does not
116
+ * violate the streaming rule the success paths keep.
117
+ */
118
+ export declare function refusalFrom(res: Response, what: string): Promise<CoreFilesRequestError>;
119
+ //# 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,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AASjE,YAAY,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,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;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,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,223 @@
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
+ /**
83
+ * A refusal with a status line: the Core answered, and said no.
84
+ *
85
+ * ## `CoreFilesErrorCode | string` is the type, and it stays that way
86
+ *
87
+ * Stated here rather than left to be inferred, because it looks like a loose
88
+ * type somebody forgot to tighten and it is the opposite (#224). A client is
89
+ * routinely older than the Core it dials: this package is published, an
90
+ * operator upgrades a Core, and the next release of that Core may answer a
91
+ * refusal code this build of the SDK has never heard of. Typing `code` as the
92
+ * closed union would make the honest handling of that — reading the code the
93
+ * Core actually sent — a cast at every call site, and would invite a client to
94
+ * `switch` exhaustively over a vocabulary it does not own the far end of.
95
+ *
96
+ * So the union is autocomplete and documentation, and `| string` is the truth
97
+ * about the wire. Narrowing this later is a **decision** — it means declaring
98
+ * that a client may refuse to name what its Core told it — and not a cleanup
99
+ * for a passing reader to do.
100
+ *
101
+ * Note what this therefore does *not* buy, so the guarantee is not overstated:
102
+ * one definition (see `core-files-error-codes.ts`) removes the drift where the
103
+ * SDK lists fewer codes than the Core sends. It cannot remove the widening's
104
+ * own blind spot, because that is not drift — it is the wire being older or
105
+ * newer than the reader, which is a fact about deployments rather than about
106
+ * this repository.
107
+ */
108
+ export class CoreFilesRequestError extends CoreFilesError {
109
+ status;
110
+ code;
111
+ constructor(status, code, message) {
112
+ super(message);
113
+ this.name = "CoreFilesRequestError";
114
+ this.status = status;
115
+ this.code = code;
116
+ }
117
+ }
118
+ /**
119
+ * The one-write-per-Project rule (F8), as an error and **never as a retry**.
120
+ *
121
+ * `409 transfer-in-progress` is a conflict a human has to resolve — some other
122
+ * transfer is running on this Project right now, possibly a multi-gigabyte one
123
+ * with twenty minutes left. A client that retried under the hood would convert
124
+ * the Core's clear, immediate refusal into a hang with no output, which is
125
+ * strictly worse than the error: the operator loses both the reason and the
126
+ * chance to decide. So nothing in this package sleeps, backs off or re-sends —
127
+ * this class *is* the handling, and the decision is the caller's.
128
+ *
129
+ * `directory-in-the-way` shares the status and the same rule: it is a refusal
130
+ * to delete a tree, and it will still be a directory on the next attempt.
131
+ */
132
+ export class CoreFilesConflictError extends CoreFilesRequestError {
133
+ constructor(status, code, message) {
134
+ super(status, code, message);
135
+ this.name = "CoreFilesConflictError";
136
+ }
137
+ }
138
+ /**
139
+ * A failure that arrived as the **last line of a progress stream** rather than
140
+ * as a status code, because the `200` was spent on the first entry.
141
+ *
142
+ * This is why the progress stream is NDJSON and not a JSON document: a document
143
+ * would have to be well-formed to be read at all, and this one stops in the
144
+ * middle by design.
145
+ */
146
+ export class CoreFilesStreamError extends CoreFilesError {
147
+ code;
148
+ constructor(code, message) {
149
+ super(message);
150
+ this.name = "CoreFilesStreamError";
151
+ this.code = code;
152
+ }
153
+ }
154
+ /**
155
+ * The default sender: `fetch`, with the client certificate carried in on a
156
+ * dispatcher.
157
+ *
158
+ * The `Agent` is built **once, lazily, and only when there is TLS material** —
159
+ * a `http://` loopback Core needs no dispatcher, and building one per request
160
+ * would open a fresh connection pool for every upload.
161
+ */
162
+ export function createCoreFilesFetch(tls) {
163
+ let dispatcher = null;
164
+ return async (req) => {
165
+ if (tls && !dispatcher) {
166
+ dispatcher = new Agent({ connect: { ca: tls.ca, cert: tls.cert, key: tls.key } });
167
+ }
168
+ // `duplex: "half"` is mandatory for a streamed request body and is not a
169
+ // detail that can be left out: without it `fetch` rejects the body outright,
170
+ // which would take this surface straight back to buffering an upload in
171
+ // memory — the one thing #167 exists to not do.
172
+ //
173
+ // The casts are the type-level shadow of the same two-copies problem the
174
+ // header describes: `@types/node` declares the global `fetch` with its own
175
+ // bundled `undici-types`, and this module deliberately uses the real
176
+ // package's `fetch` instead. The two `Response` and `RequestInit`
177
+ // declarations are structurally near-identical and neither is wrong — they
178
+ // are simply not the same declaration. Everything this package reads off a
179
+ // response (`ok`, `status`, `headers.get`, `json`, `body`) is common to
180
+ // both, and the global `Response` is what callers and tests hold, so that
181
+ // is the type this surface promises.
182
+ const init = {
183
+ method: req.method,
184
+ headers: req.headers,
185
+ ...(req.body ? { body: req.body, duplex: "half" } : {}),
186
+ ...(req.signal ? { signal: req.signal } : {}),
187
+ ...(dispatcher ? { dispatcher } : {}),
188
+ };
189
+ const res = await undiciFetch(req.url, init);
190
+ return res;
191
+ };
192
+ }
193
+ /**
194
+ * Turn a non-2xx answer into the right error class, reading the `code` beside
195
+ * the prose rather than the prose.
196
+ *
197
+ * A client branches on `code` because a message is a sentence somebody will
198
+ * reword — and `transfer-in-progress` in particular *has* to stay
199
+ * distinguishable (F8). The body is read to completion here, and only here: a
200
+ * refusal body is a short JSON object, which is why buffering it does not
201
+ * violate the streaming rule the success paths keep.
202
+ */
203
+ export async function refusalFrom(res, what) {
204
+ let code = "";
205
+ let message = "";
206
+ try {
207
+ const body = (await res.json());
208
+ if (typeof body.code === "string")
209
+ code = body.code;
210
+ if (typeof body.error === "string")
211
+ message = body.error;
212
+ }
213
+ catch {
214
+ // A refusal that is not the Core's JSON shape — a proxy's HTML error page,
215
+ // a truncated body. Say what is actually known rather than inventing a code.
216
+ }
217
+ const described = message || `${what} was refused with HTTP ${res.status}`;
218
+ const withCode = code ? `${described} (${code})` : described;
219
+ if (res.status === 409)
220
+ return new CoreFilesConflictError(res.status, code || "conflict", withCode);
221
+ return new CoreFilesRequestError(res.status, code || `http-${res.status}`, withCode);
222
+ }
223
+ //# 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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"}