@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal — reached through LoomcycleClient.deviceLogin().
|
|
3
|
+
*
|
|
4
|
+
* The client side of loomcycle's device login (RFC 8628, a `kind: device`
|
|
5
|
+
* sign-in gate): ask the server for a code pair, show the person the short
|
|
6
|
+
* code, poll until they approve it in a signed-in Web UI session, and return
|
|
7
|
+
* the token issued to them. Both requests are unauthenticated — the device
|
|
8
|
+
* code is the credential — so no bearer the client holds is sent with them.
|
|
9
|
+
*/
|
|
10
|
+
import { DeviceLoginError } from "./errors.js";
|
|
11
|
+
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
12
|
+
/** RFC 8628 §3.5: a slow_down lengthens this and every later poll by 5 s. */
|
|
13
|
+
const SLOW_DOWN_STEP_MS = 5_000;
|
|
14
|
+
const DEFAULT_INTERVAL_S = 5;
|
|
15
|
+
/** The server issues codes for ten minutes; a longer claim is not trusted. */
|
|
16
|
+
const DEFAULT_EXPIRES_S = 600;
|
|
17
|
+
const MAX_EXPIRES_S = 3_600;
|
|
18
|
+
async function postForm(ctx, path, form, signal) {
|
|
19
|
+
const res = await ctx.fetchImpl(`${ctx.baseUrl}${path}`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
|
|
22
|
+
body: new URLSearchParams(form).toString(),
|
|
23
|
+
signal,
|
|
24
|
+
});
|
|
25
|
+
const text = await res.text();
|
|
26
|
+
let body = {};
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(text);
|
|
29
|
+
if (parsed && typeof parsed === "object")
|
|
30
|
+
body = parsed;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// A non-JSON answer is reported by status below.
|
|
34
|
+
}
|
|
35
|
+
return { status: res.status, body, text };
|
|
36
|
+
}
|
|
37
|
+
function sleep(ms, signal) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
if (signal?.aborted) {
|
|
40
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const t = setTimeout(() => {
|
|
44
|
+
signal?.removeEventListener("abort", onAbort);
|
|
45
|
+
resolve();
|
|
46
|
+
}, ms);
|
|
47
|
+
const onAbort = () => {
|
|
48
|
+
clearTimeout(t);
|
|
49
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
50
|
+
};
|
|
51
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** A string shown to a person must not carry control characters a hostile
|
|
55
|
+
* server could use to rewrite what they see. */
|
|
56
|
+
function printable(s, max) {
|
|
57
|
+
return typeof s === "string" && s.length > 0 && s.length <= max && /^[\x20-\x7e]+$/.test(s);
|
|
58
|
+
}
|
|
59
|
+
function httpURL(s) {
|
|
60
|
+
if (!printable(s, 2048))
|
|
61
|
+
return false;
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(s);
|
|
64
|
+
return u.protocol === "https:" || u.protocol === "http:";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function positiveInt(v) {
|
|
71
|
+
return typeof v === "number" && Number.isInteger(v) && v > 0 ? v : undefined;
|
|
72
|
+
}
|
|
73
|
+
export async function deviceLogin(ctx, opts) {
|
|
74
|
+
const clientId = opts.clientId ?? "loomcycle-ts";
|
|
75
|
+
const start = await postForm(ctx, "/v1/_auth/device", { client_id: clientId }, opts.signal);
|
|
76
|
+
if (start.status !== 200) {
|
|
77
|
+
const code = typeof start.body.code === "string" ? start.body.code : undefined;
|
|
78
|
+
throw new DeviceLoginError(`the server refused to start a device login (HTTP ${start.status})`, {
|
|
79
|
+
status: start.status,
|
|
80
|
+
bodyText: start.text.slice(0, 1024),
|
|
81
|
+
code,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const da = start.body;
|
|
85
|
+
if (typeof da.device_code !== "string" || da.device_code === "" ||
|
|
86
|
+
!printable(da.user_code, 32) || !httpURL(da.verification_uri)) {
|
|
87
|
+
throw new DeviceLoginError("the server's device-login answer is malformed", { status: start.status });
|
|
88
|
+
}
|
|
89
|
+
const deviceCode = da.device_code;
|
|
90
|
+
let expiresIn = positiveInt(da.expires_in) ?? DEFAULT_EXPIRES_S;
|
|
91
|
+
if (expiresIn > MAX_EXPIRES_S)
|
|
92
|
+
expiresIn = DEFAULT_EXPIRES_S;
|
|
93
|
+
await opts.onPrompt({
|
|
94
|
+
userCode: da.user_code,
|
|
95
|
+
verificationUri: da.verification_uri,
|
|
96
|
+
verificationUriComplete: httpURL(da.verification_uri_complete) ? da.verification_uri_complete : undefined,
|
|
97
|
+
expiresIn,
|
|
98
|
+
});
|
|
99
|
+
let intervalMs = (positiveInt(da.interval) ?? DEFAULT_INTERVAL_S) * 1000;
|
|
100
|
+
// A little past the code's own expiry, so the server's expired_token is
|
|
101
|
+
// what ends a login nobody approved.
|
|
102
|
+
const deadline = Date.now() + expiresIn * 1000 + 30_000;
|
|
103
|
+
for (;;) {
|
|
104
|
+
await sleep(intervalMs, opts.signal);
|
|
105
|
+
if (Date.now() > deadline) {
|
|
106
|
+
throw new DeviceLoginError("the code expired before it was approved", { code: "expired_token" });
|
|
107
|
+
}
|
|
108
|
+
const res = await postForm(ctx, "/v1/_auth/device/token", { grant_type: GRANT_TYPE, device_code: deviceCode, client_id: clientId }, opts.signal);
|
|
109
|
+
const tok = res.body;
|
|
110
|
+
if (res.status === 200 && typeof tok.access_token === "string" && tok.access_token !== "") {
|
|
111
|
+
return {
|
|
112
|
+
accessToken: tok.access_token,
|
|
113
|
+
tokenType: typeof tok.token_type === "string" ? tok.token_type : "Bearer",
|
|
114
|
+
expiresIn: positiveInt(tok.expires_in),
|
|
115
|
+
scope: typeof tok.scope === "string" && tok.scope !== "" ? tok.scope.split(" ") : undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
switch (tok.error) {
|
|
119
|
+
case "authorization_pending":
|
|
120
|
+
continue;
|
|
121
|
+
case "slow_down":
|
|
122
|
+
intervalMs += SLOW_DOWN_STEP_MS;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// RFC 8628 refusals carry the code in `error`; loomcycle's own refusals
|
|
126
|
+
// (device_login_disabled, …) carry it in `code` and a message in `error`.
|
|
127
|
+
const code = typeof res.body.code === "string" ? res.body.code : typeof tok.error === "string" ? tok.error : undefined;
|
|
128
|
+
const why = code === "access_denied"
|
|
129
|
+
? "the login was denied, or its approval no longer holds"
|
|
130
|
+
: code === "expired_token"
|
|
131
|
+
? "the code expired before it was approved"
|
|
132
|
+
: typeof tok.error_description === "string"
|
|
133
|
+
? tok.error_description
|
|
134
|
+
: `the server refused the login (HTTP ${res.status})`;
|
|
135
|
+
// A refusal's body carries no token. A 200 without a usable one is
|
|
136
|
+
// malformed, and its body is not echoed, in case it holds one anyway.
|
|
137
|
+
throw new DeviceLoginError(why, {
|
|
138
|
+
status: res.status,
|
|
139
|
+
bodyText: res.status === 200 ? undefined : res.text.slice(0, 1024),
|
|
140
|
+
code,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -423,3 +423,39 @@ export declare class SubstrateToolRefusedError extends LoomcycleError {
|
|
|
423
423
|
tool?: string;
|
|
424
424
|
});
|
|
425
425
|
}
|
|
426
|
+
/** An app sign-in (RFC FL) failed. `code` is the OAuth error code: one the
|
|
427
|
+
* server sent (`invalid_grant`, `invalid_client`, `access_denied`,
|
|
428
|
+
* `temporarily_unavailable`, …, RFC 6749 §4.1.2.1 / §5.2), or one the client
|
|
429
|
+
* decided on before trusting a response:
|
|
430
|
+
* - `state_mismatch`: the redirect's `state` is not the one this sign-in
|
|
431
|
+
* sent — a forged or crossed response; nothing was exchanged.
|
|
432
|
+
* - `issuer_mismatch`: the redirect's `iss` names another server (RFC 9207).
|
|
433
|
+
* - `invalid_response`: the server's answer is not a token response.
|
|
434
|
+
* - `signed_out`: the sign-in ended (its refresh token was refused); sign in
|
|
435
|
+
* again.
|
|
436
|
+
* `invalid_grant` on a refresh means the same: the sign-in is over. */
|
|
437
|
+
export declare class OAuthError extends LoomcycleError {
|
|
438
|
+
/** The server's `error_description`, when it sent one. */
|
|
439
|
+
readonly errorDescription?: string;
|
|
440
|
+
constructor(code: string, opts?: {
|
|
441
|
+
status?: number;
|
|
442
|
+
bodyText?: string;
|
|
443
|
+
errorDescription?: string;
|
|
444
|
+
});
|
|
445
|
+
/** `temporarily_unavailable` (and a 429 throttle) clear on their own; every
|
|
446
|
+
* other sign-in failure needs the person or the app to act. */
|
|
447
|
+
get retryable(): boolean;
|
|
448
|
+
}
|
|
449
|
+
/** A device login ({@link LoomcycleClient.deviceLogin}) that ended without a
|
|
450
|
+
* token. `code` is the server's RFC 8628 error: `access_denied` (the person
|
|
451
|
+
* denied it, or the approval no longer holds — the approving session ended
|
|
452
|
+
* or its credential was retired), `expired_token` (nobody approved it in
|
|
453
|
+
* time), or another refusal. Start a new login; the same one never
|
|
454
|
+
* succeeds. */
|
|
455
|
+
export declare class DeviceLoginError extends LoomcycleError {
|
|
456
|
+
constructor(message: string, opts?: {
|
|
457
|
+
status?: number;
|
|
458
|
+
bodyText?: string;
|
|
459
|
+
code?: string;
|
|
460
|
+
});
|
|
461
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -427,3 +427,40 @@ export class SubstrateToolRefusedError extends LoomcycleError {
|
|
|
427
427
|
this.tool = opts?.tool ?? "";
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
|
+
/** An app sign-in (RFC FL) failed. `code` is the OAuth error code: one the
|
|
431
|
+
* server sent (`invalid_grant`, `invalid_client`, `access_denied`,
|
|
432
|
+
* `temporarily_unavailable`, …, RFC 6749 §4.1.2.1 / §5.2), or one the client
|
|
433
|
+
* decided on before trusting a response:
|
|
434
|
+
* - `state_mismatch`: the redirect's `state` is not the one this sign-in
|
|
435
|
+
* sent — a forged or crossed response; nothing was exchanged.
|
|
436
|
+
* - `issuer_mismatch`: the redirect's `iss` names another server (RFC 9207).
|
|
437
|
+
* - `invalid_response`: the server's answer is not a token response.
|
|
438
|
+
* - `signed_out`: the sign-in ended (its refresh token was refused); sign in
|
|
439
|
+
* again.
|
|
440
|
+
* `invalid_grant` on a refresh means the same: the sign-in is over. */
|
|
441
|
+
export class OAuthError extends LoomcycleError {
|
|
442
|
+
/** The server's `error_description`, when it sent one. */
|
|
443
|
+
errorDescription;
|
|
444
|
+
constructor(code, opts) {
|
|
445
|
+
super(opts?.errorDescription ? `loomcycle sign-in: ${code}: ${opts.errorDescription}` : `loomcycle sign-in: ${code}`, { status: opts?.status, bodyText: opts?.bodyText, code });
|
|
446
|
+
this.name = "OAuthError";
|
|
447
|
+
this.errorDescription = opts?.errorDescription;
|
|
448
|
+
}
|
|
449
|
+
/** `temporarily_unavailable` (and a 429 throttle) clear on their own; every
|
|
450
|
+
* other sign-in failure needs the person or the app to act. */
|
|
451
|
+
get retryable() {
|
|
452
|
+
return this.code === "temporarily_unavailable";
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/** A device login ({@link LoomcycleClient.deviceLogin}) that ended without a
|
|
456
|
+
* token. `code` is the server's RFC 8628 error: `access_denied` (the person
|
|
457
|
+
* denied it, or the approval no longer holds — the approving session ended
|
|
458
|
+
* or its credential was retired), `expired_token` (nobody approved it in
|
|
459
|
+
* time), or another refusal. Start a new login; the same one never
|
|
460
|
+
* succeeds. */
|
|
461
|
+
export class DeviceLoginError extends LoomcycleError {
|
|
462
|
+
constructor(message, opts) {
|
|
463
|
+
super(message, opts);
|
|
464
|
+
this.name = "DeviceLoginError";
|
|
465
|
+
}
|
|
466
|
+
}
|
package/dist/fetch-helpers.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Method-level code in client.ts stays focused on URL + body shape;
|
|
10
10
|
* the boring fetch + error-translation machinery lives here.
|
|
11
11
|
*/
|
|
12
|
+
import type { TokenProvider } from "./types.js";
|
|
12
13
|
/**
|
|
13
14
|
* @internal — not part of @loomcycle/client's public API surface.
|
|
14
15
|
*
|
|
@@ -28,6 +29,13 @@ export interface _FetchContext {
|
|
|
28
29
|
authToken: string | undefined;
|
|
29
30
|
fetchImpl: typeof fetch;
|
|
30
31
|
}
|
|
32
|
+
/** authorizedFetch wraps a fetch so every request carries the provider's
|
|
33
|
+
* current bearer (replacing whatever Authorization the caller set), and a
|
|
34
|
+
* 401 is retried once with the token `onUnauthorized` hands back — never
|
|
35
|
+
* twice, so a token the server keeps refusing cannot loop. A request whose
|
|
36
|
+
* body is a stream is not retried: the stream was consumed by the first
|
|
37
|
+
* send. */
|
|
38
|
+
export declare function authorizedFetch(inner: typeof fetch, provider: TokenProvider): typeof fetch;
|
|
31
39
|
/** authHeaders builds the standard request header set: JSON Accept
|
|
32
40
|
* + Bearer token when the client was constructed with one. The
|
|
33
41
|
* caller adds Content-Type when posting a body. */
|
package/dist/fetch-helpers.js
CHANGED
|
@@ -10,6 +10,36 @@
|
|
|
10
10
|
* the boring fetch + error-translation machinery lives here.
|
|
11
11
|
*/
|
|
12
12
|
import { AclDeniedError, AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, ChannelCursorRegressionError, InsufficientScopeError, InvalidArgumentError, LoomcycleError, NotFoundError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
|
|
13
|
+
/** authorizedFetch wraps a fetch so every request carries the provider's
|
|
14
|
+
* current bearer (replacing whatever Authorization the caller set), and a
|
|
15
|
+
* 401 is retried once with the token `onUnauthorized` hands back — never
|
|
16
|
+
* twice, so a token the server keeps refusing cannot loop. A request whose
|
|
17
|
+
* body is a stream is not retried: the stream was consumed by the first
|
|
18
|
+
* send. */
|
|
19
|
+
export function authorizedFetch(inner, provider) {
|
|
20
|
+
return async (input, init) => {
|
|
21
|
+
const token = await provider.getToken();
|
|
22
|
+
const first = await inner(input, withBearer(init, token));
|
|
23
|
+
if (first.status !== 401 || !provider.onUnauthorized || !replayableBody(init?.body))
|
|
24
|
+
return first;
|
|
25
|
+
const fresh = await provider.onUnauthorized(token);
|
|
26
|
+
if (!fresh || fresh === token)
|
|
27
|
+
return first;
|
|
28
|
+
await first.body?.cancel().catch(() => undefined);
|
|
29
|
+
return inner(input, withBearer(init, fresh));
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function withBearer(init, token) {
|
|
33
|
+
const headers = new Headers(init?.headers);
|
|
34
|
+
if (token)
|
|
35
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
36
|
+
else
|
|
37
|
+
headers.delete("Authorization");
|
|
38
|
+
return { ...init, headers };
|
|
39
|
+
}
|
|
40
|
+
function replayableBody(body) {
|
|
41
|
+
return !(typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
|
|
42
|
+
}
|
|
13
43
|
/** authHeaders builds the standard request header set: JSON Accept
|
|
14
44
|
* + Bearer token when the client was constructed with one. The
|
|
15
45
|
* caller adds Content-Type when posting a body. */
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,20 @@
|
|
|
27
27
|
* mintUserToken(subject, opts?): Promise<MintedUserToken> // delegated token
|
|
28
28
|
* listUserTokens(subject, opts?): Promise<ListUserTokensResponse>
|
|
29
29
|
* revokeUserToken(subject, defId, opts?): Promise<{def_id, retired_at}>
|
|
30
|
+
* approveUser(subject, opts?): Promise<UserRecord> // pending → active (RFC FA)
|
|
31
|
+
* listUserIdentities(subject, opts?): Promise<UserIdentitiesResponse>
|
|
32
|
+
* unlinkUserIdentity(subject, gate, opts?): Promise<void>
|
|
33
|
+
*
|
|
34
|
+
* // Sign-in gates + the password gate's management API (RFC FA)
|
|
35
|
+
* listAuthGates(opts?): Promise<AuthGateListResponse> // admin token only
|
|
36
|
+
* createInvite(gate, body, opts?): Promise<CreatedInvite> // one-time link, shown once
|
|
37
|
+
* listInvites(gate, opts?): Promise<AuthInviteListResponse>
|
|
38
|
+
* revokeInvite(gate, inviteId, opts?): Promise<void>
|
|
39
|
+
* createPasswordReset(gate, subject, opts?): Promise<CreatedPasswordReset>
|
|
40
|
+
* removeGateCredentials(gate, subject, opts?): Promise<void>
|
|
41
|
+
* renameGateLogin(gate, subject, login, opts?): Promise<{subject, login}>
|
|
42
|
+
* listGateIdentities(gate, subject, opts?): Promise<GateIdentitiesResponse>
|
|
43
|
+
* getGateTenantStatus(gate, opts?): Promise<GateTenantStatus>
|
|
30
44
|
* runnableAgents(opts?): Promise<RunnableAgentsResponse> // RFC BY (v1.51.0) — agents I can run
|
|
31
45
|
* whoami(): Promise<WhoamiResponse> // RFC L principal (v0.17.0)
|
|
32
46
|
*
|
|
@@ -115,6 +129,19 @@
|
|
|
115
129
|
* listVolumes(): Promise<PersistentVolumesResponse>
|
|
116
130
|
* listEphemeralVolumes(): Promise<EphemeralVolumesResponse>
|
|
117
131
|
*
|
|
132
|
+
* // App clients (RFC FL ClientDef; tenant operator or admin)
|
|
133
|
+
* clientDef(input): Promise<unknown> // POST /v1/_clientdef
|
|
134
|
+
* createClientDef / updateClientDef(name, client, opts?): Promise<ClientDefRecord>
|
|
135
|
+
* getClientDef({name | def_id}): Promise<AppClientEntry>
|
|
136
|
+
* listClientDefs(opts?): Promise<ClientDefListResponse>
|
|
137
|
+
* retireClientDef(name): Promise<ClientDefRetireResult>
|
|
138
|
+
* verifyClientDef(name, {contentSha256}?): Promise<ClientDefVerifyResult>
|
|
139
|
+
* listClientDefNames(opts?): Promise<ClientDefNamesResponse> // GET /v1/_clientdef/names
|
|
140
|
+
*
|
|
141
|
+
* // App sign-in (RFC FL) — also standalone as `new LoomcycleOAuth(...)`
|
|
142
|
+
* oauth: LoomcycleOAuth // beginAuthorize / completeAuthorize / refresh / revoke / discover
|
|
143
|
+
* new LoomcycleClient({ tokenProvider: new OAuthTokenProvider(...) }) // refreshes on its own
|
|
144
|
+
*
|
|
118
145
|
* // Agent Teams (RFC AP — TeamDef substrate; tenant-confined)
|
|
119
146
|
* listTeams(): Promise<ListTeamsResponse>
|
|
120
147
|
* renderTeamDiagram(name, opts?): Promise<TeamDiagram>
|
|
@@ -187,5 +214,7 @@ export { verifyLoomcycleSignature, verifyLoomcycleDelivery } from "./signature.j
|
|
|
187
214
|
export type { VerifySignatureOptions, VerifiedDelivery } from "./signature.js";
|
|
188
215
|
export { ClientToolHost, clientToolsURL, CLIENT_TOOL_SUBPROTOCOL } from "./client-tools.js";
|
|
189
216
|
export type { ClientToolSchema, ClientToolInvocation, ConnectClientToolsOptions, WebSocketLike, WebSocketCtor, } from "./client-tools.js";
|
|
190
|
-
export
|
|
191
|
-
export {
|
|
217
|
+
export { LoomcycleOAuth, OAuthTokenProvider, parseAuthorizeResponse, pkceChallenge } from "./oauth.js";
|
|
218
|
+
export type { LoomcycleOAuthOptions, OAuthTokenProviderOptions } from "./oauth.js";
|
|
219
|
+
export type { AgentEvent, ClientOptions, ContinueOptions, EventType, HostWidening, ImageMediaType, ParentContext, PromptContent, PromptSegment, RetryInfo, RunOptions, RunOverrideOptions, SamplingOptions, CompactionOptions, ContextOptions, ToolUse, Usage, CallTiming, Agent, AgentStatus, AwaitedState, AgentUsage, RunResult, RunSpec, RunSpecTeam, ConfiguredRun, ConfiguredRunPatch, ToolChoiceOptions, OutputFormatOptions, CancelAgentResult, ListAgentsResponse, ListUserAgentsOptions, AgentList, WalkRunsPage, RunBatchOptions, RunBatchResult, SpawnRunResult, CompactRunResult, RunTreeDeletion, RunTreeNotReached, DirectoryBudget, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureAccess, ErasureReport, ErasureResidue, ErasureResult, ErasureTier, ErasureTokenRef, CancelTurnResult, ReviewRunResult, ReplaySessionResult, TranscriptEvent, TranscriptResponse, HealthResponse, ListUsersResponse, UserSummary, UserStatus, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, TokenRestrictions, UserTokenMeta, ListUserTokensResponse, UserIdentity, UserIdentitiesResponse, AuthGateKind, AuthGate, AuthGateKey, AuthGateRemoval, AuthGateProbe, AuthGateListResponse, CreateInviteBody, CreatedInvite, AuthInviteState, AuthInvite, AuthInviteListResponse, CreatedPasswordReset, GateIdentity, GateIdentitiesResponse, GateTenantStatus, RunnableAgent, RunnableAgentsResponse, WhoamiResponse, WhoamiEffective, WhoamiAuth, PauseResult, RetentionOverride, ResumeResult, RuntimeStateResponse, RuntimeStateStatus, ResolverMatrix, ResolverModelStatus, ResolverProviderAvailability, CreateSnapshotOptions, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotListResponse, SnapshotRestoreResponse, MemoryEntriesResponse, MemoryEntry, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopeIDSummary, MemoryScopeKind, MemoryScopesResponse, MemorySearchDocumentFilter, MemorySearchInput, MemorySource, MemoryWhen, MemoryTimeFilter, MemorySearchEntry, MemorySearchResponse, MemoryEmbedModelStats, MemoryEmbedStatsResponse, MemoryReembedConfigured, MemoryReembedResponse, MemoryBackfillResponse, MemoryPurgeResponse, InterruptListResponse, InterruptRow, InterruptStatus, ResolveInterruptOptions, ChannelHookCall, ChannelHookResult, HookFailMode, HookPhase, HookToolCall, HookToolResult, PostHookCall, PostHookResult, PreHookCall, PreHookResult, SubstrateToolInput, SubstrateToolResponse, TeamNameSummary, ListTeamsResponse, TeamDiagram, CreatedTeam, PromotedTeam, RetiredTeam, TeamDefDetail, TeamVerification, TeamDraft, TeamDraftVerification, TeamIssue, TeamIssueKind, TeamVersion, TeamVersionList, TeamBreakpoints, TeamRunDetached, TeamRunResult, TeamRunTarget, CreateTeamOptions, ForkTeamOptions, DefNameSummary, DefNamesResponse, OperatorTokenDefNameSummary, OperatorTokenDefNamesResponse, ScheduleListEntry, ScheduleListResponse, ScheduleActiveRun, ScheduleStateView, ScheduleRunNowResult, SchedulePauseResult, ModelAlias, ModelAliasesResponse, RoutingResponse, RoutingUserTier, RoutingTier, RoutingCandidate, RoutingThroughput, RoutingProvider, SearchRoutingProvider, RoutingEmbedder, RoutingDecision, RoutingDecisionModel, ProviderModelsResponse, TeamRunStep, TeamDecisionHandler, TeamDecisionQuestion, TeamDecisionAnswer, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, DocumentSearchFilter, DocumentSearchHit, DocumentSearchResult, DocumentRelatedResult, HistoryToolInput, HistoryToolResponse, EvaluationEmitterRole, EvaluationSubmitInput, EvaluationGetInput, EvaluationListForRunInput, EvaluationListForDefInput, EvaluationAggregateInput, EvaluationInput, EvaluationSubmitResult, EvaluationRecord, EvaluationListForRunResult, EvaluationListForDefResult, EvaluationScoreStats, EvaluationAggregate, VolumeMode, PersistentVolumeEntry, PersistentVolumesResponse, EphemeralVolumeEntry, EphemeralVolumesResponse, SystemPromptPayload, UserInputPayload, ChannelDescriptor, ListChannelsResponse, RunStateEvent, RunStateStreamClose, RunStateStreamItem, RunStateStreamOpen, StreamUserRunStatesOptions, ChangeFeedError, ChangeFeedFrame, ChangeFeedStatus, EmbedderHealth, MemoryChange, MemoryChangeScope, MemoryChangeType, StreamChangesOptions, AckChannelOptions, ChannelAckResult, ChannelMessageItem, ChannelPeekResult, ListTeamChannelsOptions, PeekTeamChannelOptions, ReleaseTeamChannelOptions, TeamChannelReleaseResult, TeamChannelDeclaredIn, TeamChannelDescriptor, TeamChannelPeekResult, TeamChannelsResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelScope, ChannelSubscribeResult, PeekChannelOptions, PublishChannelOptions, SubscribeChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAwaitEntry, ChannelAwaitMode, ChannelAwaitResult, ChannelBroadcastEntry, ChannelBroadcastResult, CreateChannelOptions, ReleaseChannelOptions, UpdateChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, AgentDefRowResponse, AgentDefVerifyResult, SkillDefVerifyResult, EnsureMcpServerOptions, EnsureMcpServerResult, MCPServerDefRowResponse, MCPServerDefVerifyResult, AgentDefOverlay, AgentRouting, AgentChannelACL, AgentInterruptionACL, AgentDecision, MemoryRerank, AgentSampling, AgentCompaction, AgentTranscription, AgentSpeech, AgentContext, TierCandidate, CoreBlock, HookEvent, InlineWebhook, HookEntry, EventHooks, ToolEntry, ToolHooksByTool, EnsureCodeAgentOptions, EnsureCodeAgentResult, CreateDefOptions, ForkDefOptions, EnsureHookDefOptions, EnsureHookDefResult, HookDefBody, HookDefForkOverlay, HookDefInput, HookDefMatch, HookDefOverlay, HookDefRowResponse, HookDefVerifyResult, ScheduleDefInput, ScheduleDefOverlay, ScheduleDefRowResponse, ScheduledRunHook, ScheduledRunPromptContent, ScheduledRunPromptSegment, WebhookDefAuth, WebhookDefFilter, WebhookDefInput, WebhookDefOverlay, WebhookDefRateLimit, WebhookDefRowResponse, WebhookDefDeliveries, WebhookDefSyncResponse, LibraryAgentDefinition, LibraryEntry, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, LLMChatContent, LLMChatMessage, LLMChatOptions, LLMChatResponse, LLMChatStreamDelta, LLMChatStreamItem, LLMChatToolCall, LLMChatUsage, LLMTool, LLMEmbeddingItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, LLMEmbeddingsUsage, UsageDimension, UsageBucket, UsageAggregate, UsageReportResponse, CapabilityInertInfo, LimitInfo, FileCapturedInfo, TranscriptionInfo, AdmissionInfo, ErrorInfo, CallFormat, CallRef, HookDecisionInfo, TeamDecisionInfo, OverrideInfo, FallbackInfo, InterruptionEventInfo, SpawnChildInfo, TurnCancelledInfo, ChannelEventInfo, ContextCompactionInfo, ContextRecapInfo, ContextStateInfo, ContextExhaustedInfo, ContextTierVerdict, ContextDistillDeclinedInfo, EffectiveConfigResponse, RunFileInput, RunFileImportSource, RunFileImportHeading, RunInboxDownload, EffectiveConfigSource, EffectiveValue, InertContextSetting, AgentOverridableResponse, OverridableField, OverrideLockReason, RetuneRunResponse, RunConfigRecord, SubagentHoldInfo, RunConfigResponse, RunPromptResponse, RunPromptSnapshot, PromptBlock, TokenLimit, TokenLimitsResponse, ConfigResponse, ConfigProvider, ConfigModel, ConfigSearch, ConfigInstance, SetTokenLimitRequest, CredentialScope, CredentialMeta, CredentialListResponse, CryptoKeyVersion, CryptoKeyExport, CryptoKeyType, CryptoKeyEncoding, CryptoKeyBackend, CryptoKeyExternal, CryptoOp, CryptoRequestBase, CryptoKeyRef, CryptoEncryptRequest, CryptoDecryptRequest, CryptoRewrapRequest, CryptoMacRequest, CryptoVerifyMacRequest, CryptoSignRequest, CryptoVerifyRequest, CryptoPublicKeyRequest, CryptoTotpRequest, CryptoHashRequest, CryptoRandomRequest, CryptoHashPasswordRequest, CryptoVerifyPasswordRequest, CryptoRequest, CryptoEncryptResult, CryptoDecryptResult, CryptoRewrapResult, CryptoMacResult, CryptoVerifyResult, CryptoSignResult, CryptoPublicJWK, CryptoPublicKeyResult, CryptoTotpResult, CryptoHashResult, CryptoRandomResult, CryptoHashPasswordResult, CryptoVerifyPasswordResult, CryptoResultByOp, CryptoResult, DecideRequest, DecideResponse, DecisionQuestion, DecisionChoiceQuestion, DecisionNoulQuestion, DecisionScoreQuestion, DecisionAnswer, DecisionChoiceAnswer, DecisionNoulAnswer, DecisionScoreAnswer, DecisionModel, DecisionModelsResponse, AclMode, AclSource, AclEffect, AclSubjectKind, AclCondition, AclRule, AclList, AclDefRow, AclDefListResponse, AclDefNamesResponse, AclValidateResult, AclCatalogType, AclCatalog, AclDefOp, AclDefInput, GroupRow, GroupMemberKind, GroupMember, GroupListResponse, GroupDetail, GroupEffective, CreateGroupBody, GroupMemberChange, AclBinding, AclBindingListResponse, CreateAclBindingBody, AuthzReason, AuthzDecision, AuthzRequestContext, AuthzEvaluationRequest, AuthzEvaluationsSemantic, AuthzEvaluationsRequest, AuthzEvaluationsResponse, AuthzTrace, AuthzAppliedBinding, AuthzExplainResponse, AuthzDraftBinding, AuthzSimulateRequest, AuthzSimulateResult, AuthzSimulateResponse, OAuthServerMetadata, OAuthTokenResponse, OAuthTokenSet, BeginAuthorizeOptions, AuthorizeRequest, AuthorizeResponseParams, CompleteAuthorizeOptions, RefreshTokenOptions, RevokeTokenOptions, TokenProvider, AppClientRefresh, AppClientConfig, ClientDefInput, ClientDefRecord, ConfigAppClient, AppClientEntry, ClientDefListResponse, ClientDefVerifyResult, ClientDefRetireResult, ClientDefNameSummary, ClientDefNamesResponse, DeviceLoginPrompt, DeviceLoginOptions, DeviceLoginResult, } from "./types.js";
|
|
220
|
+
export { AclDeniedError, AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, DeviceLoginError, NotFoundError, InsufficientScopeError, InvalidArgumentError, ChannelCursorRegressionError, LoomcycleError, NotPausedError, OAuthError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,20 @@
|
|
|
27
27
|
* mintUserToken(subject, opts?): Promise<MintedUserToken> // delegated token
|
|
28
28
|
* listUserTokens(subject, opts?): Promise<ListUserTokensResponse>
|
|
29
29
|
* revokeUserToken(subject, defId, opts?): Promise<{def_id, retired_at}>
|
|
30
|
+
* approveUser(subject, opts?): Promise<UserRecord> // pending → active (RFC FA)
|
|
31
|
+
* listUserIdentities(subject, opts?): Promise<UserIdentitiesResponse>
|
|
32
|
+
* unlinkUserIdentity(subject, gate, opts?): Promise<void>
|
|
33
|
+
*
|
|
34
|
+
* // Sign-in gates + the password gate's management API (RFC FA)
|
|
35
|
+
* listAuthGates(opts?): Promise<AuthGateListResponse> // admin token only
|
|
36
|
+
* createInvite(gate, body, opts?): Promise<CreatedInvite> // one-time link, shown once
|
|
37
|
+
* listInvites(gate, opts?): Promise<AuthInviteListResponse>
|
|
38
|
+
* revokeInvite(gate, inviteId, opts?): Promise<void>
|
|
39
|
+
* createPasswordReset(gate, subject, opts?): Promise<CreatedPasswordReset>
|
|
40
|
+
* removeGateCredentials(gate, subject, opts?): Promise<void>
|
|
41
|
+
* renameGateLogin(gate, subject, login, opts?): Promise<{subject, login}>
|
|
42
|
+
* listGateIdentities(gate, subject, opts?): Promise<GateIdentitiesResponse>
|
|
43
|
+
* getGateTenantStatus(gate, opts?): Promise<GateTenantStatus>
|
|
30
44
|
* runnableAgents(opts?): Promise<RunnableAgentsResponse> // RFC BY (v1.51.0) — agents I can run
|
|
31
45
|
* whoami(): Promise<WhoamiResponse> // RFC L principal (v0.17.0)
|
|
32
46
|
*
|
|
@@ -115,6 +129,19 @@
|
|
|
115
129
|
* listVolumes(): Promise<PersistentVolumesResponse>
|
|
116
130
|
* listEphemeralVolumes(): Promise<EphemeralVolumesResponse>
|
|
117
131
|
*
|
|
132
|
+
* // App clients (RFC FL ClientDef; tenant operator or admin)
|
|
133
|
+
* clientDef(input): Promise<unknown> // POST /v1/_clientdef
|
|
134
|
+
* createClientDef / updateClientDef(name, client, opts?): Promise<ClientDefRecord>
|
|
135
|
+
* getClientDef({name | def_id}): Promise<AppClientEntry>
|
|
136
|
+
* listClientDefs(opts?): Promise<ClientDefListResponse>
|
|
137
|
+
* retireClientDef(name): Promise<ClientDefRetireResult>
|
|
138
|
+
* verifyClientDef(name, {contentSha256}?): Promise<ClientDefVerifyResult>
|
|
139
|
+
* listClientDefNames(opts?): Promise<ClientDefNamesResponse> // GET /v1/_clientdef/names
|
|
140
|
+
*
|
|
141
|
+
* // App sign-in (RFC FL) — also standalone as `new LoomcycleOAuth(...)`
|
|
142
|
+
* oauth: LoomcycleOAuth // beginAuthorize / completeAuthorize / refresh / revoke / discover
|
|
143
|
+
* new LoomcycleClient({ tokenProvider: new OAuthTokenProvider(...) }) // refreshes on its own
|
|
144
|
+
*
|
|
118
145
|
* // Agent Teams (RFC AP — TeamDef substrate; tenant-confined)
|
|
119
146
|
* listTeams(): Promise<ListTeamsResponse>
|
|
120
147
|
* renderTeamDiagram(name, opts?): Promise<TeamDiagram>
|
|
@@ -184,4 +211,5 @@ export { LoomcycleClient } from "./client.js";
|
|
|
184
211
|
export { InteractiveSession } from "./interactive.js";
|
|
185
212
|
export { verifyLoomcycleSignature, verifyLoomcycleDelivery } from "./signature.js";
|
|
186
213
|
export { ClientToolHost, clientToolsURL, CLIENT_TOOL_SUBPROTOCOL } from "./client-tools.js";
|
|
187
|
-
export {
|
|
214
|
+
export { LoomcycleOAuth, OAuthTokenProvider, parseAuthorizeResponse, pkceChallenge } from "./oauth.js";
|
|
215
|
+
export { AclDeniedError, AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, DeviceLoginError, NotFoundError, InsufficientScopeError, InvalidArgumentError, ChannelCursorRegressionError, LoomcycleError, NotPausedError, OAuthError, PauseNotConfiguredError, PerUserQuotaExhaustedError, PromptTooLargeError, RateLimitedError, RequestTooLargeError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, TokenLimitExceededError, UnavailableError, } from "./errors.js";
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { OAuthError } from "./errors.js";
|
|
2
|
+
import type { AuthorizeRequest, AuthorizeResponseParams, BeginAuthorizeOptions, CompleteAuthorizeOptions, OAuthServerMetadata, OAuthTokenSet, RefreshTokenOptions, RevokeTokenOptions, TokenProvider } from "./types.js";
|
|
3
|
+
/** Options for {@link LoomcycleOAuth}. */
|
|
4
|
+
export interface LoomcycleOAuthOptions {
|
|
5
|
+
/** loomcycle's address as the person's browser reaches it, e.g.
|
|
6
|
+
* `https://loom.example.com`. Defaults to http://127.0.0.1:8787. */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Custom fetch (tests, Tauri's HTTP plugin); defaults to global `fetch`. */
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
/** Web Crypto; defaults to `globalThis.crypto` (browsers, Node 19+). On
|
|
11
|
+
* Node 18 pass `require("node:crypto").webcrypto`. */
|
|
12
|
+
crypto?: Crypto;
|
|
13
|
+
/** The server's issuer identifier, when the app knows it. A redirect's
|
|
14
|
+
* `iss` is compared with it; without it, the issuer is discovered from
|
|
15
|
+
* the server's metadata the first time a redirect carries `iss`. */
|
|
16
|
+
issuer?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The app sign-in helper. Construct one per loomcycle server.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* const oauth = new LoomcycleOAuth({ baseUrl: "https://loom.example.com" });
|
|
23
|
+
* // 1. Start: keep verifier + state in session storage, then navigate.
|
|
24
|
+
* const req = await oauth.beginAuthorize({ clientId: "loomboard", redirectUri });
|
|
25
|
+
* sessionStorage.setItem("lc_pkce", JSON.stringify({ verifier: req.verifier, state: req.state }));
|
|
26
|
+
* location.assign(req.url);
|
|
27
|
+
* // 2. Back on redirectUri:
|
|
28
|
+
* const saved = JSON.parse(sessionStorage.getItem("lc_pkce")!);
|
|
29
|
+
* const tokens = await oauth.completeAuthorize({
|
|
30
|
+
* ...parseAuthorizeResponse(location.href),
|
|
31
|
+
* expectedState: saved.state, verifier: saved.verifier, clientId: "loomboard", redirectUri,
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class LoomcycleOAuth {
|
|
36
|
+
private readonly baseUrl;
|
|
37
|
+
private readonly fetchImpl;
|
|
38
|
+
private readonly cryptoImpl?;
|
|
39
|
+
private readonly issuer?;
|
|
40
|
+
private metadata?;
|
|
41
|
+
constructor(opts?: LoomcycleOAuthOptions);
|
|
42
|
+
/** Start a sign-in: a fresh PKCE verifier, its S256 challenge, a state,
|
|
43
|
+
* and the authorize URL to send the person to. Nothing is sent to the
|
|
44
|
+
* server. */
|
|
45
|
+
beginAuthorize(opts: BeginAuthorizeOptions): Promise<AuthorizeRequest>;
|
|
46
|
+
/** Finish a sign-in from the redirect's parameters (see
|
|
47
|
+
* {@link parseAuthorizeResponse}). In order: the `state` must be the one
|
|
48
|
+
* this sign-in sent, an `iss` must name this server (RFC 9207), an
|
|
49
|
+
* `error` is thrown as an {@link OAuthError}, and only then is the code
|
|
50
|
+
* exchanged. */
|
|
51
|
+
completeAuthorize(opts: CompleteAuthorizeOptions): Promise<OAuthTokenSet>;
|
|
52
|
+
/** Trade a refresh token for a new token set. The refresh token is spent:
|
|
53
|
+
* persist the returned one before anything else uses the old. A spent
|
|
54
|
+
* token presented again ends the whole sign-in. */
|
|
55
|
+
refresh(opts: RefreshTokenOptions): Promise<OAuthTokenSet>;
|
|
56
|
+
/** Sign out (RFC 7009): a refresh token ends the whole sign-in, an access
|
|
57
|
+
* token ends that token. The server answers the same for a token it does
|
|
58
|
+
* not know, so this resolves unless the request itself failed. */
|
|
59
|
+
revoke(opts: RevokeTokenOptions): Promise<void>;
|
|
60
|
+
/** The server's authorization server metadata (RFC 8414). Cached after the
|
|
61
|
+
* first success. Fails with a 404 when the server does not know its public
|
|
62
|
+
* address (`LOOMCYCLE_PUBLIC_URL` unset). */
|
|
63
|
+
discover(opts?: {
|
|
64
|
+
signal?: AbortSignal;
|
|
65
|
+
}): Promise<OAuthServerMetadata>;
|
|
66
|
+
private fetchMetadata;
|
|
67
|
+
private tokenRequest;
|
|
68
|
+
/** POST a form: parameters in the body, never the URL; no bearer and no
|
|
69
|
+
* cookie (the code or the refresh token is the credential); only
|
|
70
|
+
* CORS-safelisted headers, so a browser sends it without a preflight. */
|
|
71
|
+
private postForm;
|
|
72
|
+
private webCrypto;
|
|
73
|
+
}
|
|
74
|
+
/** Read the authorization response parameters from the redirect URL (its
|
|
75
|
+
* query). Pass the result to {@link LoomcycleOAuth.completeAuthorize}. */
|
|
76
|
+
export declare function parseAuthorizeResponse(url: string | URL): AuthorizeResponseParams;
|
|
77
|
+
/** The PKCE S256 code challenge of a verifier (RFC 7636 §4.2):
|
|
78
|
+
* BASE64URL(SHA-256(ASCII(verifier))). */
|
|
79
|
+
export declare function pkceChallenge(verifier: string, cryptoImpl?: Crypto): Promise<string>;
|
|
80
|
+
/** Options for {@link OAuthTokenProvider}. */
|
|
81
|
+
export interface OAuthTokenProviderOptions {
|
|
82
|
+
/** The helper for the server that issued the tokens. */
|
|
83
|
+
oauth: LoomcycleOAuth;
|
|
84
|
+
clientId: string;
|
|
85
|
+
/** The current token set (from completeAuthorize, or persisted). */
|
|
86
|
+
tokens: OAuthTokenSet;
|
|
87
|
+
/** Called with every new token set, before the token is used. Persist it:
|
|
88
|
+
* the refresh token it replaced is spent. A throw fails the request that
|
|
89
|
+
* caused the refresh; the new set is kept in memory regardless. */
|
|
90
|
+
onTokens?: (tokens: OAuthTokenSet) => void | Promise<void>;
|
|
91
|
+
/** Called once when the sign-in ends: the server refused the refresh token
|
|
92
|
+
* (revoked, expired, reused) or {@link OAuthTokenProvider.signOut} ran.
|
|
93
|
+
* Clear what you persisted and send the person to sign in again. */
|
|
94
|
+
onSignedOut?: (reason: OAuthError) => void;
|
|
95
|
+
/** Refresh this long before the access token expires (default 60 s). */
|
|
96
|
+
refreshSkewMs?: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A {@link TokenProvider} over an app sign-in: hands out the access token,
|
|
100
|
+
* refreshes it before it expires and once after a 401, and shares one
|
|
101
|
+
* refresh between concurrent requests (two refreshes of the same refresh
|
|
102
|
+
* token would end the sign-in, since the server takes the second as a
|
|
103
|
+
* replay).
|
|
104
|
+
*
|
|
105
|
+
* One provider per sign-in: two providers — or two browser tabs — refreshing
|
|
106
|
+
* the same refresh token end it the same way.
|
|
107
|
+
*/
|
|
108
|
+
export declare class OAuthTokenProvider implements TokenProvider {
|
|
109
|
+
private readonly opts;
|
|
110
|
+
private current?;
|
|
111
|
+
private inflight?;
|
|
112
|
+
private ended?;
|
|
113
|
+
private readonly skew;
|
|
114
|
+
constructor(opts: OAuthTokenProviderOptions);
|
|
115
|
+
/** The current token set; undefined once signed out. */
|
|
116
|
+
get tokens(): OAuthTokenSet | undefined;
|
|
117
|
+
getToken(): Promise<string | undefined>;
|
|
118
|
+
onUnauthorized(rejected: string | undefined): Promise<string | undefined>;
|
|
119
|
+
/** Refresh now, sharing a refresh already under way. */
|
|
120
|
+
refresh(): Promise<OAuthTokenSet>;
|
|
121
|
+
/** End the sign-in: revoke the refresh token on the server (which also
|
|
122
|
+
* ends the access tokens it issued) and forget the tokens. Resolves even
|
|
123
|
+
* if the server could not be reached; the tokens are forgotten either
|
|
124
|
+
* way. */
|
|
125
|
+
signOut(opts?: {
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
}): Promise<void>;
|
|
128
|
+
private refreshOnce;
|
|
129
|
+
private doRefresh;
|
|
130
|
+
private end;
|
|
131
|
+
}
|