foldrun 5.0.0 → 5.0.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/docs/api.md +3 -3
- package/docs/budgets-and-billing.md +6 -35
- package/docs/cli.md +1 -1
- package/docs/evals.md +2 -2
- package/docs/feature-flags.md +2 -5
- package/docs/security.md +24 -0
- package/docs/status.md +1 -2
- package/docs/tools.md +7 -1
- package/package.json +1 -1
- package/src/commands.mjs +32 -3
- package/src/credentials.mjs +7 -0
package/docs/api.md
CHANGED
|
@@ -41,7 +41,7 @@ read (a deploy with issues, an `Idempotency-Key` reused with a different body),
|
|
|
41
41
|
than the person who minted it, and opens every route below with that — except
|
|
42
42
|
the ones that are about people: `/api/team` (invite, roles, scope, remove,
|
|
43
43
|
transfer), `/api/auth/*` (profile, password, MFA, sessions), a user's
|
|
44
|
-
`/api/avatars/user/<id
|
|
44
|
+
`/api/avatars/user/<id>`. Those need a signed-in
|
|
45
45
|
session and answer a key with `401` (a user's avatar answers a key with
|
|
46
46
|
`403`: the key is read, then refused because it is not that person). Everything a workspace or an account
|
|
47
47
|
*does* — files, runs, secrets, shares, schedules, hooks, storage, billing —
|
|
@@ -335,7 +335,7 @@ Reads return which secrets exist, never their values.
|
|
|
335
335
|
|
|
336
336
|
| Route | Methods | |
|
|
337
337
|
|---|---|---|
|
|
338
|
-
| `/api/keys` | GET, POST, DELETE | API keys — an admin's (`keys:manage`) to list, mint and revoke; any key may revoke itself (DELETE with its own `id`, audited as self — what `foldrun logout` does). POST `label`, `role` (viewer \| editor \| admin, never above your own, default editor), `workspaces` (names, or `null` for all — never wider than your own scope); `expires` (`30d`, `90d`, `365d`, any number of days up to 3650, or `never` — the default; anything else is a 400, never a key that lives forever); returns `{ ok, key, id, prefix, role, workspaces, expiresAt }`, and the key is shown once. GET lists each key with `createdAt`, `createdBy`, `expiresAt`, `lastUsedAt`, `lastIp` and `replacedBy` (the id of the key a rotation replaced it with; `null` until then) — the last-use pair written at most once a minute per key, the address cut to its /24 (IPv4) or /48 (IPv6). A key past its `expiresAt` is refused with 401 `API key expired on <date>`; the owner is emailed seven days before. Mint, revoke and rotate are written to the audit log. POST with `workspace` or `access` is refused (400): those minted deploy keys for the platform's own git, which went on 17 Oct 2026 — a deploy key minted before is refused everywhere (403). A key for one workspace is `workspaces: ["<ws>"]`. DELETE `id`. `?tenant=` / body `tenant` is read only on an install with no users yet (first-run bootstrap) |
|
|
338
|
+
| `/api/keys` | GET, POST, DELETE | API keys — an admin's (`keys:manage`) to list, mint and revoke; any key may revoke itself (DELETE with its own `id`, audited as self — what `foldrun logout` does). POST `label`, `role` (viewer \| editor \| admin, never above your own, default editor), `workspaces` (names, or `null` for all — never wider than your own scope); `expires` (`30d`, `90d`, `365d`, any number of days up to 3650, or `never` — the default; anything else is a 400, never a key that lives forever); returns `{ ok, key, id, prefix, role, workspaces, expiresAt }`, and the key is shown once. GET lists each key with `createdAt`, `createdBy`, `expiresAt`, `lastUsedAt`, `lastIp` and `replacedBy` (the id of the key a rotation replaced it with; `null` until then) — the last-use pair written at most once a minute per key, the address cut to its /24 (IPv4) or /48 (IPv6). A key past its `expiresAt` is refused with 401 `API key expired on <date>`; the owner is emailed seven days before. Mint, revoke and rotate are written to the audit log. POST with `workspace` or `access` is refused (400): those minted deploy keys for the platform's own git, which went on 17 Oct 2026 — a deploy key minted before is refused everywhere (403). A key for one workspace is `workspaces: ["<ws>"]`. DELETE `id`. `?tenant=` / body `tenant` is read only on an install with no users yet (first-run bootstrap), and there, when the install has a setup token (`FOLDRUN_SETUP_TOKEN`, written by the self-hosting installer), only with that token in `X-Foldrun-Setup-Token` |
|
|
339
339
|
| `/api/keys/<id>/rotate` | POST | a new key with the old one's label, role, workspaces and deploy scope — and, when it had an end, the same lifetime from today — returned once like a mint (`{ ok, key, id, prefix, role, workspaces, expiresAt, replaced: { id, endsAt } }`). `grace`: `0` (default — the old key is revoked at once), or a duration up to `7d` such as `1h` or `24h`, during which the old key still works and after which it is refused as expired. A key is rotated once: rotating a key already replaced is 409 `this key was already rotated to <prefix>…; rotate that one` — and someone who lost the new key's secret rotates the NEW key, which mints another and ends the lost one (rotating the old one again would leave the lost key working). Two rotations of one key at once mint one key. `keys:manage`; a key outside the caller's scope is 404. Audited as `apikey.rotated` |
|
|
340
340
|
| `/api/team` | GET, POST | members with their roles and workspace scope; mint an invite — `role` (viewer \| editor \| admin, default editor), `workspaces` (names, or omit for all), `email` (bound into the invite: only that address can accept it; sent through the account's `RESEND_API_KEY` unless `send: false`). Returns `path` (the invite's relative URL), `expires`, `sent` and any `sendError` either way; the full link is in the mail |
|
|
341
341
|
| `/api/team/members/<id>` | PATCH, DELETE | change a member's role (`role`) and/or which workspaces they may open (`workspaces`: names, or null for all); remove them |
|
|
@@ -404,7 +404,7 @@ person: a key names an account, never a who.
|
|
|
404
404
|
| `/api/billing/wallet` | GET, PUT | the balance summary (burn, runway, daily spend, month to date, the suggested top-up), the refill rule and the low-balance email — GET `{ enabled, stripe, summary, cardSaved, autoTopUp, email, dunning }`, PUT `autoTopUp`, `email`. `dunning.status` is `none`, `grace` (a plan invoice failed; runs go on until `graceUntil`) or `suspended` (new runs paused until it is paid), with `payUrl`, `amountDue` (minor units) and `currency` |
|
|
405
405
|
| `/api/billing/plans` | GET | the catalogue (four plans: fee, credits, per-credit price, runs at once, workspaces, lane), where this account stands (plan, status, this cycle's credits and what is left, when it resets), the balance in dollars and credits, and any overdraft the platform has allowed |
|
|
406
406
|
| `/api/billing/subscribe` | POST, DELETE | POST `plan` — starter, creator, pro or scale. With no subscription yet, answers `url`: a Stripe Checkout in subscription mode; the return trip starts the first cycle. With a live one, changes it at Stripe at once, prorated (an upgrade is invoiced today and the bigger bundle lands with it; a downgrade comes back as credit on the next invoice), and answers `changed: true` — only once its prorated invoice is paid: when the payment needs the customer (a declined card, 3-D Secure) the change waits at Stripe as a pending update, nothing about the plan moves, and the answer is 402 `{ pending: true, payUrl }` — paying that invoice applies it; unpaid, Stripe drops it and voids the invoice after 23 hours. A plan set to stop and changed this way stays set to stop once paid — resuming it is the customer's own choice (`resume: true`); nothing resumes on its own. A plan set to stop at the period's end is still that live subscription: choosing a plan takes the stop off (and changes it if it is another plan) — never a second Checkout. With no subscription known here but one still billing at Stripe for the account's customer, 409: a second is not started. POST `resume: true` takes a pending cancellation off — nothing is charged and no cycle moves. DELETE ends the plan with the current cycle: the credits stand until then, and credits bought on top are kept |
|
|
407
|
-
| `/api/billing/stripe` | POST | Stripe's webhook — signature-verified, not for you. Every delivery is logged
|
|
407
|
+
| `/api/billing/stripe` | POST | Stripe's webhook — signature-verified, not for you. Every delivery is logged. Beside top-ups it turns plan cycles: one grant per (subscription, plan, period), whichever of the checkout, `invoice.paid` (either invoice shape) or `customer.subscription.updated` arrives first; `invoice.payment_failed` marks the plan past due and starts dunning (an email, a grace period, then new runs paused — see budgets-and-billing.md), `invoice.paid` and `invoice.voided` end it, `customer.subscription.deleted` ends the plan that subscription paid for — what is left of the bundle expires, the levers come off, money the customer added stays. `charge.refunded` debits a refunded top-up (never a plan fee), `charge.dispute.created` debits a chargeback and `charge.dispute.closed` returns it when won. Answers 500 — so Stripe redelivers — only when a lookup the event needs failed. The full event list is in budgets-and-billing.md |
|
|
408
408
|
|
|
409
409
|
## Versions
|
|
410
410
|
|
|
@@ -198,7 +198,7 @@ and on the audit; it is a term, never a surprise.
|
|
|
198
198
|
|
|
199
199
|
**Charges an account does not pay.** The platform can waive any of the three
|
|
200
200
|
meters for one account: model tokens, sandbox time, network. It is set per
|
|
201
|
-
account
|
|
201
|
+
account by the platform, never as an install-wide rate, because a
|
|
202
202
|
rate turned off globally would quietly stop charging every customer. The
|
|
203
203
|
meters still run and the Usage page still shows everything measured; what
|
|
204
204
|
changes is the bill. Every run still writes a ledger line — at zero when
|
|
@@ -390,26 +390,6 @@ runs, schedules stop firing — while the platform team settles the last
|
|
|
390
390
|
invoice and any refund. Nothing is deleted by the request itself; people can
|
|
391
391
|
still sign in and take the copy.
|
|
392
392
|
|
|
393
|
-
## The admin console's money levers
|
|
394
|
-
|
|
395
|
-
Each customer (account) in the super admin console has a Billing tab with
|
|
396
|
-
every lever that changes what the customer pays, each behind a confirmation
|
|
397
|
-
that names the amount and the account: **credit or debit** with a reason on
|
|
398
|
-
the ledger (the console sends an idempotency key, so a double click is one
|
|
399
|
-
line), **extend the trial** (more trial credits as a grant, with whatever
|
|
400
|
-
trial credit is left carried into it; not for an account on a plan), the
|
|
401
|
-
**plan** (grant a cycle by hand, end it, and the lane and limits that ride
|
|
402
|
-
with it), **going into minus**, and **charges this account does not pay**.
|
|
403
|
-
The overview beside it holds the contact on record (name, email, phone,
|
|
404
|
-
company, ABN), notes, suspension, and the customer's Stripe customer and
|
|
405
|
-
subscription with links into Stripe's own dashboard — test or live, read
|
|
406
|
-
from the install's key.
|
|
407
|
-
|
|
408
|
-
Revenue has a CSV per month for the accountant (`GET /api/admin/export`):
|
|
409
|
-
one row per customer and a total, charges by leg, fees, top-ups, grants,
|
|
410
|
-
expiries, refunds, disputes, adjustments and net, with Stripe's own figures
|
|
411
|
-
for the month beside them when Stripe is connected.
|
|
412
|
-
|
|
413
393
|
## The wallet
|
|
414
394
|
|
|
415
395
|
Usage is settled from a credit balance. Add credit by card, once or with
|
|
@@ -469,11 +449,7 @@ dispute-won, plan, trial, grant-expired, credits-expired — and lines written
|
|
|
469
449
|
before the field have theirs derived from their key and note. Statements
|
|
470
450
|
label lines by it. **Burn**, the 30-day spend and month-to-date count usage
|
|
471
451
|
and the daily fees only — not expiries, refunds, chargebacks or an
|
|
472
|
-
operator's debit.
|
|
473
|
-
account pod sessions) in the customer list and the money report alike;
|
|
474
|
-
**paid in** is top-ups paid plus plan fees from the invoices Stripe said
|
|
475
|
-
were paid; **held in wallets** is credit bought and unspent — granted
|
|
476
|
-
credit is shown apart, because nobody paid for it by the dollar.
|
|
452
|
+
operator's debit.
|
|
477
453
|
|
|
478
454
|
**Hand-placed plans renew.** A plan the super admin grants runs a month and
|
|
479
455
|
is renewed by the hourly sweep at its period's end, with the same grant and
|
|
@@ -482,9 +458,6 @@ rollover rules. Granting a plan to an account Stripe is billing is refused
|
|
|
482
458
|
the subscription at Stripe first, at once, and changes nothing if Stripe
|
|
483
459
|
refuses.
|
|
484
460
|
|
|
485
|
-
**An operator's credit or debit is written once** per idempotency key the
|
|
486
|
-
console sends, and audited once.
|
|
487
|
-
|
|
488
461
|
**Stripe.** Every call pins its API version (`2025-03-31.basil`;
|
|
489
462
|
`STRIPE_API_VERSION` overrides) — create the webhook endpoint on the same
|
|
490
463
|
version — and charges in `FOLDRUN_CURRENCY` (default `usd`), which is also
|
|
@@ -513,12 +486,10 @@ kept too, for the types above only, up to 256 KB, set when it first
|
|
|
513
486
|
arrives and emptied after **180 days**: Stripe keeps events for 30, and a
|
|
514
487
|
replay after that needs a copy. It carries what Stripe sends (names,
|
|
515
488
|
emails, addresses, a card's last four digits — never a full card number),
|
|
516
|
-
so
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
write it makes is keyed once, so a replay of an event that was handled
|
|
521
|
-
changes nothing.
|
|
489
|
+
so it is never shown to the account. The platform can replay an event
|
|
490
|
+
through the same handler — Stripe's copy while Stripe has it, the kept
|
|
491
|
+
copy after; every write it makes is keyed once, so a replay of an event
|
|
492
|
+
that was handled changes nothing.
|
|
522
493
|
|
|
523
494
|
## Plan gates
|
|
524
495
|
|
package/docs/cli.md
CHANGED
|
@@ -449,7 +449,7 @@ this way: they can take it back without changing a password.
|
|
|
449
449
|
|
|
450
450
|
**You reset it, as the platform.** Only the platform's own super admin can,
|
|
451
451
|
only on the hosted platform, and it ends every session that account had:
|
|
452
|
-
|
|
452
|
+
from its console, the member → **reset password**. The new password is
|
|
453
453
|
shown once. Sign in as them in a private window, then `foldrun login
|
|
454
454
|
--profile <name>` and approve the code in that window.
|
|
455
455
|
|
package/docs/evals.md
CHANGED
|
@@ -47,9 +47,9 @@ satisfy. The heading is the case's name, so make it the thing being checked.
|
|
|
47
47
|
|---|---|
|
|
48
48
|
| `contains:` | the text must appear |
|
|
49
49
|
| `not-contains:` | the text must not appear |
|
|
50
|
-
| `matches:` | a JavaScript regular expression over the reply, case-insensitive. No inline flags such as `(?m)`: write `(^\|\n)` for the start of a line. `foldrun check` reports one that does not compile |
|
|
50
|
+
| `matches:` | a JavaScript regular expression over the reply, case-insensitive, given a quarter of a second (a pattern that backtracks fails the check). No inline flags such as `(?m)`: write `(^\|\n)` for the start of a line. `foldrun check` reports one that does not compile |
|
|
51
51
|
| `file:` | the path exists and is non-empty |
|
|
52
|
-
| `run:` | a shell command that must exit 0 |
|
|
52
|
+
| `run:` | a shell command that must exit 0 — `foldrun eval` on your machine only: an install that isolates runs refuses it (the check fails, saying so), because there it would run on the platform's own web server rather than in a sandbox |
|
|
53
53
|
| `judge:` | a model grades the reply against this sentence |
|
|
54
54
|
|
|
55
55
|
**Deterministic checks run first and cost nothing.** The judge only runs if they
|
package/docs/feature-flags.md
CHANGED
|
@@ -46,11 +46,8 @@ foldrun flags --json
|
|
|
46
46
|
|
|
47
47
|
## Running the platform yourself
|
|
48
48
|
|
|
49
|
-
On
|
|
50
|
-
the install-wide on/off and rollout, and an override per account.
|
|
51
|
-
is `PUT`/`DELETE /api/admin/flags/<flag>` and
|
|
52
|
-
`/api/admin/flags/<flag>/accounts/<account>` (see the API reference's
|
|
53
|
-
super admin section). Every change is on the platform's audit as
|
|
49
|
+
On the hosted platform the operator changes flags from its own console:
|
|
50
|
+
the install-wide on/off and rollout, and an override per account. Every change is on the platform's audit as
|
|
54
51
|
`operator.flag.global`, `operator.flag.global_cleared`,
|
|
55
52
|
`operator.flag.account` or `operator.flag.account_cleared`, with who made
|
|
56
53
|
it and what it was before. The settings are rows in Postgres
|
package/docs/security.md
CHANGED
|
@@ -152,6 +152,30 @@ name only — for the owner and admins to read or export. Support can view an
|
|
|
152
152
|
account only [read-only, for thirty minutes, with a reason the customer
|
|
153
153
|
reads](team-and-access#support-access).
|
|
154
154
|
|
|
155
|
+
Signing in, in particular:
|
|
156
|
+
|
|
157
|
+
- a password reset or change, two-step sign-in turned on or off, and a new
|
|
158
|
+
API key each send a notice to the person's address;
|
|
159
|
+
- signing out ends the session on the server, not just in the browser;
|
|
160
|
+
- a two-step code works once; replacing a second factor means turning it
|
|
161
|
+
off first, which needs the password and a code;
|
|
162
|
+
- the sign-in forms refuse a request from another site, and `?next=` only
|
|
163
|
+
ever leads to a page on the same site;
|
|
164
|
+
- `foldrun login` asks you to type the code your terminal shows — a link
|
|
165
|
+
with the code in it, sent by someone else, cannot sign their machine in
|
|
166
|
+
as you;
|
|
167
|
+
- before the first sign-up, a self-hosted install's setup token guards key
|
|
168
|
+
minting as it guards sign-up;
|
|
169
|
+
- only the account's owner can set the account's `RESEND_API_KEY` and
|
|
170
|
+
`EMAIL_FROM`, because that sender carries everyone's reset links;
|
|
171
|
+
- a member or key limited to some workspaces cannot change what every
|
|
172
|
+
workspace shares — the library, the account's AGENTS.md defaults, its
|
|
173
|
+
model provider, its OAuth apps — nor people who work outside its
|
|
174
|
+
workspaces.
|
|
175
|
+
|
|
176
|
+
Frontmatter is YAML only: a file opened with `---js` (which some parsers
|
|
177
|
+
run as code) is refused.
|
|
178
|
+
|
|
155
179
|
## The dashboard in your browser
|
|
156
180
|
|
|
157
181
|
Every page is sent with an enforced Content-Security-Policy: scripts run
|
package/docs/status.md
CHANGED
|
@@ -46,8 +46,7 @@ not see a network problem between you and the platform — the page saying
|
|
|
46
46
|
|
|
47
47
|
## Incidents and maintenance
|
|
48
48
|
|
|
49
|
-
The
|
|
50
|
-
`/api/admin/incidents`): a title, what customers should read, the impact
|
|
49
|
+
The platform's operator posts them from its console: a title, what customers should read, the impact
|
|
51
50
|
(minor or major) and the components affected. Each update adds a line with
|
|
52
51
|
a status — `investigating`, `identified`, `monitoring`, `resolved`.
|
|
53
52
|
Maintenance is a window with a start and an end; it shows as upcoming before
|
package/docs/tools.md
CHANGED
|
@@ -1890,7 +1890,13 @@ tool is broken" and "the agent used it wrong", and it costs no model call.
|
|
|
1890
1890
|
|
|
1891
1891
|
Every tool has a Test button, and `POST /api/workspaces/<ws>/tools/<tool>/test`
|
|
1892
1892
|
behind it: it calls the tool with typed arguments from where a run would call
|
|
1893
|
-
it, shows the raw result, and costs no model call.
|
|
1893
|
+
it, shows the raw result, and costs no model call. On an install that
|
|
1894
|
+
isolates runs (the hosted platform, a Docker or Kubernetes self-host) a
|
|
1895
|
+
script tool or an MCP server started by `command:` is your own code, which
|
|
1896
|
+
runs only inside a step's sandbox — the button says so and points to
|
|
1897
|
+
`foldrun tool test` on your machine, or a test run. An HTTP tool is tested
|
|
1898
|
+
as before, except that a secret bound to sites
|
|
1899
|
+
(`foldrun secrets sites`) is never sent to a host outside them. A single-file tool that
|
|
1894
1900
|
carries its program in a fenced block is materialised for the test exactly as
|
|
1895
1901
|
a call materialises it, so the test runs the identical program from the
|
|
1896
1902
|
identical directory — for a long time it did not, and every fenced-code tool
|
package/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -3835,6 +3835,13 @@ async function connect(positional, flags) {
|
|
|
3835
3835
|
const DEFAULT_PLATFORM = "https://app.foldrun.io";
|
|
3836
3836
|
|
|
3837
3837
|
function openInBrowser(target) {
|
|
3838
|
+
// A server-supplied address: only a web page, never a file, an app or a
|
|
3839
|
+
// custom scheme handed to `open`.
|
|
3840
|
+
try {
|
|
3841
|
+
if (!/^https?:$/.test(new URL(target).protocol)) return Promise.resolve(false);
|
|
3842
|
+
} catch {
|
|
3843
|
+
return Promise.resolve(false);
|
|
3844
|
+
}
|
|
3838
3845
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3839
3846
|
return import("node:child_process")
|
|
3840
3847
|
.then(({ spawn }) => {
|
|
@@ -4872,11 +4879,13 @@ async function pullCmd(layout, flags, only) {
|
|
|
4872
4879
|
/** @type {{file: string, content: string}[]} */
|
|
4873
4880
|
const incoming = [];
|
|
4874
4881
|
for (const name of names) {
|
|
4882
|
+
if (!SAFE_NAME.test(name)) throw new Error(`the platform named a workspace "${name}", which is not a workspace name — refusing to write it`);
|
|
4875
4883
|
const there = await remoteWorkspaceFiles(url, flags, name, isSourcePath);
|
|
4876
|
-
for (const [rel, content] of there) incoming.push({ file: path.join(wsRoot, name, rel), content });
|
|
4884
|
+
for (const [rel, content] of there) incoming.push({ file: within(path.join(wsRoot, name), rel), content });
|
|
4877
4885
|
}
|
|
4878
4886
|
for (const f of await remoteLibrary(url, flags, LIBRARY_KINDS)) {
|
|
4879
|
-
|
|
4887
|
+
if (!LIBRARY_KINDS.includes(f.kind)) throw new Error(`the platform sent a library kind "${f.kind}" this CLI does not know — refusing to write it`);
|
|
4888
|
+
incoming.push({ file: within(path.join(root, "library", f.kind), f.path), content: f.content });
|
|
4880
4889
|
}
|
|
4881
4890
|
|
|
4882
4891
|
const clashes = incoming.filter((f) => {
|
|
@@ -9299,6 +9308,22 @@ async function runRmCmd(runId, flags, layout) {
|
|
|
9299
9308
|
return 0;
|
|
9300
9309
|
}
|
|
9301
9310
|
|
|
9311
|
+
const SAFE_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
9312
|
+
|
|
9313
|
+
/**
|
|
9314
|
+
* `rel` under `base`, or a refusal. Every path a server names is written
|
|
9315
|
+
* through this: a server (or anyone in between) must not be able to say
|
|
9316
|
+
* "../../.ssh/authorized_keys" and have pull write it.
|
|
9317
|
+
*/
|
|
9318
|
+
function within(base, rel) {
|
|
9319
|
+
const root = path.resolve(base);
|
|
9320
|
+
const to = path.resolve(root, String(rel));
|
|
9321
|
+
if (path.isAbsolute(String(rel)) || (to !== root && !to.startsWith(root + path.sep))) {
|
|
9322
|
+
throw new Error(`the platform sent a path outside ${root} ("${rel}") — refusing to write it`);
|
|
9323
|
+
}
|
|
9324
|
+
return to;
|
|
9325
|
+
}
|
|
9326
|
+
|
|
9302
9327
|
/** Where downloaded bytes go: --file (or - for stdout), else a default name. Refuses to clobber. */
|
|
9303
9328
|
function writeDownload(bytes, fallback, flags, from) {
|
|
9304
9329
|
if (flags.file === "-") {
|
|
@@ -9361,7 +9386,11 @@ async function remoteBytes(url, flags, apiPath, what) {
|
|
|
9361
9386
|
}
|
|
9362
9387
|
throw new HttpError(`${what}: ${why}${res.status === 401 || res.status === 403 ? refusedHint(url, flags) : ""}`, res.status, {});
|
|
9363
9388
|
}
|
|
9364
|
-
|
|
9389
|
+
// The server's name is a suggestion for a file in THIS folder: only its
|
|
9390
|
+
// last segment, and never a dot-file (".zshrc", "..").
|
|
9391
|
+
const said = /filename="([^"]+)"/.exec(res.headers.get("content-disposition") ?? "")?.[1];
|
|
9392
|
+
const base = said ? path.basename(said.replaceAll("\\", "/")) : undefined;
|
|
9393
|
+
const name = base && !base.startsWith(".") ? base : undefined;
|
|
9365
9394
|
return { bytes: Buffer.from(await res.arrayBuffer()), name };
|
|
9366
9395
|
}
|
|
9367
9396
|
|
package/src/credentials.mjs
CHANGED
|
@@ -93,6 +93,13 @@ function write(current, profiles) {
|
|
|
93
93
|
JSON.stringify({ current, default: current && profiles[current] ? normaliseUrl(profiles[current].url) : null, profiles, platforms }, null, 2) + "\n",
|
|
94
94
|
{ mode: 0o600 },
|
|
95
95
|
);
|
|
96
|
+
// `mode` applies only when the file is created; one made before (or by
|
|
97
|
+
// hand) keeps whatever it had. It holds tokens: owner-only, every write.
|
|
98
|
+
try {
|
|
99
|
+
fs.chmodSync(file(), 0o600);
|
|
100
|
+
} catch {
|
|
101
|
+
/* not ours to fix (Windows ACLs, a read-only home) */
|
|
102
|
+
}
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
/**
|