@loomcycle/client 1.125.0 → 1.127.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 +158 -0
- package/dist/cjs/client-tools.js +28 -5
- package/dist/cjs/client.js +239 -9
- package/dist/cjs/device-login.js +146 -0
- package/dist/cjs/errors.js +40 -1
- package/dist/cjs/fetch-helpers.js +31 -0
- package/dist/cjs/index.js +35 -1
- package/dist/cjs/oauth.js +400 -0
- package/dist/client-tools.d.ts +6 -2
- package/dist/client-tools.js +28 -5
- package/dist/client.d.ts +196 -3
- package/dist/client.js +240 -10
- package/dist/device-login.d.ts +12 -0
- package/dist/device-login.js +143 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +37 -0
- package/dist/fetch-helpers.d.ts +8 -0
- package/dist/fetch-helpers.js +30 -0
- package/dist/index.d.ts +31 -2
- package/dist/index.js +29 -1
- package/dist/oauth.d.ts +131 -0
- package/dist/oauth.js +393 -0
- package/dist/types.d.ts +512 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ TypeScript client for the [loomcycle](https://github.com/denn-gubsky/loomcycle)
|
|
|
12
12
|
|
|
13
13
|
### What's new since v0.8.18
|
|
14
14
|
|
|
15
|
+
- **Sign in with loomcycle (apps)** (RFC FL) — `LoomcycleOAuth` (also `client.oauth`) drives loomcycle's own OAuth sign-in for an app: `beginAuthorize` / `completeAuthorize` (PKCE S256, state and RFC 9207 `iss` checked), `refresh`, `revoke`, `discover`. A new `tokenProvider` client option (instead of `authToken`) takes short-lived bearers; `OAuthTokenProvider` refreshes before expiry and once after a 401. ClientDef methods register the apps: `createClientDef` / `updateClientDef` / `getClientDef` / `listClientDefs` / `retireClientDef` / `verifyClientDef` / `listClientDefNames`. See [Sign in with loomcycle (apps)](#sign-in-with-loomcycle-apps). Additive — an `authToken` client is unchanged.
|
|
15
16
|
- **Breaking — `tools` replaces `allowedTools`** — the per-run tool allowlist is now a single canonical key. The `runStreaming` / `continueSession` option is renamed from `allowedTools` to **`tools`**, and the `AgentDefOverlay` + library wire field is likewise **`tools`**, matching the loomcycle YAML rename and aligning with Claude Code agent frontmatter. No dual-key fallback — update call sites to `runStreaming({ …, tools: [...] })`. Requires a loomcycle server carrying the same rename.
|
|
16
17
|
- **Path/Document browse-by-subject + the full Document op set** (v1.12.1, RFC AS/AK) — `path(input, opts)` and `document(input, opts)` accept optional `scopeId` / `tenant` browse overrides, sent as `?scope_id=` / `?tenant=` query params (the server reads them from the URL and re-checks authorization — a tenant principal's `tenant` is ignored, `scopeId` picks any subject it may see); omit both to browse your own subject (byte-identical to the pre-RFC-AS request). `DocumentToolInput.op` now covers all 16 backend ops — adds **`set_path`** (attach/re-home a Path-tree name for an existing document), **`export_md`** (render to Markdown; `include_metadata: false` for clean human-facing output), and **`import_md`** (build a document from export_md-shaped `markdown`). Additive — existing `path()` / `document()` callers are unchanged.
|
|
17
18
|
- **`interactiveSession` / `sendRunInput` / `streamRunByID` + the `interactive` flag** (v1.1.1, RFC AI) — the interactive agentic session, the adapter port of the Web UI's run terminal. Pass `interactive: true` to `runStreaming` / `continueSession` to start a **persistent** run that parks at end_turn (an `awaiting_input` frame) instead of ending; **`sendRunInput(runId, text)`** steers it (the response arrives on the same stream); **`streamRunByID(runId, {fromSeq})`** re-attaches by run_id (the operator's prior turns replay as `steer` events, `user_input.source === "replay"`, so a cold client — e.g. another device — reconstructs the whole conversation). The high-level **`client.interactiveSession({agent, segments})`** returns an `InteractiveSession` with `events()` / `send()` / `cancel()`; **`attachInteractiveSession(runId)`** resumes one. The `AgentEvent` union gains `awaiting_input` / `steer` / `context_compaction`.
|
|
@@ -553,6 +554,163 @@ const why = await client.authzExplain({
|
|
|
553
554
|
console.log(why.decision.outcome, why.traces);
|
|
554
555
|
```
|
|
555
556
|
|
|
557
|
+
### Users and sign-in gates (RFC FA)
|
|
558
|
+
|
|
559
|
+
First-class users and how people sign in. A user's `status` (`UserStatus`) is `active`, `disabled` or `pending`: a sign-in gate configured with `new_user.status: pending` provisions people who can sign in but are refused everything until an operator approves them. `createUser` / `updateUser` accept only `active` and `disabled`; a pending user is activated by `approveUser` (a PATCH to `active` is a 409 `use_approve`). `whoami().auth.status` is `"pending"` for such a session.
|
|
560
|
+
|
|
561
|
+
The tenant is your own. `opts.tenant` names another for an admin token, and for an `auth:manage` credential (a website's), which must name one (400 `tenant_required`) and is held to the gate's new-user floor; a tenant operator always acts in its own tenant. Every call needs a persistent store.
|
|
562
|
+
|
|
563
|
+
| Method | Route | Returns |
|
|
564
|
+
|---|---|---|
|
|
565
|
+
| `createUser(body, opts?)` / `updateUser(subject, body, opts?)` / `deleteUser(subject, opts?)` | `POST /v1/_users` / `PATCH` / `DELETE /v1/_users/{subject}` | `UserRecord` / `UserRecord` / void |
|
|
566
|
+
| `approveUser(subject, opts?)` | `POST /v1/_users/{subject}/approve` | `UserRecord`, now `active`; 409 `not_pending` otherwise |
|
|
567
|
+
| `listUserIdentities(subject, opts?)` | `GET /v1/_users/{subject}/identities` | `UserIdentitiesResponse` — the identity-provider accounts a user signs in with; a user reads its own |
|
|
568
|
+
| `unlinkUserIdentity(subject, gate, opts?)` | `DELETE /v1/_users/{subject}/identities/{gate}` | void; for an operator — a user unlinks its own only from its browser session |
|
|
569
|
+
| `listAuthGates({probe?}?)` | `GET /v1/_authgate` | `AuthGateListResponse` — every configured gate, never key material. **Admin token only** |
|
|
570
|
+
| `createInvite(gate, body, opts?)` | `POST /v1/_authgate/{gate}/invites` | `CreatedInvite` — `{invite_id, url, expires_at}`; the URL is shown once |
|
|
571
|
+
| `listInvites(gate, opts?)` | `GET /v1/_authgate/{gate}/invites` | `AuthInviteListResponse` — `state` is `live` / `redeemed` / `revoked` / `expired`; never a code |
|
|
572
|
+
| `revokeInvite(gate, inviteId, opts?)` | `DELETE /v1/_authgate/{gate}/invites/{id}` | void; 409 `invite_not_live` |
|
|
573
|
+
| `createPasswordReset(gate, subject, opts?)` | `POST /v1/_authgate/{gate}/resets` | `CreatedPasswordReset` — a one-time link that sets a new password |
|
|
574
|
+
| `removeGateCredentials(gate, subject, opts?)` | `DELETE /v1/_authgate/{gate}/credentials/{subject}` | void — password, authenticator and recovery codes go; the login stays |
|
|
575
|
+
| `renameGateLogin(gate, subject, login, opts?)` | `PATCH /v1/_authgate/{gate}/logins/{subject}` | `{subject, login}`; 409 `login_taken` |
|
|
576
|
+
| `listGateIdentities(gate, subject, opts?)` | `GET /v1/_authgate/{gate}/identities?subject=` | `GateIdentitiesResponse` — logins, `has_password`, `has_totp` |
|
|
577
|
+
| `getGateTenantStatus(gate, opts?)` | `GET /v1/_authgate/{gate}/tenant-status` | `GateTenantStatus` — whether the tenant has, or ever had, a user, and whether an owner invite is live |
|
|
578
|
+
| `deviceLogin({onPrompt, clientId?, signal?})` | `POST /v1/_auth/device`, then `POST /v1/_auth/device/token` | `DeviceLoginResult` — signs a person in through the server's device gate (RFC 8628): `onPrompt` gets the code to show, the person approves it at `/ui/device`, and the token is theirs, never outliving the session they approved from. Sends no bearer. `DeviceLoginError` (`code` `access_denied` / `expired_token`) otherwise |
|
|
579
|
+
|
|
580
|
+
None of these takes a password: the person sets their own through the invite or reset link. Hand the link's `url` to the person and do not log it — its fragment is the one-time code.
|
|
581
|
+
|
|
582
|
+
Signing in, signing out and a person's own password or authenticator are not here. Those routes (`POST /ui/session`, `POST /v1/_auth/logout`, the password login, redeeming an invite or a reset, `/v1/_auth/account`, `/v1/_auth/password`, `/v1/_auth/totp`) establish or act on a browser's cookie session and accept only same-origin requests from loomcycle's own Web UI. A bearer has no session to end: retire a delegated user token with `revokeUserToken`, an operator token with `operatorTokenDef`.
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
const pending = (await client.listUsers({ tenant: "acme" })).users.filter((u) => u.status === "pending");
|
|
586
|
+
for (const u of pending) await client.approveUser(u.user_id, { tenant: "acme" });
|
|
587
|
+
|
|
588
|
+
const { url } = await client.createInvite("password", { email: "carol@example.com", ttl: "72h" }, { tenant: "acme" });
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Sign in with loomcycle (apps)
|
|
592
|
+
|
|
593
|
+
An app built on this client does not need a pasted token: it can send the person to loomcycle's own login page, where they sign in through whatever gates the operator configured, and get back a short-lived access token for that same user (RFC FL). It is the OAuth 2.0 authorization code flow with PKCE S256: the app holds no secret, never sees a password, and is authorized exactly as the user is anywhere else. The server side needs `LOOMCYCLE_PUBLIC_URL` and the app registered as a client — in the config's `auth.clients` or at runtime with `createClientDef` (below); see `docs/CONFIGURATION.md`, "App sign-in".
|
|
594
|
+
|
|
595
|
+
`LoomcycleOAuth` (also `client.oauth`) runs on Web Crypto and `fetch`, so the same code serves a browser SPA, a Chrome extension and a Tauri or Node app. On Node 18, which has no global `crypto`, pass `crypto: require("node:crypto").webcrypto`.
|
|
596
|
+
|
|
597
|
+
| Method | Route | Returns |
|
|
598
|
+
|---|---|---|
|
|
599
|
+
| `beginAuthorize({clientId, redirectUri, state?})` | — (builds `GET /v1/_oauth/authorize?…`) | `{url, verifier, state}` — persist `verifier` and `state`, then send the person to `url` |
|
|
600
|
+
| `completeAuthorize({...parseAuthorizeResponse(href), expectedState, verifier, clientId, redirectUri})` | `POST /v1/_oauth/token` | `OAuthTokenSet` — `{access_token, refresh_token, token_type, expires_in, expires_at}` (`expires_at` in ms since the epoch) |
|
|
601
|
+
| `refresh({refreshToken, clientId})` | `POST /v1/_oauth/token` | a new `OAuthTokenSet`; the old refresh token is spent |
|
|
602
|
+
| `revoke({token, clientId})` | `POST /v1/_oauth/revoke` | void — a refresh token ends the whole sign-in |
|
|
603
|
+
| `discover()` | `GET /.well-known/oauth-authorization-server` | `OAuthServerMetadata` (RFC 8414), cached |
|
|
604
|
+
|
|
605
|
+
`completeAuthorize` refuses, before the code is used, a redirect whose `state` is not the one this sign-in sent (`state_mismatch`) or whose `iss` names another server (`issuer_mismatch`, RFC 9207; the issuer comes from the `issuer` option or from `discover()`), and throws a redirect's `error` (`access_denied` with `pending` for a user awaiting approval). Every failure is an `OAuthError` whose `code` is the OAuth error code.
|
|
606
|
+
|
|
607
|
+
A browser SPA:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
import { LoomcycleClient, LoomcycleOAuth, OAuthTokenProvider, parseAuthorizeResponse } from "@loomcycle/client";
|
|
611
|
+
|
|
612
|
+
const baseUrl = "https://loom.example.com";
|
|
613
|
+
const clientId = "loomboard";
|
|
614
|
+
const redirectUri = `${location.origin}/auth/callback`; // registered exactly
|
|
615
|
+
const oauth = new LoomcycleOAuth({ baseUrl });
|
|
616
|
+
|
|
617
|
+
// "Sign in" button:
|
|
618
|
+
const req = await oauth.beginAuthorize({ clientId, redirectUri });
|
|
619
|
+
sessionStorage.setItem("lc_sign_in", JSON.stringify({ verifier: req.verifier, state: req.state }));
|
|
620
|
+
location.assign(req.url);
|
|
621
|
+
|
|
622
|
+
// On /auth/callback:
|
|
623
|
+
const saved = JSON.parse(sessionStorage.getItem("lc_sign_in") ?? "{}");
|
|
624
|
+
sessionStorage.removeItem("lc_sign_in");
|
|
625
|
+
const tokens = await oauth.completeAuthorize({
|
|
626
|
+
...parseAuthorizeResponse(location.href),
|
|
627
|
+
expectedState: saved.state,
|
|
628
|
+
verifier: saved.verifier,
|
|
629
|
+
clientId,
|
|
630
|
+
redirectUri,
|
|
631
|
+
});
|
|
632
|
+
history.replaceState(null, "", "/"); // the code is spent; drop it from the address bar
|
|
633
|
+
|
|
634
|
+
const provider = new OAuthTokenProvider({
|
|
635
|
+
oauth,
|
|
636
|
+
clientId,
|
|
637
|
+
tokens,
|
|
638
|
+
onTokens: (t) => saveTokens(t), // persist every rotated set
|
|
639
|
+
onSignedOut: () => showSignIn(),
|
|
640
|
+
});
|
|
641
|
+
const client = new LoomcycleClient({ baseUrl, tokenProvider: provider });
|
|
642
|
+
console.log((await client.whoami()).subject);
|
|
643
|
+
|
|
644
|
+
// "Sign out":
|
|
645
|
+
await provider.signOut();
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
A Chrome extension does the same with `chrome.identity.launchWebAuthFlow({url: req.url, interactive: true})`, which resolves with the redirect URL to hand to `parseAuthorizeResponse`; register `https://<extension-id>.chromiumapp.org/` as the redirect URI and `chrome-extension://<extension-id>` as an allowed origin.
|
|
649
|
+
|
|
650
|
+
A desktop app (Tauri, Electron, a CLI) uses a loopback redirect: register `http://127.0.0.1/callback` and the server accepts it on any port (RFC 8252). In Node:
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
import { createServer } from "node:http";
|
|
654
|
+
|
|
655
|
+
const server = createServer();
|
|
656
|
+
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));
|
|
657
|
+
const port = (server.address() as { port: number }).port;
|
|
658
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
659
|
+
|
|
660
|
+
const req = await oauth.beginAuthorize({ clientId: "loomboard-desktop", redirectUri });
|
|
661
|
+
openInSystemBrowser(req.url); // e.g. Tauri's shell.open, Electron's shell.openExternal
|
|
662
|
+
|
|
663
|
+
const href = await new Promise<string>((resolve) =>
|
|
664
|
+
server.on("request", (r, res) => {
|
|
665
|
+
res.end("Signed in. You can close this window.");
|
|
666
|
+
resolve(`http://127.0.0.1:${port}${r.url}`);
|
|
667
|
+
}),
|
|
668
|
+
);
|
|
669
|
+
server.close();
|
|
670
|
+
const tokens = await oauth.completeAuthorize({
|
|
671
|
+
...parseAuthorizeResponse(href),
|
|
672
|
+
expectedState: req.state,
|
|
673
|
+
verifier: req.verifier,
|
|
674
|
+
clientId: "loomboard-desktop",
|
|
675
|
+
redirectUri,
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
A Tauri app may register a private-use scheme instead (`loomboard://auth/callback`) and pass the deep link it receives to `parseAuthorizeResponse`. Token calls made from its webview send the origin `tauri://localhost`, which the client must list in `allowed_origins`; calls from the Rust or Node side send none.
|
|
680
|
+
|
|
681
|
+
**The token provider.** `new LoomcycleClient({ tokenProvider })` (instead of `authToken`; passing both throws) asks the provider for the bearer on every request, `connectClientTools` included. `OAuthTokenProvider` refreshes 60 s before the access token expires (`refreshSkewMs`), and once after a 401: the request is retried a single time with the new token, never more. Concurrent requests share one refresh. Any other source of short-lived tokens can implement `TokenProvider` (`getToken`, optional `onUnauthorized`).
|
|
682
|
+
|
|
683
|
+
Things to know:
|
|
684
|
+
|
|
685
|
+
- **Persist every rotated set.** A refresh spends the old refresh token, and presenting a spent one again ends the whole sign-in (reuse detection). `onTokens` is called before the new token is used.
|
|
686
|
+
- **One provider per sign-in.** Two providers, or two browser tabs, refreshing the same refresh token end it the same way. Share one, or sign each tab in separately.
|
|
687
|
+
- **Never log a token or put one in a URL.** The helper sends codes, verifiers and tokens only in POST bodies, with no cookie and no `Authorization` header; keep the `verifier` in session storage, not the URL, and clear it after the exchange.
|
|
688
|
+
- **An ended sign-in** (revoked, expired, its client retired) reaches `onSignedOut` once, when its refresh token is refused; requests then fail with an `OAuthError` (or the server's 401 as `AuthError`). Send the person to sign in again.
|
|
689
|
+
|
|
690
|
+
**Registering apps (ClientDef).** A tenant operator (`substrate:tenant`) registers its own tenant's apps; an admin any tenant's, or one for every tenant (`tenant: ""`, the default). Config clients are listed read only, and another tenant's client reads as not found. A refusal raises `SubstrateToolRefusedError`.
|
|
691
|
+
|
|
692
|
+
| Method | Route | Returns |
|
|
693
|
+
|---|---|---|
|
|
694
|
+
| `createClientDef(name, client, {description?, tenant?}?)` | `POST /v1/_clientdef` `op: create` | `ClientDefRecord` |
|
|
695
|
+
| `updateClientDef(name, client, {description?}?)` | `op: update` | `ClientDefRecord`, a new version; unexchanged codes stop working |
|
|
696
|
+
| `getClientDef({name} \| {def_id})` | `op: get` | `AppClientEntry` (`source: "def"` or `"config"`) |
|
|
697
|
+
| `listClientDefs({tenant?}?)` | `op: list` | `{clients: AppClientEntry[]}` |
|
|
698
|
+
| `retireClientDef(name)` | `op: retire` | `{name, retired}` — its sign-ins are revoked and their tokens retired |
|
|
699
|
+
| `verifyClientDef(name, {contentSha256?}?)` | `op: verify` | `ClientDefVerifyResult` |
|
|
700
|
+
| `listClientDefNames({tenant?}?)` | `GET /v1/_clientdef/names` | `{names: ClientDefNameSummary[]}` |
|
|
701
|
+
| `clientDef(input)` | `POST /v1/_clientdef` | the op dispatch, as sent |
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
await admin.createClientDef("loomboard", {
|
|
705
|
+
kind: "public",
|
|
706
|
+
label: "Loomboard",
|
|
707
|
+
redirect_uris: ["https://board.example.com/auth/callback", "http://127.0.0.1/callback"],
|
|
708
|
+
allowed_origins: ["https://board.example.com"],
|
|
709
|
+
access_token_ttl: "1h",
|
|
710
|
+
refresh: { idle: "7d", absolute: "30d" },
|
|
711
|
+
}, { tenant: "acme" });
|
|
712
|
+
```
|
|
713
|
+
|
|
556
714
|
### Decision models
|
|
557
715
|
|
|
558
716
|
Ask a **decision model** typed questions about a piece of text and get each answer with its probabilities. A decision model does not write: it reads the `state` you give it and answers a choice, a yes/no or a score, so there is no reply to parse and a call costs a few output tokens. Use it to route, gate, rank or grade text you already have. No run is involved; the tokens are charged to the caller and count against its token budget.
|
package/dist/cjs/client-tools.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.ClientToolHost = exports.CLIENT_TOOL_SUBPROTOCOL = void 0;
|
|
16
16
|
exports.clientToolsURL = clientToolsURL;
|
|
17
|
+
const errors_js_1 = require("./errors.js");
|
|
17
18
|
/** The app-level subprotocol the server negotiates for /v1/client-tools. */
|
|
18
19
|
exports.CLIENT_TOOL_SUBPROTOCOL = "loomcycle.client-tools.v1";
|
|
19
20
|
/**
|
|
@@ -24,13 +25,16 @@ exports.CLIENT_TOOL_SUBPROTOCOL = "loomcycle.client-tools.v1";
|
|
|
24
25
|
*/
|
|
25
26
|
class ClientToolHost {
|
|
26
27
|
url;
|
|
27
|
-
|
|
28
|
+
auth;
|
|
28
29
|
opts;
|
|
29
30
|
ws;
|
|
30
31
|
closed = false;
|
|
31
|
-
|
|
32
|
+
/** `auth` is the bearer, or a function that supplies one on each
|
|
33
|
+
* (re)connect — a client built with a token provider passes one, so a
|
|
34
|
+
* reconnect after the access token expired carries the refreshed one. */
|
|
35
|
+
constructor(url, auth, opts) {
|
|
32
36
|
this.url = url;
|
|
33
|
-
this.
|
|
37
|
+
this.auth = auth;
|
|
34
38
|
this.opts = opts;
|
|
35
39
|
}
|
|
36
40
|
/** Open the connection (called for you by connectClientTools). */
|
|
@@ -49,9 +53,28 @@ class ClientToolHost {
|
|
|
49
53
|
if (!WS) {
|
|
50
54
|
throw new Error("connectClientTools: no WebSocket implementation available — pass WebSocketImpl (e.g. the `ws` package) on Node < 22");
|
|
51
55
|
}
|
|
56
|
+
if (typeof this.auth === "function") {
|
|
57
|
+
this.auth().then((token) => this.open(WS, token), (err) => {
|
|
58
|
+
this.opts.onError?.(err);
|
|
59
|
+
// A sign-in that ended does not come back by retrying: stop
|
|
60
|
+
// rather than ask again every few seconds.
|
|
61
|
+
if (err instanceof errors_js_1.OAuthError && !err.retryable) {
|
|
62
|
+
this.closed = true;
|
|
63
|
+
this.opts.onStatus?.("closed");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.scheduleReconnect();
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.open(WS, this.auth);
|
|
71
|
+
}
|
|
72
|
+
open(WS, token) {
|
|
73
|
+
if (this.closed)
|
|
74
|
+
return;
|
|
52
75
|
const protocols = [exports.CLIENT_TOOL_SUBPROTOCOL];
|
|
53
|
-
if (
|
|
54
|
-
protocols.push("bearer." +
|
|
76
|
+
if (token)
|
|
77
|
+
protocols.push("bearer." + token);
|
|
55
78
|
this.opts.onStatus?.("connecting");
|
|
56
79
|
const ws = new WS(this.url, protocols);
|
|
57
80
|
this.ws = ws;
|
package/dist/cjs/client.js
CHANGED
|
@@ -29,6 +29,8 @@ exports.LoomcycleClient = void 0;
|
|
|
29
29
|
const fetch_helpers_js_1 = require("./fetch-helpers.js");
|
|
30
30
|
const stream_js_1 = require("./stream.js");
|
|
31
31
|
const interactive_js_1 = require("./interactive.js");
|
|
32
|
+
const oauth_js_1 = require("./oauth.js");
|
|
33
|
+
const device_login_js_1 = require("./device-login.js");
|
|
32
34
|
const client_tools_js_1 = require("./client-tools.js");
|
|
33
35
|
/** samplingToWire maps the camelCase SamplingOptions to the snake_case wire
|
|
34
36
|
* object, omitting unset fields so they inherit the agent's value. */
|
|
@@ -220,13 +222,30 @@ function applyOverridesToWire(body, opts) {
|
|
|
220
222
|
}
|
|
221
223
|
class LoomcycleClient {
|
|
222
224
|
ctx;
|
|
225
|
+
tokenProvider;
|
|
226
|
+
oauthHelper;
|
|
227
|
+
rawFetch;
|
|
223
228
|
constructor(opts = {}) {
|
|
229
|
+
if (opts.authToken && opts.tokenProvider) {
|
|
230
|
+
throw new TypeError("LoomcycleClient: pass authToken or tokenProvider, not both");
|
|
231
|
+
}
|
|
232
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
233
|
+
this.rawFetch = opts.fetch;
|
|
234
|
+
this.tokenProvider = opts.tokenProvider;
|
|
224
235
|
this.ctx = {
|
|
225
236
|
baseUrl: (opts.baseUrl ?? "http://127.0.0.1:8787").replace(/\/$/, ""),
|
|
226
237
|
authToken: opts.authToken,
|
|
227
|
-
|
|
238
|
+
// With a provider, ctx.authToken stays unset and the wrapper sets the
|
|
239
|
+
// header on every request, so no method needs to know.
|
|
240
|
+
fetchImpl: opts.tokenProvider ? (0, fetch_helpers_js_1.authorizedFetch)(fetchImpl, opts.tokenProvider) : fetchImpl,
|
|
228
241
|
};
|
|
229
242
|
}
|
|
243
|
+
/** The app sign-in helper (RFC FL) for this server: same base URL and
|
|
244
|
+
* fetch, never this client's bearer. See {@link LoomcycleOAuth}. */
|
|
245
|
+
get oauth() {
|
|
246
|
+
this.oauthHelper ??= new oauth_js_1.LoomcycleOAuth({ baseUrl: this.ctx.baseUrl, fetch: this.rawFetch });
|
|
247
|
+
return this.oauthHelper;
|
|
248
|
+
}
|
|
230
249
|
// ---- Run lifecycle ----
|
|
231
250
|
/**
|
|
232
251
|
* Run an agent and stream events. Returns AsyncIterable<AgentEvent>;
|
|
@@ -844,7 +863,8 @@ class LoomcycleClient {
|
|
|
844
863
|
return { agents: resp.agents ?? [], next_cursor: resp.next_cursor ?? "" };
|
|
845
864
|
}
|
|
846
865
|
/** Aggregated token-usage + cost report (RFC AV). Group by any of
|
|
847
|
-
* tenant/user/provider/model/source/purpose over
|
|
866
|
+
* tenant/user/provider/model/source/purpose/tier/user_tier/agent/class over
|
|
867
|
+
* an optional time window, split into periods with `bucket`;
|
|
848
868
|
* group by `source` for the operator-vs-tenant split, by `purpose` to see
|
|
849
869
|
* what OCR and speech-to-text cost (a row's `audio_seconds` is the audio its
|
|
850
870
|
* speech-to-text calls transcribed). Tenant-scoped server-side: a
|
|
@@ -860,6 +880,16 @@ class LoomcycleClient {
|
|
|
860
880
|
params.set("to", opts.to);
|
|
861
881
|
if (opts?.tenant)
|
|
862
882
|
params.set("tenant", opts.tenant);
|
|
883
|
+
if (opts?.bucket)
|
|
884
|
+
params.set("bucket", opts.bucket);
|
|
885
|
+
if (opts?.top)
|
|
886
|
+
params.set("top", String(opts.top));
|
|
887
|
+
if (opts?.topBy)
|
|
888
|
+
params.set("top_by", opts.topBy);
|
|
889
|
+
for (const [dim, values] of Object.entries(opts?.filters ?? {})) {
|
|
890
|
+
if (values && values.length > 0)
|
|
891
|
+
params.set(dim, values.join(","));
|
|
892
|
+
}
|
|
863
893
|
const q = params.toString() ? `?${params.toString()}` : "";
|
|
864
894
|
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_usage${q}`, opts);
|
|
865
895
|
}
|
|
@@ -984,28 +1014,151 @@ class LoomcycleClient {
|
|
|
984
1014
|
//
|
|
985
1015
|
// A tenant operator (substrate:tenant) manages the first-class users in its
|
|
986
1016
|
// OWN tenant and mints/lists/revokes bearer tokens for them. The tenant is
|
|
987
|
-
//
|
|
988
|
-
//
|
|
1017
|
+
// server-derived from the authenticated principal. `opts.tenant` (sent as
|
|
1018
|
+
// ?tenant=) names another only for an admin token, and for an `auth:manage`
|
|
1019
|
+
// credential, which must name one (400 `tenant_required` otherwise); a
|
|
1020
|
+
// tenant operator always acts in its own tenant. Requires a persistent store
|
|
989
1021
|
// (503 otherwise).
|
|
990
1022
|
/** Register a first-class user in the caller's own tenant. `access_mode`
|
|
991
1023
|
* defaults to "tenant" (collaborates on tenant-shared primitives);
|
|
992
1024
|
* "isolated" confines the member to its own user scope. 409 on a duplicate
|
|
993
1025
|
* (tenant, subject). */
|
|
994
1026
|
async createUser(body, opts) {
|
|
995
|
-
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_users", body,
|
|
1027
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath("/v1/_users", opts?.tenant), body, {
|
|
1028
|
+
signal: opts?.signal,
|
|
1029
|
+
});
|
|
996
1030
|
}
|
|
997
1031
|
/** Patch mutable fields on a user in the caller's own tenant. An omitted key
|
|
998
1032
|
* leaves the column unchanged. Disabling a user (status:"disabled") also
|
|
999
|
-
* revokes its delegated tokens server-side.
|
|
1033
|
+
* revokes its delegated tokens server-side. A pending user is activated by
|
|
1034
|
+
* {@link approveUser}, not here (409 `use_approve`). Cross-tenant target →
|
|
1035
|
+
* 404. */
|
|
1000
1036
|
async updateUser(subject, body, opts) {
|
|
1001
|
-
return (0, fetch_helpers_js_1.patchJSON)(this.ctx, `/v1/_users/${encodeURIComponent(subject)}`, body, opts);
|
|
1037
|
+
return (0, fetch_helpers_js_1.patchJSON)(this.ctx, this.aclPath(`/v1/_users/${encodeURIComponent(subject)}`, opts?.tenant), body, { signal: opts?.signal });
|
|
1002
1038
|
}
|
|
1003
1039
|
/** Delete the user identity row in the caller's own tenant (204). Its
|
|
1004
1040
|
* delegated tokens are retired first so the bearer cannot outlive the row.
|
|
1005
1041
|
* Owned data (runs / sessions / memory) is left intact — this is not
|
|
1006
1042
|
* erasure ({@link erasureExecute}). Cross-tenant target → 404. */
|
|
1007
1043
|
async deleteUser(subject, opts) {
|
|
1008
|
-
return (0, fetch_helpers_js_1.deleteRequest)(this.ctx, `/v1/_users/${encodeURIComponent(subject)}`, opts);
|
|
1044
|
+
return (0, fetch_helpers_js_1.deleteRequest)(this.ctx, this.aclPath(`/v1/_users/${encodeURIComponent(subject)}`, opts?.tenant), { signal: opts?.signal });
|
|
1045
|
+
}
|
|
1046
|
+
/** Approve a user awaiting approval: `pending` → `active`
|
|
1047
|
+
* (`POST /v1/_users/{subject}/approve`). A sign-in gate configured with
|
|
1048
|
+
* `new_user.status: pending` provisions people who can sign in but are
|
|
1049
|
+
* refused everything until this call. Returns the updated record. 409
|
|
1050
|
+
* `not_pending` for a user that is not pending; 404 for an unknown or
|
|
1051
|
+
* cross-tenant subject. An `auth:manage` credential may approve only a user
|
|
1052
|
+
* at its floor (403 `target_beyond_floor`). */
|
|
1053
|
+
async approveUser(subject, opts) {
|
|
1054
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, this.aclPath(`/v1/_users/${encodeURIComponent(subject)}/approve`, opts?.tenant), undefined, { signal: opts?.signal });
|
|
1055
|
+
}
|
|
1056
|
+
/** The identity-provider accounts a user signs in with
|
|
1057
|
+
* (`GET /v1/_users/{subject}/identities`): one row per linked gate, never a
|
|
1058
|
+
* secret. A user reads its own; a tenant operator or an admin reads any in
|
|
1059
|
+
* the tenant. Password-gate logins, with whether a password and an
|
|
1060
|
+
* authenticator are set, are {@link listGateIdentities}. */
|
|
1061
|
+
async listUserIdentities(subject, opts) {
|
|
1062
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath(`/v1/_users/${encodeURIComponent(subject)}/identities`, opts?.tenant), opts);
|
|
1063
|
+
}
|
|
1064
|
+
/** Unlink one identity-provider account from a user
|
|
1065
|
+
* (`DELETE /v1/_users/{subject}/identities/{gate}`, 204). Sessions made
|
|
1066
|
+
* through it end on their next request. For a tenant operator or an admin;
|
|
1067
|
+
* a user unlinking its own account must do it from a recently
|
|
1068
|
+
* signed-in Web UI session, so a bearer is refused (403
|
|
1069
|
+
* `link_needs_own_session`). A password gate's login is removed with
|
|
1070
|
+
* {@link removeGateCredentials} instead (409 `password_gate`). 404
|
|
1071
|
+
* `identity_not_found` when no account of that gate is linked. */
|
|
1072
|
+
async unlinkUserIdentity(subject, gate, opts) {
|
|
1073
|
+
return (0, fetch_helpers_js_1.deleteRequest)(this.ctx, this.aclPath(`/v1/_users/${encodeURIComponent(subject)}/identities/${encodeURIComponent(gate)}`, opts?.tenant), { signal: opts?.signal });
|
|
1074
|
+
}
|
|
1075
|
+
/** Sign in through the server's device gate (RFC 8628): shows the person a
|
|
1076
|
+
* code through `onPrompt`, polls until they approve it in a signed-in Web
|
|
1077
|
+
* UI session (`/ui/device`), and returns the token issued to them. The
|
|
1078
|
+
* token is the approver's own member token, at most the device gate's
|
|
1079
|
+
* `token_ttl` and never past the approving session or the credential it
|
|
1080
|
+
* was made from. Sends no bearer: a client constructed without one can
|
|
1081
|
+
* call it. Throws {@link DeviceLoginError} when the login is denied,
|
|
1082
|
+
* expires, or the server refuses it; 404 `device_login_disabled` when no
|
|
1083
|
+
* device gate is configured. */
|
|
1084
|
+
async deviceLogin(opts) {
|
|
1085
|
+
return (0, device_login_js_1.deviceLogin)(this.ctx, opts);
|
|
1086
|
+
}
|
|
1087
|
+
// ---- RFC FA: sign-in gates + the password gate's management API ----
|
|
1088
|
+
//
|
|
1089
|
+
// Invitations, reset links, credential removal, login renames and a user's
|
|
1090
|
+
// logins at a password gate, addressed by the gate's configured name. None
|
|
1091
|
+
// takes a password: the person sets their own through the link. A tenant
|
|
1092
|
+
// operator manages its own tenant, an admin any (`opts.tenant`, defaulting
|
|
1093
|
+
// to its own), and an `auth:manage` credential any tenant it names — it
|
|
1094
|
+
// must name one (400 `tenant_required`) and is held to the gate's new-user
|
|
1095
|
+
// floor. A gate that is not a password gate is a 404 `gate_not_found`.
|
|
1096
|
+
// Requires a persistent store (503 otherwise).
|
|
1097
|
+
/** Every configured sign-in gate (`GET /v1/_authgate`): kind, label,
|
|
1098
|
+
* whether a browser can sign in through it, its identity map and an
|
|
1099
|
+
* external gate's key fingerprint and removal timing — never key
|
|
1100
|
+
* material. **Admin token only.** `probe: true` also fetches each
|
|
1101
|
+
* external gate's discovery document (bounded) and compares it with the
|
|
1102
|
+
* registration. */
|
|
1103
|
+
async listAuthGates(opts) {
|
|
1104
|
+
const q = opts?.probe ? "?probe=1" : "";
|
|
1105
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_authgate${q}`, opts);
|
|
1106
|
+
}
|
|
1107
|
+
/** Invite a person to sign in through a password gate
|
|
1108
|
+
* (`POST /v1/_authgate/{gate}/invites`). Without `subject` the invite
|
|
1109
|
+
* makes a new user, at the gate's new-user floor unless the gate is
|
|
1110
|
+
* trusted; with one it gives that existing user a login (409
|
|
1111
|
+
* `credential_exists` if it has one — issue a reset instead). Returns the
|
|
1112
|
+
* one-time link, shown once. */
|
|
1113
|
+
async createInvite(gate, body, opts) {
|
|
1114
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_authgate/${encodeURIComponent(gate)}/invites`, withTenant(body, opts?.tenant), { signal: opts?.signal });
|
|
1115
|
+
}
|
|
1116
|
+
/** A password gate's invites in the tenant, live and past
|
|
1117
|
+
* (`GET /v1/_authgate/{gate}/invites`), never their codes. An admin naming
|
|
1118
|
+
* no tenant lists every tenant's. */
|
|
1119
|
+
async listInvites(gate, opts) {
|
|
1120
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath(`/v1/_authgate/${encodeURIComponent(gate)}/invites`, opts?.tenant), opts);
|
|
1121
|
+
}
|
|
1122
|
+
/** Revoke a live invite (`DELETE /v1/_authgate/{gate}/invites/{id}`, 204).
|
|
1123
|
+
* 409 `invite_not_live` when it was already redeemed, revoked or expired;
|
|
1124
|
+
* another tenant's invite is a 404. */
|
|
1125
|
+
async revokeInvite(gate, inviteId, opts) {
|
|
1126
|
+
return (0, fetch_helpers_js_1.deleteRequest)(this.ctx, this.aclPath(`/v1/_authgate/${encodeURIComponent(gate)}/invites/${encodeURIComponent(inviteId)}`, opts?.tenant), { signal: opts?.signal });
|
|
1127
|
+
}
|
|
1128
|
+
/** Issue a one-time link that sets a new password for a user with a login
|
|
1129
|
+
* at the gate (`POST /v1/_authgate/{gate}/resets`). A user with an
|
|
1130
|
+
* authenticator still needs a code from it. Completing the reset ends the
|
|
1131
|
+
* user's sessions from the old password and retires its delegated tokens.
|
|
1132
|
+
* 404 `login_not_found` when the user has no login at the gate. */
|
|
1133
|
+
async createPasswordReset(gate, subject, opts) {
|
|
1134
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_authgate/${encodeURIComponent(gate)}/resets`, withTenant({ subject }, opts?.tenant), { signal: opts?.signal });
|
|
1135
|
+
}
|
|
1136
|
+
/** Remove a user's password, authenticator and recovery codes at a gate
|
|
1137
|
+
* (`DELETE /v1/_authgate/{gate}/credentials/{subject}`, 204). The login
|
|
1138
|
+
* stays reserved, so a later {@link createPasswordReset} restores access. */
|
|
1139
|
+
async removeGateCredentials(gate, subject, opts) {
|
|
1140
|
+
return (0, fetch_helpers_js_1.deleteRequest)(this.ctx, this.aclPath(`/v1/_authgate/${encodeURIComponent(gate)}/credentials/${encodeURIComponent(subject)}`, opts?.tenant), { signal: opts?.signal });
|
|
1141
|
+
}
|
|
1142
|
+
/** Change the login a user signs in with at a gate
|
|
1143
|
+
* (`PATCH /v1/_authgate/{gate}/logins/{subject}`). The subject does not
|
|
1144
|
+
* change, so nothing the user owns moves. Returns the normalised login;
|
|
1145
|
+
* 409 `login_taken` when another user has it. */
|
|
1146
|
+
async renameGateLogin(gate, subject, login, opts) {
|
|
1147
|
+
return (0, fetch_helpers_js_1.patchJSON)(this.ctx, `/v1/_authgate/${encodeURIComponent(gate)}/logins/${encodeURIComponent(subject)}`, withTenant({ login }, opts?.tenant), { signal: opts?.signal });
|
|
1148
|
+
}
|
|
1149
|
+
/** A user's logins at every gate, and whether it has a password and a
|
|
1150
|
+
* confirmed authenticator (`GET /v1/_authgate/{gate}/identities`) — never
|
|
1151
|
+
* a secret. `gate` must name a password gate. */
|
|
1152
|
+
async listGateIdentities(gate, subject, opts) {
|
|
1153
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath(`/v1/_authgate/${encodeURIComponent(gate)}/identities`, opts?.tenant, {
|
|
1154
|
+
subject,
|
|
1155
|
+
}), opts);
|
|
1156
|
+
}
|
|
1157
|
+
/** Whether the tenant has, or ever had, a user and whether an owner invite
|
|
1158
|
+
* is live (`GET /v1/_authgate/{gate}/tenant-status`) — what deciding on an
|
|
1159
|
+
* owner invite needs, and nothing about who the users are. */
|
|
1160
|
+
async getGateTenantStatus(gate, opts) {
|
|
1161
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, this.aclPath(`/v1/_authgate/${encodeURIComponent(gate)}/tenant-status`, opts?.tenant), opts);
|
|
1009
1162
|
}
|
|
1010
1163
|
/** Mint a bearer token for one of the caller's own ACTIVE users. The granted
|
|
1011
1164
|
* scopes are DERIVED server-side from the member's access_mode (never sent),
|
|
@@ -1199,7 +1352,11 @@ class LoomcycleClient {
|
|
|
1199
1352
|
* `.close()` to stop. Uses the global WebSocket (browsers, Node 22+); pass
|
|
1200
1353
|
* `WebSocketImpl` (e.g. the `ws` package) on older Node. */
|
|
1201
1354
|
connectClientTools(opts) {
|
|
1202
|
-
|
|
1355
|
+
// A provider is asked on every (re)connect, so a host outliving one
|
|
1356
|
+
// access token reconnects with the next.
|
|
1357
|
+
const provider = this.tokenProvider;
|
|
1358
|
+
const auth = provider ? () => provider.getToken() : this.ctx.authToken;
|
|
1359
|
+
const host = new client_tools_js_1.ClientToolHost((0, client_tools_js_1.clientToolsURL)(this.ctx.baseUrl), auth, opts);
|
|
1203
1360
|
host.start();
|
|
1204
1361
|
return host;
|
|
1205
1362
|
}
|
|
@@ -1836,6 +1993,73 @@ class LoomcycleClient {
|
|
|
1836
1993
|
async volumeDef(input, opts) {
|
|
1837
1994
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_volumedef", input, opts);
|
|
1838
1995
|
}
|
|
1996
|
+
// ---- App clients (RFC FL ClientDef) ----
|
|
1997
|
+
//
|
|
1998
|
+
// The apps that sign people in through this server's gates, registered at
|
|
1999
|
+
// runtime beside the config's `auth.clients`. A tenant operator
|
|
2000
|
+
// (substrate:tenant) registers its own tenant's; an admin any tenant's, or
|
|
2001
|
+
// one for every tenant. Config clients are read only here, and another
|
|
2002
|
+
// tenant's client answers as not found. A refusal raises
|
|
2003
|
+
// SubstrateToolRefusedError. Each method posts to `POST /v1/_clientdef`.
|
|
2004
|
+
/** The ClientDef op dispatch, as sent: `{op, name?, def_id?, client?,
|
|
2005
|
+
* description?, tenant?, content_sha256?}`. The typed methods below are
|
|
2006
|
+
* the usual way in. */
|
|
2007
|
+
async clientDef(input, opts) {
|
|
2008
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_clientdef", input, opts);
|
|
2009
|
+
}
|
|
2010
|
+
/** Register an app (`op: "create"`). Its redirect URIs and origins are
|
|
2011
|
+
* checked as strictly as a config client's. `tenant` is for an admin: the
|
|
2012
|
+
* tenant whose people it signs in, "" (the default) for every tenant. */
|
|
2013
|
+
async createClientDef(name, client, opts) {
|
|
2014
|
+
const input = { op: "create", name, client };
|
|
2015
|
+
if (opts?.description !== undefined)
|
|
2016
|
+
input.description = opts.description;
|
|
2017
|
+
if (opts?.tenant !== undefined)
|
|
2018
|
+
input.tenant = opts.tenant;
|
|
2019
|
+
return (await this.clientDef(input, signalOpts(opts)));
|
|
2020
|
+
}
|
|
2021
|
+
/** Change an app (`op: "update"`): a new version, whole. Codes issued
|
|
2022
|
+
* under the old version that were not exchanged yet stop working. */
|
|
2023
|
+
async updateClientDef(name, client, opts) {
|
|
2024
|
+
const input = { op: "update", name, client };
|
|
2025
|
+
if (opts?.description !== undefined)
|
|
2026
|
+
input.description = opts.description;
|
|
2027
|
+
return (await this.clientDef(input, signalOpts(opts)));
|
|
2028
|
+
}
|
|
2029
|
+
/** One app by name (a config client too) or one version by `def_id`
|
|
2030
|
+
* (`op: "get"`). */
|
|
2031
|
+
async getClientDef(ref, opts) {
|
|
2032
|
+
return (await this.clientDef({ op: "get", ...ref }, opts));
|
|
2033
|
+
}
|
|
2034
|
+
/** The apps the caller may see, config ones included (`op: "list"`). An
|
|
2035
|
+
* admin may focus one tenant with `tenant` ("" = the every-tenant ones). */
|
|
2036
|
+
async listClientDefs(opts) {
|
|
2037
|
+
const input = { op: "list" };
|
|
2038
|
+
if (opts?.tenant !== undefined)
|
|
2039
|
+
input.tenant = opts.tenant;
|
|
2040
|
+
return (await this.clientDef(input, signalOpts(opts)));
|
|
2041
|
+
}
|
|
2042
|
+
/** Take an app out of service (`op: "retire"`): its unused codes are
|
|
2043
|
+
* voided, its sign-ins revoked and their access tokens retired. The name
|
|
2044
|
+
* is not reusable. */
|
|
2045
|
+
async retireClientDef(name, opts) {
|
|
2046
|
+
return (await this.clientDef({ op: "retire", name }, opts));
|
|
2047
|
+
}
|
|
2048
|
+
/** Whether an app is deployed and, with `contentSha256`, whether it matches
|
|
2049
|
+
* (`op: "verify"`). An absent app answers `deployed: false`. */
|
|
2050
|
+
async verifyClientDef(name, opts) {
|
|
2051
|
+
const input = { op: "verify", name };
|
|
2052
|
+
if (opts?.contentSha256)
|
|
2053
|
+
input.content_sha256 = opts.contentSha256;
|
|
2054
|
+
return (await this.clientDef(input, signalOpts(opts)));
|
|
2055
|
+
}
|
|
2056
|
+
/** The apps the caller may see, by name (`GET /v1/_clientdef/names`). An
|
|
2057
|
+
* admin may focus one tenant with `tenant`; unlike the other families'
|
|
2058
|
+
* name lists, `""` is a focus too (the every-tenant apps). */
|
|
2059
|
+
async listClientDefNames(opts) {
|
|
2060
|
+
const q = opts?.tenant !== undefined ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
|
|
2061
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_clientdef/names${q}`, opts);
|
|
2062
|
+
}
|
|
1839
2063
|
// ---- Def name enumeration (GET /v1/_<family>def/names) ----
|
|
1840
2064
|
//
|
|
1841
2065
|
// Every name declared in one substrate family, each with its version
|
|
@@ -3351,6 +3575,12 @@ function defWriteInput(op, name, overlay, opts) {
|
|
|
3351
3575
|
input.promote = opts.promote;
|
|
3352
3576
|
return input;
|
|
3353
3577
|
}
|
|
3578
|
+
/** A password-gate management body with the tenant the caller named. The
|
|
3579
|
+
* POST/PATCH routes read the tenant from the body where the others read
|
|
3580
|
+
* ?tenant=; like aclPath, an empty tenant is left out. */
|
|
3581
|
+
function withTenant(body, tenant) {
|
|
3582
|
+
return tenant ? { ...body, tenant } : body;
|
|
3583
|
+
}
|
|
3354
3584
|
function signalOpts(opts) {
|
|
3355
3585
|
return opts?.signal ? { signal: opts.signal } : undefined;
|
|
3356
3586
|
}
|