@pipelex/sdk 0.48.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/artifacts.d.ts +4 -3
- package/dist/artifacts.js +20 -14
- package/dist/artifacts.js.map +1 -1
- package/dist/cli/script.d.ts +1 -1
- package/dist/client.d.ts +95 -9
- package/dist/client.js +143 -12
- package/dist/client.js.map +1 -1
- package/dist/codegen-check.d.ts +2 -3
- package/dist/codegen-check.js +2 -3
- package/dist/codegen-check.js.map +1 -1
- package/dist/errors.d.ts +91 -6
- package/dist/errors.js +48 -6
- package/dist/errors.js.map +1 -1
- package/dist/hooks/bundle-gather.d.ts +29 -10
- package/dist/hooks/bundle-gather.js +91 -14
- package/dist/hooks/bundle-gather.js.map +1 -1
- package/dist/hooks/check-core.d.ts +70 -6
- package/dist/hooks/check-core.js +188 -29
- package/dist/hooks/check-core.js.map +1 -1
- package/dist/hooks/claude-mthds-check.d.ts +12 -0
- package/dist/hooks/claude-mthds-check.js +12 -0
- package/dist/hooks/claude-mthds-check.js.map +1 -1
- package/dist/hooks/secret-guard.d.ts +38 -0
- package/dist/hooks/secret-guard.js +134 -0
- package/dist/hooks/secret-guard.js.map +1 -0
- package/dist/hooks/validate-stage.d.ts +5 -3
- package/dist/hooks/validate-stage.js +16 -7
- package/dist/hooks/validate-stage.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/method-trees.d.ts +19 -8
- package/dist/method-trees.js +37 -11
- package/dist/method-trees.js.map +1 -1
- package/dist/node-host/node.d.ts +33 -0
- package/dist/node-host/node.js +27 -0
- package/dist/node-host/node.js.map +1 -0
- package/dist/node-host/unavailable.d.ts +8 -0
- package/dist/node-host/unavailable.js +8 -0
- package/dist/node-host/unavailable.js.map +1 -0
- package/dist/node-host.d.ts +53 -0
- package/dist/node-host.js +72 -0
- package/dist/node-host.js.map +1 -0
- package/dist/prepare-inputs.d.ts +12 -1
- package/dist/prepare-inputs.js +26 -3
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/product-models.d.ts +219 -2
- package/dist/product-models.js +2 -2
- package/dist/upload.d.ts +43 -20
- package/dist/upload.js +128 -24
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +32 -6
- package/docs/crate-routes.md +1 -1
- package/docs/errors.md +6 -1
- package/docs/files.md +88 -0
- package/docs/hook-bundle.md +27 -1
- package/docs/input-preparation.md +16 -6
- package/docs/method-trees.md +2 -2
- package/docs/organization-branding.md +102 -0
- package/package.json +7 -1
package/docs/files.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# An organization's files
|
|
2
|
+
|
|
3
|
+
A **file** is the record the hosted platform keeps of a stored object: a member's upload, a test file made for a method, or a file a run made that reached its result. The bytes stay where they were written, and a run, a form or an example still points at them by their `pipelex-storage://` address; the record adds what the bytes lack, the name a person sees, a kind, a type, a size and where the file came from. A file belongs to the organization: every role of it reads every file of it, as every role reads every run. The client reaches the list on the hosted API's `GET /v1/files`, and the types are in `src/product-models.ts`. The platform's own contract is `platform/docs/files.md` in `pipelex-server`.
|
|
4
|
+
|
|
5
|
+
The point of the list for a program is the `uri` on every row: a run's inputs take it as they take any address, so an agent or an app can find "the invoice uploaded this morning" and run a method on it without uploading it again.
|
|
6
|
+
|
|
7
|
+
## A row — `FileSummary`
|
|
8
|
+
|
|
9
|
+
| Field | Meaning |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `file_id` | The record's id, `fl_…`. |
|
|
12
|
+
| `uri` | The `pipelex-storage://` address of the bytes. |
|
|
13
|
+
| `name` | The name shown and used for downloads. It starts as the name the file arrived with and changes with a rename. |
|
|
14
|
+
| `media_type` | The MIME type the object is stored under. |
|
|
15
|
+
| `format` | The format key the list's `format` filter matches: `pdf`, `docx`, `image`, `html` and the rest of the runtime's table. |
|
|
16
|
+
| `size` | The byte size. |
|
|
17
|
+
| `kind` | A `FileKind`: `uploaded`, a person's upload or a run's inline input; `generated`, a file a run made whose result reaches it; `test`, a test input made for one method. |
|
|
18
|
+
| `origin` | A `FileOrigin`, who or what made the file, each member `null` where the kind fills no value: `surface`, the surface an upload came through as its `User-Agent` declared it (`app`, `mcp`, `cli`, `n8n`, `hook`, `sdk`, `raw_http`), which is provenance to show and never authority; `run_id`, the run that made it; `method_id`, the method whose run made it or the method a test file was made for; `method_version`, the version number or `"draft"` that ran. |
|
|
19
|
+
| `created_at` | The ISO-8601 instant the record was made. |
|
|
20
|
+
| `created_by` | The member the file is attributed to: the uploader, or the member who started the run that made it. |
|
|
21
|
+
|
|
22
|
+
**A list never holds a file a run made along the way** (an intermediate file, which the run's result does not reach), **nor an upload whose bytes have not been seen stored**, nor anything of another organization.
|
|
23
|
+
|
|
24
|
+
## The methods
|
|
25
|
+
|
|
26
|
+
| Method | Route | Answer |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `listFiles(query?)` | `GET /v1/files` | One `FilePage`, `{ items, nextCursor }`, newest first. |
|
|
29
|
+
| `iterateFiles(query?)` | the same, page after page | Every file the query selects, as an async iterator. |
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const page = await client.listFiles({ q: "invoice", kind: "uploaded", format: ["pdf"] });
|
|
33
|
+
for (const file of page.items) console.log(file.name, file.uri);
|
|
34
|
+
|
|
35
|
+
// A method's files: what its runs and examples took or made.
|
|
36
|
+
for await (const file of client.iterateFiles({ methodId: "mt_abc123" })) {
|
|
37
|
+
if (file.kind === "test") console.log("test input", file.name);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The query — `ListFilesQuery`
|
|
42
|
+
|
|
43
|
+
Every member is optional, and each is sent only when given, so an empty `q`, cursor or date is sent and refused by the API rather than dropped into an unfiltered list. The members follow the SDK's camelCase, as `ListRunsQuery`'s do, and each is sent under the API's own name.
|
|
44
|
+
|
|
45
|
+
| Member | Sent as | Meaning |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `methodId` | `method_id` | The method's list, by its bare catalog id: the files its runs and examples took or made. |
|
|
48
|
+
| `runId` | `run_id` | The run's own list: its final outputs and its inline inputs, **in the order of their addresses** rather than newest first. Beside `methodId`, the run's list when the method is the run's own, and an empty page otherwise. The files a run took from the organization as inputs are not in it. |
|
|
49
|
+
| `createdBy` | `created_by` | One member's files by user id, or `"me"` for the caller's own: their uploads and their runs' outputs. |
|
|
50
|
+
| `kind` | `kind` | `uploaded`, `generated` or `test`. |
|
|
51
|
+
| `format` | `format`, repeated | Only the files of one of these format keys, one parameter per value (`format=pdf&format=image`), at most 100. |
|
|
52
|
+
| `mediaType` | `media_type`, repeated | Only the files of one of these MIME types, one parameter per value, at most 100. |
|
|
53
|
+
| `createdFrom`, `createdTo` | `created_from`, `created_to` | ISO-8601 **instants** with an offset, both inclusive. A bare date or a naive timestamp is a `400`, since only the caller knows which time zone's day it means. |
|
|
54
|
+
| `q` | `q` | A case-insensitive search in the name and the name the file arrived with, at most 200 characters. |
|
|
55
|
+
| `limit` | `limit` | Page size, 1 to 200, 50 by default. |
|
|
56
|
+
| `cursor` | `cursor` | The previous page's `nextCursor`. `iterateFiles` takes no cursor: it follows its own. |
|
|
57
|
+
|
|
58
|
+
`methodId`, `runId` and `createdBy` choose which list serves the page, and with none of them it is the organization's. The other members filter that list.
|
|
59
|
+
|
|
60
|
+
**An empty `format` or `mediaType` is refused before any request** with a `RequestArgumentError`, since a list with no value would be sent as no parameter at all and list every format; a value that is not a list of strings is a `TypeError`, a bug in the calling code. Everything else the API checks itself: a `limit` out of range, an unknown `kind`, more than 100 values of `format` or `mediaType`, or an id that cannot be one is a `422`; a date bound that is not an instant with an offset, a range whose start is after its end, or a cursor of another list or other filters is a `400`; and a run that is not the organization's is a `404` `run_not_found`. Each is an `ApiResponseError` ([errors.md](./errors.md)).
|
|
61
|
+
|
|
62
|
+
## Paging
|
|
63
|
+
|
|
64
|
+
**Only a `null` `nextCursor` ends a list.** The platform applies the filters to the rows a page reads and never refills the page, so a filtered page can hold fewer rows than `limit`, or none, and still carry a cursor; a client keeps asking until the cursor is `null`. **A cursor is bound to the list and the filters that issued it**, so a client passes it back with the same query, and passing it to another is a `400`.
|
|
65
|
+
|
|
66
|
+
`iterateFiles` does both: it sends the same query with every page, keeps going through an empty page that carries a cursor, stops on a `null` cursor or on a cursor the server failed to advance, and throws `PagingNotTerminatingError` past a page ceiling no real list reaches, through the loop `iterateMethods`, `iterateRuns` and `iterateMethodExamples` share. A page whose `items` is not an array, or whose `next_cursor` is neither a string nor `null`, is an `ApiResponseError` of an answer the SDK cannot read (`runtime`, not retryable), never a page the iterator walks.
|
|
67
|
+
|
|
68
|
+
## Uploading a test file
|
|
69
|
+
|
|
70
|
+
What an upload is recorded as travels with it to `POST /v1/upload`: its `kind`, `uploaded` when omitted, and for a `test` file the bare catalog id of the method it is for, `method_id`. A test file is a statement about a method, so the platform holds it to what changing that method takes. The pair is spelled the same on every surface that sends it: `UploadInput` (the raw `upload`), `UploadFileOptions` (`uploadFile`) and `prepareInputs`' nested `upload` member, which applies to every file the preparation uploads. `UploadKind` names the two kinds, and `UploadKindOptions` the pair.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// One file the build assistant made to try a method:
|
|
74
|
+
await client.uploadFile("cases/invoice.pdf", { kind: "test", method_id: "mt_abc123" });
|
|
75
|
+
|
|
76
|
+
// Every file a preparation uploads, recorded as test files of the method it prepares:
|
|
77
|
+
const prepared = await client.prepareInputs({
|
|
78
|
+
method_id: "mt_abc123",
|
|
79
|
+
inputs: { invoice: "cases/invoice.pdf" },
|
|
80
|
+
upload: { kind: "test", method_id: "mt_abc123" },
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`upload` is nested so that its `method_id`, the method a test file is for, never reads as the request's own `method_id` selector, which names the method whose inputs are prepared and excludes `method_ref` and the method's tree.
|
|
85
|
+
|
|
86
|
+
**The pair is checked before any file is read**, as the upload route checks it, so a request the route would refuse with a `422` is an `InputPreparationError` instead: a `kind` that is neither `uploaded` nor `test`, a `method_id` that is not a catalog id or carries a version suffix, `kind: "test"` without a `method_id`, and a `method_id` without `kind: "test"`. `null` reads as absent. `prepareInputs` names the members it refuses `upload.kind` and `upload.method_id`.
|
|
87
|
+
|
|
88
|
+
**A test file the route refuses for what it claims is a `TestFileRefusedError`**, whose `code` says why: `role_not_permitted`, the `403` of a caller whose role may not change methods, such as a runner, which an admin of the organization must change (`config`), or which the caller avoids by uploading the file as `uploaded`; `method_not_found`, the `404` of a method that is not one of the organization's (`input`); and `method_being_deleted`, the `409` of a method being erased (`input`). It carries the `status`, the `filename` it was sending and the `methodId` it named, and wraps the `ApiResponseError` as `cause`, whose verdict it takes. These refusals are read off the platform's code, never the status alone: a `403` without `role_not_permitted` is still a refused credential (`UploadAuthenticationError`), and a `404` is still a deployment without upload (`UnsupportedUploadCapabilityError`) unless the upload named a method and the platform answered its domain miss, `not_found` with the `input` domain, rather than the `config` `404` of a route it does not serve ([input-preparation.md](./input-preparation.md#error-and-capability-behavior)).
|
package/docs/hook-bundle.md
CHANGED
|
@@ -6,7 +6,33 @@
|
|
|
6
6
|
|
|
7
7
|
Each file goes through three stages: a local lint that blocks on any diagnostic, a local format that rewrites the file in place when its layout changes, and a validation by the hosted API (`POST /v1/validate`) that blocks on an invalid verdict and adds a non-blocking note about pending pipe signatures. The hook fails open: when the engine cannot load, when `PIPELEX_API_KEY` is unset or the API cannot be reached, the stage that could not run passes silently and the others stand. When several files are checked, one block among them wins and the notes are joined, and each file is read when its turn comes, so an edit made to it while an earlier file was being validated is checked rather than overwritten. The hook always exits 0, since its verdict is on stdout.
|
|
8
8
|
|
|
9
|
-
The validation sends the method's tree around the file, the tree a run of the method would send: the edited file's directory listed by the `mthds` package's lister under the MTHDS standard's membership rule, so it is rooted at the nearest `METHODS.toml` at or above that directory, else at the directory itself, and holds every file the rule keeps — every bundle of the method, and every file a step reads, such as the template a `PipeDocGen` step names in `template_file`, under its tree path — while hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` keeps out and a run's inputs file never travel ([`method-trees.md`](./method-trees.md#the-four-forms-of-a-tree)). A method that fills a template thus validates as it would load from disk, its template checked by the dry run, and a step whose template file the tree does not hold makes the verdict say so, which blocks until the file is written. The client sends the tree inline when small and by hash otherwise, uploading the files the API lacks. The whole validation runs under one ten-second deadline, the version handshake, the blob route, the uploads and the validation request together, and the deadline stops whichever of them is under way when it runs out. Validation passes silently, as when the API cannot be reached, when that deadline runs out, when the tree holds a symbolic link or a directory the lister cannot read, when the edited file is not in its own tree because the rule keeps it out, when a file of the tree fails to upload, and past the hook's own caps: 100 files, 5 MiB for one of them or 12 MiB in all, each checked while the tree is listed, before any file is read.
|
|
9
|
+
The validation sends the method's tree around the file, the tree a run of the method would send: the edited file's directory listed by the `mthds` package's lister under the MTHDS standard's membership rule, so it is rooted at the nearest `METHODS.toml` at or above that directory, else at the directory itself, and holds every file the rule keeps — every bundle of the method, and every file a step reads, such as the template a `PipeDocGen` step names in `template_file`, under its tree path — while hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` inside the tree keeps out and a run's inputs file never travel ([`method-trees.md`](./method-trees.md#the-four-forms-of-a-tree)), and neither does a file that looks like a secret ([below](#secrets-stay-home)). A method that fills a template thus validates as it would load from disk, its template checked by the dry run, and a step whose template file the tree does not hold makes the verdict say so, which blocks until the file is written. The client sends the tree inline when small and by hash otherwise, uploading the files the API lacks. The whole validation runs under one ten-second deadline, the version handshake, the blob route, the uploads and the validation request together, and the deadline stops whichever of them is under way when it runs out. Validation passes silently, as when the API cannot be reached, when that deadline runs out, when the tree holds a symbolic link or a directory the lister cannot read, when the edited file is not in its own tree because the rule keeps it out, when a file of the tree changes between its listing and its reading, when a file of the tree fails to upload, and past the hook's own caps: 100 files, 5 MiB for one of them or 12 MiB in all, each checked while the tree is listed, before any file is read.
|
|
10
|
+
|
|
11
|
+
## Secrets stay home
|
|
12
|
+
|
|
13
|
+
When a `PIPELEX_API_KEY` is set, and before the validation sends anything, the hook reads every file of the tree once, after the format write-back, and screens it with the workshop's secret guard, the rule pipelex-mcp applies to every tree it sends, in a copy (`src/hooks/secret-guard.ts`) that is ahead of pipelex-mcp's `src/capabilities/secret-guard.ts` on the SSH key names and the PEM private key block below, until the workshop takes them up. The bytes screened are the bytes sent, never read again. The screen is needed because the listing reads only the `.gitignore` files inside the tree, so a repository's own `.gitignore` above the method's directory protects nothing in it, and the hook sends every file of the tree, not only its bundles. A `.env` never travels in any case, since the membership rule keeps out every name starting with a dot. With no key, the hook neither reads nor sends any file of the tree.
|
|
14
|
+
|
|
15
|
+
A file looks like a secret when its name, matched on its last segment ignoring case, is a known secret file's, whatever it holds: a `.key` file that is no zip archive (a Keynote deck is one), a key or certificate store (`.p12`, `.pfx`, `.jks`, `.keystore`), a PuTTY `.ppk`, a KeePass `.kdbx`, an SSH private key under exactly its default name (`id_rsa`, `id_dsa`, `id_ecdsa` or `id_ed25519`, with the `_sk` of a hardware-backed key, while its public `.pub` half and a file such as `id_rsa_utils.py` go), a `credentials` or `credentials.json` file, or an OAuth `client_secret…json` export. It also looks like one when its bytes hold a well-known key format, in any encoding, UTF-16 included: a PEM private key block, its header followed by a key's body where a key writes it, at the start of a line soon after the header, as a key file writes it, an encrypted legacy key after its `Proc-Type` and `DEK-Info` lines and a key escaped into JSON after a `\n`, or directly on the header's line, as a key flattened onto one line writes it, so a prompt that names the header in prose goes, even with a long path or link after it in the sentence; an AWS access key id, a GitHub, GitLab, Slack or npm token, a Stripe live key, a Google, Anthropic, OpenAI or Hugging Face API key, or a Pipelex API key. Each format is the prefix its provider puts on its keys so that scanners find them, with enough of the key after it that prose naming the prefix does not match. A `.pem` file is judged by what it holds, since one is as often a public certificate, and a service-account export is found by the private key block inside it. The rule is a tripwire for the well-known shapes, never a promise that a tree holds no secret: a password written in prose goes through it.
|
|
16
|
+
|
|
17
|
+
What the hook does with such a file depends on whether every validation reads it:
|
|
18
|
+
|
|
19
|
+
| The file | What the hook does |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| Neither a bundle nor the manifest, such as a key file beside the method, a data file, a template or a Python module | It stays home, and the method is validated without it. When the validation returns a verdict, the verdict decides as usual, and a note after the verdict's text names each file kept home: non-blocking beside a valid verdict, and inside the block of an invalid one. When it returns none, because the API cannot be reached, answers with an error or runs past the deadline, the validation passes silently as it always does then, and no note is shown, though the file was never sent. |
|
|
22
|
+
| A `.mthds` bundle, or the `METHODS.toml` manifest at the tree's root | Nothing is sent, not even the version handshake, because every validation reads the method's bundles and its manifest, so no tree without one is the method. A non-blocking note names the method by its tree's root and every file of the tree that looks like a secret, the bundles and the manifest first, in place of a verdict. |
|
|
23
|
+
|
|
24
|
+
Leaving a file out rather than refusing the tree keeps the hook validating in the common case of a key file that merely sits beside a method, which no step reads and whose absence changes nothing in the verdict. A file a step does read, a `PipeDocGen` step's template or a `PipeFunc` step's module, is one the step cannot load without, so the verdict comes back invalid and blocks. The note, placed after that verdict's errors, says the hook kept the file home, so the agent reads the error as the hook's doing, and the fix it points to is the one the method needs anyway, since a run sends the same tree: move the secret out of the file.
|
|
25
|
+
|
|
26
|
+
A note names each file by its absolute path and what matched, **never the matched text**, since the hook's output is shown to a model and may be logged. Each path, and the root a refusal names, is written as a Markdown code span that no name can break out of: a control character in it, a line break included, is shown as an escape such as `\n`, and the span is fenced by more backticks than any run inside the path, so a file's name cannot add a line of its own to what the agent reads. The path is under the tree's root as the caller reached it, through a link above the root when that is how the edited file was named, and under the root's real path when a link points into the tree, since the caller's path then spells no way to the root. A note names the first ten files and counts the rest, and it never takes more than a share of the hook's output, so a verdict's errors keep their place and every character the note leaves them. Several files of one tree checked in one call, as a Codex patch may write, name the tree's note once, and a note is kept when another file's check blocks, since what stayed home matters whatever that check found. The reported case, a method directory holding its bundle, the template the bundle's `PipeDocGen` step fills, a `key.pem`, a `secrets.yaml` holding keys and a `service-account.json` export, sends the bundle and the template, and the valid verdict comes with this note:
|
|
27
|
+
|
|
28
|
+
> The .mthds hook kept these files of the method's tree on this machine, since each looks like a secret, and validated the method without them:
|
|
29
|
+
> - `/work/methods/foo/key.pem` (it holds a private key block (PEM))
|
|
30
|
+
> - `/work/methods/foo/secrets.yaml` (it holds an AWS access key id and an Anthropic API key)
|
|
31
|
+
> - `/work/methods/foo/service-account.json` (it holds a private key block (PEM))
|
|
32
|
+
>
|
|
33
|
+
> If a step reads one of them, as its template or its Python module, the validation reports that file missing until the secret is moved out of it. A key belongs in the environment, never in a method: a method's files leave this machine for the Pipelex API, which keeps them in the organization's storage, where every member can read them. Move the secret out of the file, or keep the file out of the method's tree with a `.gitignore` line inside the tree, since one above the tree's root is not read, or with a name starting with a dot. A file kept home for its name alone that holds no key can be renamed.
|
|
34
|
+
|
|
35
|
+
The manifest's `exclude` keeps a file out of the tree as well. A file kept out by any of these ways is never read by the hook, and so never named.
|
|
10
36
|
|
|
11
37
|
## Which file it checks
|
|
12
38
|
|
|
@@ -20,8 +20,9 @@ Uploads one asset and returns its upload record. It is the language-native conve
|
|
|
20
20
|
- **Path strings are Node-only.** A filesystem path string is read from disk, which only a process that owns a filesystem can do. In a non-Node runtime a path string fails instructively rather than being misread — the SDK does not silently treat it as text or as a URL.
|
|
21
21
|
- A string that is an **HTTP(S) URL** or an existing **`pipelex-storage://` URI** is not a local asset and passes through in any runtime (see pass-through rules below).
|
|
22
22
|
- Open file objects and streams are **deferred** — they can be added later without removing anything.
|
|
23
|
+
- **A test file.** `kind: "test"` with the `method_id` of the method it is for, among the options, records the upload as that method's test file rather than a person's file, sent with it to `POST /v1/upload`; the pair is checked before the asset is read ([files.md](./files.md#uploading-a-test-file)).
|
|
23
24
|
|
|
24
|
-
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment,
|
|
25
|
+
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment, and the main `@pipelex/sdk` entry bundles for a browser or edge target with nothing marked external: such a bundle resolves the Node host to its empty variant, a module holding no Node built-in ([architecture.md](./architecture.md#the-node-host)), so path strings stay Node-only, and where the host is absent a path string is refused with `InvalidLocalSourceError`, never read. The client still belongs on a server, since it holds an API key (Next.js Server Actions, the MCP server, the plugins hook), so a page imports the main entry for its error classes and types, and a page that must store a file uses the upload grant below instead, whose sender ships from the `@pipelex/sdk/upload` entry.
|
|
25
26
|
|
|
26
27
|
The returned **upload record** guarantees, beyond the source identity:
|
|
27
28
|
|
|
@@ -66,7 +67,7 @@ It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers
|
|
|
66
67
|
|
|
67
68
|
**The time limit.** The whole exchange with storage runs under a time limit — the request, the file going out, the answer coming back and the read of an error body — so a stalled upload settles even when the caller passes no signal. By default the limit is 60 s plus 1 s for every started 128 KiB of the file, which assumes about 1 Mbit/s and comes to 460 s for a 50 MiB file. `timeoutMs` replaces the default, for a slower link or a caller that wants a tighter bound; it must be a positive number no larger than 2147483647, the longest delay a timer honours, or the call throws an `InputPreparationError` before anything is sent. A `signal` ends the upload whenever it fires, so it can shorten the limit but never lengthen it, and a caller's abort still propagates as the signal's own reason even when the limit ran out too. When the limit runs out before storage answers, the call throws an `UploadTransportError` whose `code` is `timeout` and whose message names the seconds allowed. When it runs out while storage's error body is still arriving, storage has already answered, so the call settles as that answer instead, classified from its status and whatever part of the body arrived. After a timeout, whether storage stored the file is unknown: retrying with the same grant before it expires either stores the file or answers the `412` of a used grant, and then the grant's `uri` already names the file. The grant's expiry does not bound the upload, because storage checks the signature when the request starts, not when it ends.
|
|
68
69
|
|
|
69
|
-
**The browser-safe entry.** `uploadWithGrant`, the grant types and the three error classes it raises (`InputPreparationError`, `RejectedAssetError`, `UploadTransportError`) are exported from `@pipelex/sdk/upload`, whose import graph reaches no Node builtin, no `undici` and not the client, so a page bundles it with nothing marked external. They are the same function and the same classes the main entry exports, so an `instanceof` check agrees whichever entry the error came through. The unit suite bundles
|
|
70
|
+
**The browser-safe entry.** `uploadWithGrant`, the grant types and the three error classes it raises (`InputPreparationError`, `RejectedAssetError`, `UploadTransportError`) are exported from `@pipelex/sdk/upload`, whose import graph reaches no Node builtin, no `undici` and not the client, so a page bundles it with nothing marked external. They are the same function and the same classes the main entry exports, so an `instanceof` check agrees whichever entry the error came through. The unit suite bundles both entries for the browser with esbuild and fails if anything Node-only becomes reachable.
|
|
70
71
|
|
|
71
72
|
**Storage's refusals** map onto the input-preparation errors, so a caller that already handles `uploadFile` needs no new case:
|
|
72
73
|
|
|
@@ -160,6 +161,14 @@ client.prepareInputs({ method_id, inputs, allowUploads: false }) → PreparedInp
|
|
|
160
161
|
|
|
161
162
|
The error names the input as `inputName` and where inside it the value sits as `inputPath`: the input's name for a top-level file, then `.field` for each declared field and `[index]` for each list element (`dossier.attachments[1]`). An explicit envelope adds nothing to the path, since its `content` is the input's value. `valueKind` says what the value is, `local_path`, `data_url` or `bytes`, so a consumer can tell its caller what to send instead; the value itself is never in the message or on the error, since it is the caller's data. The option is `true` when omitted, must be a boolean when given — anything else is refused with an `InputPreparationError` before any request, since reading a stray `"false"` as permission would upload exactly what the caller meant to keep — and is not sent to the server. The option covers `prepareInputs` alone: `uploadFile` is an upload by definition.
|
|
162
163
|
|
|
164
|
+
#### Recording the uploads as test files
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
client.prepareInputs({ method_id, inputs, upload: { kind: "test", method_id } }) → PreparedInputs
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`upload` says what every file the preparation uploads is recorded as, sent with each upload: its `kind` and, for a test file, the bare catalog id of the method it is for, such as the inputs a build assistant makes to try a method. Omitted, every upload is a person's file. It is a member of its own so that its `method_id` never reads as the request's own `method_id` selector, and it is checked once, before the pipe I/O request, as `uploadFile` checks its options: anything the upload route would refuse with a `422` is an `InputPreparationError` naming `upload.kind` or `upload.method_id`, and an `upload` that is not an object is refused the same way. A file the route refuses for what a test file claims is a `TestFileRefusedError` ([files.md](./files.md#uploading-a-test-file)). It is not sent to the pipe I/O route, and under `allowUploads: false` it records nothing, since nothing is uploaded.
|
|
171
|
+
|
|
163
172
|
## A method's files in hand
|
|
164
173
|
|
|
165
174
|
Every method-taking operation — `execute` / `start`, `validate` / `resolve` / `codegen` / `pipeIo`, and `prepareInputs` — takes a `method_id` **natively** on the hosted API, as a server pass-through, its version suffix included (`mt_…`, `mt_…@<n>`, `mt_…@draft`): the platform resolves it and injects the tree of the version it names before the runner sees the request (see [architecture.md → hosted run extensions](./architecture.md#hosted-run-extensions-method_id)). That is the uniform rule — *an id option is always a server pass-through, and any client-side expansion is the caller's own explicit call*.
|
|
@@ -229,13 +238,14 @@ The contract distinguishes these semantic outcomes, each a typed subclass of `In
|
|
|
229
238
|
- **invalid input value** (`InvalidInputValueError`, carries `inputName` and `inputPath`) — a value at a file input that cannot be turned into a file to upload: a `data:` URL that does not decode (no comma, bad base64, bad percent-encoding), or a value of a type no file input takes, such as a number or an object that is neither bytes nor `{url}` content. It is raised while the inputs are read, before anything is uploaded. It names the input and the path inside it as `UploadNotAllowedError` does (`Input "dossier.attachments[1]" holds a number, which no file input takes: …`), describes the value by its kind and never quotes it, and lists the forms a file input takes in the mode it ran in. With `InvalidLocalSourceError`, it is how a consumer tells the inputs' own mistakes from a method that does not load, though all three are `InputPreparationError`s with the same verdict;
|
|
230
239
|
- **upload not allowed** (`UploadNotAllowedError`, carries `inputName`, `inputPath` and `valueKind`) — under `allowUploads: false`, a value at a file input that would need an upload: a local path, a `data:` URL or bytes. It is raised before anything is read, decoded or uploaded (see "[Preparing without uploads](#preparing-without-uploads)");
|
|
231
240
|
- **rejected asset** (`RejectedAssetError`) — the server refused it (e.g. a `413` past the service-defined size cap — see "Storage policy" — surfaced as a clear rejection with `code` `too_large`, not a raw transport error), or storage refused an upload with a grant (a used or expired grant, or a file that differs from what the grant signed — see the table above for each `code`);
|
|
232
|
-
- **
|
|
233
|
-
- **
|
|
241
|
+
- **refused test file** (`TestFileRefusedError`, carries `code`, `status`, `filename` and `methodId`) — the upload route refused a test file for what it claims, never for its bytes: `role_not_permitted`, the `403` of a caller whose role may not change methods; `method_not_found`, the `404` the platform answers as `not_found` in the `input` domain for a method that is not the organization's; `method_being_deleted`, the `409` of a method being erased. Each is read off the platform's code, never the status alone;
|
|
242
|
+
- **unsupported server capability** (`UnsupportedUploadCapabilityError`) — the configured deployment has no upload route: any other `404`, the platform's `config` one for a route it does not serve or a bare runner's, which carries no code;
|
|
243
|
+
- **authentication / authorization failure** (`UploadAuthenticationError`) — a `401`, or a `403` other than a test file's `role_not_permitted`;
|
|
234
244
|
- **transport failure** (`UploadTransportError`) — a network or server fault, or any other unexpected upload failure; its `status` is the HTTP status when a response produced it, and its `code` says which failure it was. From `uploadFile` the code is `server_error` for a `5xx`, `timeout` when the client's own request timeout ran out, `unreachable` when no response came back, and `unexpected` for any other status or failure, a `2xx` answer the SDK cannot read included, such as one with no string `uri`; `uploadWithGrant`'s codes are in the table above.
|
|
235
245
|
|
|
236
|
-
**Which file.** Every upload failure `uploadFile` raises names the file it was sending, as it sent it: `RejectedAssetError`, `UploadAuthenticationError`, `UnsupportedUploadCapabilityError` and `UploadTransportError` each carry `filename`, so a consumer that prepared several files says which one failed without parsing a message. `uploadWithGrant` sends a grant's file, not a named one, and leaves `filename` undefined on the `UploadTransportError` it raises.
|
|
246
|
+
**Which file.** Every upload failure `uploadFile` raises names the file it was sending, as it sent it: `RejectedAssetError`, `TestFileRefusedError`, `UploadAuthenticationError`, `UnsupportedUploadCapabilityError` and `UploadTransportError` each carry `filename`, so a consumer that prepared several files says which one failed without parsing a message. `uploadWithGrant` sends a grant's file, not a named one, and leaves `filename` undefined on the `UploadTransportError` it raises.
|
|
237
247
|
|
|
238
|
-
Each of these carries the verdict every SDK error does, `retryable` and `errorDomain` ([errors.md](./errors.md#each-classs-verdict)): the family's own is `input` and not retryable, an unsupported deployment and a refused credential are `config`, and a transport failure takes the verdict of the error it wraps, so a `402` plan refusal wrapped by `uploadFile` is `config` and not retryable, or, wrapping none, the verdict its `code` gives (`timeout`, `storage_timeout` and `conflict` retryable, `server_error` retryable unless storage answered a `501`, `unreachable` and `redirected` the environment's). A pipe I/O answer whose `is_valid` is not a boolean, whose `input_form` does not describe the pipe it selected, or whose descriptor for that pipe is malformed where the walk reads it or names a kind this SDK does not know, is a plain `InputPreparationError` with a `runtime` verdict, not retryable, since the route broke its own contract. The descriptor is checked whole, once, before any caller value is read or anything uploaded, so the refusal does not depend on which inputs the caller supplied, and its message names the pipe, where in the descriptor the fault sits (`fields[3].item`) and what was found there.
|
|
248
|
+
Each of these carries the verdict every SDK error does, `retryable` and `errorDomain` ([errors.md](./errors.md#each-classs-verdict)): the family's own is `input` and not retryable, an unsupported deployment and a refused credential are `config`, a refused test file takes the platform's verdict, `config` for a role and `input` for a method, and a transport failure takes the verdict of the error it wraps, so a `402` plan refusal wrapped by `uploadFile` is `config` and not retryable, or, wrapping none, the verdict its `code` gives (`timeout`, `storage_timeout` and `conflict` retryable, `server_error` retryable unless storage answered a `501`, `unreachable` and `redirected` the environment's). A pipe I/O answer whose `is_valid` is not a boolean, whose `input_form` does not describe the pipe it selected, or whose descriptor for that pipe is malformed where the walk reads it or names a kind this SDK does not know, is a plain `InputPreparationError` with a `runtime` verdict, not retryable, since the route broke its own contract. The descriptor is checked whole, once, before any caller value is read or anything uploaded, so the refusal does not depend on which inputs the caller supplied, and its message names the pipe, where in the descriptor the fault sits (`fields[3].item`) and what was found there.
|
|
239
249
|
|
|
240
250
|
All preparation failures are raised **before any run is created**.
|
|
241
251
|
|
package/docs/method-trees.md
CHANGED
|
@@ -31,7 +31,7 @@ const verdict = await client.validate({
|
|
|
31
31
|
});
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
**A directory is listed by the standard's lister.** The client loads the `mthds` package's `listMethodTree` when a call gives a `directory`, and lists it under the MTHDS standard's membership rule and Manifest Discovery: the tree is rooted at the nearest `METHODS.toml` at or above the directory, else at the directory itself, and holds every file the rule keeps. Hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` or the manifest's `exclude` keeps out, and **a run's inputs file** (`inputs.json`, `inputs.toml`) never travel with the method. A tree holding a symbolic link or a special file is refused rather than followed. The directory is listed under the limits the deployment announces (below), so a tree the deployment would refuse for its size is refused while it is listed, before any file is hashed. Every refusal is a `RequestArgumentError` naming the call and the directory, whose `cause` is `mthds`'s `MethodTreeError`, with its `code` (`not_found`, `symbolic_link`, `limit_exceeded`, `empty_tree` and the rest). Listing needs Node: a browser gives `contents`, `tree` or `files`.
|
|
34
|
+
**A directory is listed by the standard's lister.** The client loads the `mthds` package's `listMethodTree` when a call gives a `directory`, and lists it under the MTHDS standard's membership rule and Manifest Discovery: the tree is rooted at the nearest `METHODS.toml` at or above the directory, else at the directory itself, and holds every file the rule keeps. Hidden entries, `node_modules`, `__pycache__`, what a `.gitignore` or the manifest's `exclude` keeps out, and **a run's inputs file** (`inputs.json`, `inputs.toml`) never travel with the method. A tree holding a symbolic link or a special file is refused rather than followed. The directory is listed under the limits the deployment announces (below), so a tree the deployment would refuse for its size is refused while it is listed, before any file is hashed. Every refusal is a `RequestArgumentError` naming the call and the directory, whose `cause` is `mthds`'s `MethodTreeError`, with its `code` (`not_found`, `symbolic_link`, `limit_exceeded`, `empty_tree` and the rest). Listing needs Node: the lister comes from the Node host ([architecture.md](./architecture.md#the-node-host)), so a bundle built for the browser holds none of it, and a `directory` given outside Node is refused with `RequestArgumentError` before the version handshake, so before any request, and a browser gives `contents`, `tree` or `files`.
|
|
35
35
|
|
|
36
36
|
**A map in memory.** A text in `contents` is encoded in UTF-8, and a text a file cannot hold, one carrying a lone surrogate, is refused, as is a value that is neither a string nor a `Uint8Array`. The keys are sent as they are, and the platform refuses one that is not a tree path with the `422` `invalid_method_files` naming it.
|
|
37
37
|
|
|
@@ -166,7 +166,7 @@ A method whose draft names a tree the platform holds no record of answers `getMe
|
|
|
166
166
|
|
|
167
167
|
## Writing a tree to disk
|
|
168
168
|
|
|
169
|
-
`writeMethodTree(files, { destination })` writes a listing to disk, each file at its tree path under `destination`. It runs in Node only,
|
|
169
|
+
`writeMethodTree(files, { destination })` writes a listing to disk, each file at its tree path under `destination`. It runs in Node only, with the file system of the Node host ([architecture.md](./architecture.md#the-node-host)), and is refused with `RequestArgumentError` anywhere else, before any download:
|
|
170
170
|
|
|
171
171
|
```ts
|
|
172
172
|
const detail = await client.getMethod("mt_123");
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# An organization's branding
|
|
2
|
+
|
|
3
|
+
An organization may carry a brand: up to two logos, a square one and a wide one, and two colour sets of three colours, the light set and an optional dark one. The webapp shows it on the organization's menu and on the apps of its methods, and an app scaffolded from the method-app template reads it with its API key to wear the same brand. A personal workspace never has one. The client reaches it on two routes of the hosted API, and the types are in `src/product-models.ts`.
|
|
4
|
+
|
|
5
|
+
## The methods
|
|
6
|
+
|
|
7
|
+
| Method | Route | Who may call it | Answer |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| `getBranding()` | `GET /v1/branding` | Every member of the active organization, `runner` included, by web session or API key | The active organization's `OrganizationBranding`. |
|
|
10
|
+
| `updateOrganizationBranding(orgId, input)` | `PATCH /v1/organizations/{org_id}/branding` | An admin of the active organization, by web session | The `OrganizationBranding` as the write left it. |
|
|
11
|
+
|
|
12
|
+
Both answer the same shape, so a client holds one type whichever it called. `getBranding` takes no organization id: it reads the caller's active organization.
|
|
13
|
+
|
|
14
|
+
## The branding
|
|
15
|
+
|
|
16
|
+
| Field | Type | Meaning |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `org_id` | `string` | The organization the branding belongs to. |
|
|
19
|
+
| `org_name` | `string` | Its display name, the wordmark a client shows when it has no logo; an API key has no other route to read it. |
|
|
20
|
+
| `colors` | `BrandingColors` | The light colour set. |
|
|
21
|
+
| `dark_colors` | `BrandingColors` | The dark colour set, used in dark mode only. |
|
|
22
|
+
| `square_logo` | `LogoDescriptor \| null` | The square logo, for the organization menu and small headers. |
|
|
23
|
+
| `wide_logo` | `LogoDescriptor \| null` | The wide logo, for an app's header. |
|
|
24
|
+
|
|
25
|
+
A `BrandingColors` always carries its three keys, each `#rrggbb` in lower case or `null` when not set: `action` is the button colour, `accent` the accent colour and `header` the header band's colour. A dark colour that is `null` falls back to the light colour of the same role, and a light one that is `null` to Pipelex's own palette for that theme. The `@pipelex/mthds-form/brand` entry turns a colour set into the roles a page paints with (`resolveDarkSeeds`, then `deriveBrandRoles` once per theme).
|
|
26
|
+
|
|
27
|
+
A `LogoDescriptor` is what the platform recorded when it accepted the logo:
|
|
28
|
+
|
|
29
|
+
| Field | Meaning |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `uri` | The platform's own copy of the logo, `pipelex-storage://<org_id>/branding/<uuid>.<ext>`, never the upload URI the write sent: the platform copies every logo it accepts, under a fresh uuid each time, so the same bytes attached again get a new URI. Sending it back unchanged in its slot keeps the logo ([below](#writing-it)). `resolveStorageUrl` turns it into a short-lived link to its bytes; the webapp serves it through its asset proxy instead. |
|
|
32
|
+
| `sha256` | The SHA-256 of the logo's bytes, 64 lower-case hexadecimal digits, which serves as a cache key for them. |
|
|
33
|
+
| `content_type` | The type the platform read from the file's own bytes: `image/png`, `image/jpeg`, `image/webp` or `image/svg+xml`. |
|
|
34
|
+
| `width`, `height` | The size in pixels for a raster, oriented as a browser draws it; for an SVG, its `width` and `height` in pixels when both are in absolute units, which is the size a browser draws it at, else the width and height of its `viewBox`; an SVG's may be fractional. Their ratio sizes the logo's box before the image loads. |
|
|
35
|
+
|
|
36
|
+
## Writing it
|
|
37
|
+
|
|
38
|
+
Every key of an `OrganizationBrandingUpdate` is optional: a value sets it, `null` clears it, and a key left out keeps its value, so `{}` writes nothing and answers the branding as it stands. A key holding `undefined` is a key left out, since it does not survive JSON.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// Set the button and accent colours, keep the header colour, clear the whole dark set,
|
|
42
|
+
// attach a square logo and remove the wide one.
|
|
43
|
+
const branding = await client.updateOrganizationBranding(orgId, {
|
|
44
|
+
colors: { action: "#14213d", accent: "#fca311" },
|
|
45
|
+
dark_colors: null,
|
|
46
|
+
square_logo: "pipelex-storage://<org_id>/assets/<file>.png",
|
|
47
|
+
wide_logo: null,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- **Colours** (`BrandingColorsUpdate`): each of `action`, `accent` and `header` takes `#rrggbb` in either case, stored in lower case. `colors: null` or `dark_colors: null` clears all three colours of that set.
|
|
52
|
+
- **Logos**: `square_logo` and `wide_logo` take one of three values. The `pipelex-storage://` URI of a file the organization uploaded on its own through `requestUploadGrant` or `upload` sets a new logo: the platform reads and measures it, then writes its own copy under `branding/`, and the answer's `uri` names that copy, never the upload, which the platform leaves as it is. The `uri` the branding answers for that slot, sent back unchanged, leaves the slot out of the write: nothing is read, and the slot keeps whatever logo the branding names when the write lands, so a save that resends the logo it read never puts back one a concurrent save replaced. `null` removes the logo. Any other URI is refused as `logo_not_an_upload`: a run's or a method's file, a `branding/` copy the slot no longer holds, and the other slot's logo too, so a logo cannot move between slots by its URI; upload it again for the other slot instead. Once the write has landed, the platform erases the `branding/` copy it replaced or cleared.
|
|
53
|
+
- **The rules a logo meets**: at most 1 MiB; a PNG, JPEG, WebP or SVG image by its bytes, whatever type the upload declared, an SVG in UTF-8 or, behind its byte-order mark, UTF-16; a still image, so never an animated PNG (an APNG) or an animated WebP; a raster of at most 4096 × 4096 pixels that decodes whole; a square logo's longer side at most 10% longer than its shorter, and a raster one at least 128 px on each side; a wide logo at least one and a half times as wide as it is tall, and a raster one at least 64 px tall. An SVG is measured from its root element, by its `width` and `height` when both are in absolute units, else by its `viewBox`, and each side must lie between 0.001 and 1,000,000, in pixels for a `width` or `height` and in the SVG's own units for a `viewBox`. It must declare no XML entities or external references and carry no active content: no `script` or `foreignObject` element, no element in the XHTML namespace, no `on…` event handler attribute, no `set` or `animate` that rewrites a link, no `xml-stylesheet` processing instruction, and no link but a reference inside the file (`#…`) or an embedded PNG, JPEG, GIF or WebP image (`data:image/…`), whether an `href`, an `xlink:href`, or a CSS `url()` or `src()`. Wherever a browser reads CSS, which is a `<style>` element's text and any attribute in no namespace or the SVG one, such as `style`, `fill` or `filter`, it must hold no `@import`, no `image()` or `image-set()`, and no escape (a backslash, which can spell a link out of sight). A `<style>` element itself is allowed, so the class-based styles a design tool exports pass, and so does a reference inside the file such as `fill:url(#grad)`; an attribute in another namespace, such as Inkscape's `inkscape:export-filename`, and a `data-*` one, such as the layer names Illustrator writes in `data-name`, are never read as CSS.
|
|
54
|
+
|
|
55
|
+
The write sends no `Idempotency-Key`, as `renameOrganization` sends none. A later write that should keep a logo leaves its key out, or sends back the `uri` the branding answered.
|
|
56
|
+
|
|
57
|
+
## When a write is refused
|
|
58
|
+
|
|
59
|
+
A refused write changes nothing, whichever part of it was refused, and throws an `ApiResponseError` whose `code` is the platform's; only a `503` leaves it unknown whether the write landed:
|
|
60
|
+
|
|
61
|
+
| Status | `code` | When |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `403` | `forbidden` | `orgId` is not the caller's active organization. |
|
|
64
|
+
| `403` | `role_not_permitted` | The caller is not the organization's admin. |
|
|
65
|
+
| `404` | `not_found` | The organization does not exist. |
|
|
66
|
+
| `409` | `conflict` | The organization is a personal workspace, which has no branding. |
|
|
67
|
+
| `422` | `validation_failed` | A colour is not `#rrggbb`, the body holds a key the route does not take, or a logo was refused. |
|
|
68
|
+
| `400` | `invalid_storage_uri` | A logo's URI is not a storage URI. |
|
|
69
|
+
| `403` | `forbidden` | A logo's URI is a file of another organization. |
|
|
70
|
+
| `500` | `storage_backend_failed` | The platform's storage failed while reading or copying a logo. Its verdict is `runtime` and retryable, and nothing was written. |
|
|
71
|
+
| `503` | `service_unavailable` | The platform's database failed or could not be reached, so the write may have landed or not. Its verdict is `runtime` and retryable: the same `input` sent again is safe, and `getBranding` reads what the branding holds. |
|
|
72
|
+
|
|
73
|
+
**A refusal about a logo or a colour names it in `errors`**, the platform's field-level list, so the settings screen shows each sentence under the control it concerns rather than in a toast. Each entry's `field` is the slot, `square_logo` or `wide_logo`, or the colour's dotted key, such as `colors.action` or `dark_colors.header`; its `detail` is the sentence to show. A logo the platform read and refused carries a `LogoRefusalCode`, one entry per broken rule, both slots answered in one refusal:
|
|
74
|
+
|
|
75
|
+
| `code` | Example `detail` |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `logo_not_an_upload` | The square logo must be a newly uploaded file (pipelex-storage://<organization>/assets/<file>) or the URI the square logo already has. |
|
|
78
|
+
| `logo_not_found` | The square logo file was not found in the organization's uploads; upload it again. |
|
|
79
|
+
| `logo_too_large` | The wide logo file is 2,048 KiB; a logo file can be at most 1 MiB (1,024 KiB). |
|
|
80
|
+
| `logo_type_not_accepted` | The square logo file is a GIF image; a logo must be a PNG, JPEG, WebP or SVG image. (An animation reads "The square logo is an animated WebP; a logo must be a still image.", or "an animated PNG" for an APNG.) |
|
|
81
|
+
| `logo_unreadable` | The wide logo is an SVG that cannot be read: it is not well-formed XML. (An SVG side out of range reads "… its width or height lies outside the 0.001 to 1,000,000 px a logo's side may measure.", or "… its viewBox's width or height lies outside the 0.001 to 1,000,000 units a logo's side may measure." A raster reads "The square logo looks like a JPEG image but cannot be read: the file is damaged or incomplete.", its one frame being decoded whole so a file cut short is caught whatever its format, or "… at 5,000 × 5,000 px it has more than the 16,777,216 pixels (4096 × 4096) a logo may have.") |
|
|
82
|
+
| `logo_active_content` | The square logo is an SVG that carries active content, which a logo may not: a \<script\> element. (Or: a \<foreignObject\> element; an element in the XHTML namespace; an event handler attribute (onload); a \<set\> element that rewrites a link; a link to something outside the file, where only a reference inside it or an embedded PNG, JPEG, GIF or WebP image is allowed; an xml-stylesheet processing instruction, which loads a style sheet; a CSS @import, which loads a style sheet; a CSS url() that links to something outside the file, where only a reference inside it or an embedded PNG, JPEG, GIF or WebP image is allowed, which reads "a CSS src()" for `src()`; a CSS image-set() function, which can load an image from outside the file, which reads "a CSS image() function" for `image()`; a CSS escape (a backslash), which can disguise a link.) |
|
|
83
|
+
| `logo_not_square` | The square logo is 640 × 512 px, 1.25 times as wide as it is tall; a square logo's longer side can be at most 10% longer than its shorter side. |
|
|
84
|
+
| `logo_too_small` | The square logo is 100 × 100 px; a square logo must be at least 128 px on each side. |
|
|
85
|
+
| `logo_not_wide` | The wide logo is 600 × 500 px, 1.2 times as wide as it is tall; a wide logo must be at least 1.5 times as wide as it is tall. |
|
|
86
|
+
| `logo_too_short` | The wide logo is 240 × 48 px; a wide logo must be at least 64 px tall. |
|
|
87
|
+
|
|
88
|
+
The two refusals of a logo URI that come before any file is read, `invalid_storage_uri` and another organization's `forbidden`, name the slot the same way, with the platform's code in the entry. A colour's entry carries pydantic's code, such as `string_pattern_mismatch`. Reading one slot's sentences is the same filter in every case:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
try {
|
|
92
|
+
await client.updateOrganizationBranding(orgId, { square_logo: uri });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (!(err instanceof ApiResponseError)) throw err;
|
|
95
|
+
const underSquare = (err.errors ?? [])
|
|
96
|
+
.filter((entry) => entry.field === "square_logo")
|
|
97
|
+
.map((entry) => entry.detail);
|
|
98
|
+
// Show `underSquare` under the square logo's slot; an error naming no field goes in a toast.
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`getBranding` answers `400` when the caller has no active organization and `404` when its organization's row is missing. Either method throws the `ApiResponseError` of an answer it cannot read for a `2xx` that is not a JSON object, an empty one included.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
"default": "./dist/upload-grant.js"
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
+
"imports": {
|
|
38
|
+
"#node-host": {
|
|
39
|
+
"node": "./dist/node-host/node.js",
|
|
40
|
+
"default": "./dist/node-host/unavailable.js"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
37
43
|
"scripts": {
|
|
38
44
|
"build": "tsc",
|
|
39
45
|
"build:hook": "node scripts/build-hook.mjs",
|