@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.
- package/README.md +54 -0
- package/dist/core-client.d.ts +99 -1
- package/dist/core-client.d.ts.map +1 -1
- package/dist/core-client.js +138 -2
- package/dist/core-client.js.map +1 -1
- package/dist/core-files-error-codes.d.ts +23 -0
- package/dist/core-files-error-codes.d.ts.map +1 -0
- package/dist/core-files-error-codes.js +123 -0
- package/dist/core-files-error-codes.js.map +1 -0
- package/dist/core-files-http.d.ts +119 -0
- package/dist/core-files-http.d.ts.map +1 -0
- package/dist/core-files-http.js +223 -0
- package/dist/core-files-http.js.map +1 -0
- package/dist/core-files.d.ts +227 -0
- package/dist/core-files.d.ts.map +1 -0
- package/dist/core-files.js +410 -0
- package/dist/core-files.js.map +1 -0
- package/dist/core-link-frames.d.ts +59 -0
- package/dist/core-link-frames.d.ts.map +1 -1
- package/dist/core-link-frames.js +35 -0
- package/dist/core-link-frames.js.map +1 -1
- package/dist/core-link-transport.d.ts.map +1 -1
- package/dist/core-link-transport.js +6 -0
- package/dist/core-link-transport.js.map +1 -1
- package/dist/core-project.d.ts +19 -0
- package/dist/core-project.d.ts.map +1 -0
- package/dist/core-project.js +38 -0
- package/dist/core-project.js.map +1 -0
- package/dist/core-registration-blob.d.ts +11 -0
- package/dist/core-registration-blob.d.ts.map +1 -1
- package/dist/core-registration-blob.js +5 -1
- package/dist/core-registration-blob.js.map +1 -1
- package/package.json +2 -1
|
@@ -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":"AAAA,2EAA2E;AAC3E,2CAA2C;AAC3C,EAAE;AACF,wEAAwE;AACxE,8EAA8E;AAC9E,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,wEAAwE;AACxE,EAAE;AACF,kDAAkD;AAClD,EAAE;AACF,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,+DAA+D;AAC/D,+CAA+C;AAC/C,EAAE;AACF,gFAAgF;AAChF,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,2EAA2E;AAC3E,6EAA6E;AAC7E,+EAA+E;AAC/E,4EAA4E;AAC5E,EAAE;AACF,gEAAgE;AAChE,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,+EAA+E;AAC/E,gFAAgF;AAChF,0EAA0E;AAC1E,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,YAAY;AACZ,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,iCAAiC;AACjC,EAAE;AACF,sDAAsD;AACtD,EAAE;AACF,yEAAyE;AACzE,8EAA8E;AAC9E,+EAA+E;AAC/E,kFAAkF;AAClF,gFAAgF;AAChF,iFAAiF;AACjF,kFAAkF;AAClF,wDAAwD;AACxD,OAAO,EAAE,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AAcrD,sFAAsF;AACtF,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,yBAA0B,SAAQ,cAAc;IAClD,MAAM,CAAS;IAExB,YAAY,MAAc;QACxB,KAAK,CAAC,MAAM,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,OAAO,qBAAsB,SAAQ,cAAc;IAC9C,MAAM,CAAS;IACf,IAAI,CAA8B;IAE3C,YAAY,MAAc,EAAE,IAAiC,EAAE,OAAe;QAC5E,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,sBAAuB,SAAQ,qBAAqB;IAC/D,YAAY,MAAc,EAAE,IAAiC,EAAE,OAAe;QAC5E,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,oBAAqB,SAAQ,cAAc;IAC7C,IAAI,CAA8B;IAE3C,YAAY,IAAiC,EAAE,OAAe;QAC5D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAuBD;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAA+B;IAClE,IAAI,UAAU,GAAiB,IAAI,CAAC;IACpC,OAAO,KAAK,EAAE,GAAqB,EAAqB,EAAE;QACxD,IAAI,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACvB,UAAU,GAAG,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACpF,CAAC;QACD,yEAAyE;QACzE,6EAA6E;QAC7E,wEAAwE;QACxE,gDAAgD;QAChD,EAAE;QACF,yEAAyE;QACzE,2EAA2E;QAC3E,qEAAqE;QACrE,kEAAkE;QAClE,2EAA2E;QAC3E,2EAA2E;QAC3E,wEAAwE;QACxE,0EAA0E;QAC1E,qCAAqC;QACrC,MAAM,IAAI,GAA4B;YACpC,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvD,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7C,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtC,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,IAAyC,CAAC,CAAC;QAClF,OAAO,GAA0B,CAAC;IACpC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAa,EAAE,IAAY;IAC3D,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwC,CAAC;QACvE,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACpD,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,6EAA6E;IAC/E,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,IAAI,GAAG,IAAI,0BAA0B,GAAG,CAAC,MAAM,EAAE,CAAC;IAC3E,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7D,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,sBAAsB,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,UAAU,EAAE,QAAQ,CAAC,CAAC;IACpG,OAAO,IAAI,qBAAqB,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,QAAQ,GAAG,CAAC,MAAM,EAAE,EAAE,QAAQ,CAAC,CAAC;AACvF,CAAC"}
|