@foir/sdk 0.28.0 → 0.30.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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Sign a person in through the OpenID Connect code flow, from a server app
3
+ * that has no OpenID Connect library of its own.
4
+ *
5
+ * `beginSignIn` finds the issuer's endpoints from its discovery document and
6
+ * builds the authorization URL, with a PKCE S256 challenge, a state and a
7
+ * nonce. It returns the URL to send the browser to and what the app keeps
8
+ * until the person comes back: keep it on the server, or in an http-only
9
+ * cookie scoped to the callback. `completeSignIn` checks the callback against
10
+ * it, redeems the code at the token endpoint and returns the tokens.
11
+ *
12
+ * These are the same endpoints any OpenID Connect library uses
13
+ * (`/oauth/authorize` and `/oauth/token`), so an app can move to a library
14
+ * later without changing anything in Foir. Only `fetch` and Web Crypto are
15
+ * used, so the helper runs in Workers, Oxygen and Node 18 or later.
16
+ *
17
+ * The ID token is checked for its issuer, audience, nonce and expiry. Its
18
+ * signature is not checked: it came straight from the issuer's token
19
+ * endpoint over TLS, which OpenID Connect accepts in place of the signature
20
+ * (Core 1.0, section 3.1.3.7).
21
+ */
22
+ /** Where the app signs people in. */
23
+ export interface CodeFlowConfig {
24
+ /**
25
+ * The issuer: Foir's own (`https://api.foir.dev`) or a project's login
26
+ * domain (`https://login.example.com`), which is the issuer of the people
27
+ * who sign in through it.
28
+ */
29
+ issuer: string;
30
+ /** The auth client's id. */
31
+ clientId: string;
32
+ /** The callback, exactly as the auth client lists it. */
33
+ redirectUri: string;
34
+ /** A fetch to use in place of the global one. */
35
+ fetch?: typeof fetch;
36
+ }
37
+ /** What one sign-in asks for. */
38
+ export interface BeginSignInOptions {
39
+ /**
40
+ * What the sign-in asks to carry. Default `openid profile email`. Add the
41
+ * permission names the app's list holds to ask for fewer than all of them.
42
+ */
43
+ scope?: string | string[];
44
+ /** The organization the person signs in to, when the app serves several. */
45
+ org?: string;
46
+ /** `login` asks the person to sign in again, `consent` to agree again. */
47
+ prompt?: string;
48
+ /** The address to start the sign-in with. */
49
+ loginHint?: string;
50
+ }
51
+ /** A sign-in begun: where to send the browser, and what to keep for the callback. */
52
+ export interface PendingSignIn {
53
+ /** The authorization URL. */
54
+ url: string;
55
+ /** Kept to check the callback's `state`. */
56
+ state: string;
57
+ /** Kept, and sent with the code. Never put it in a URL. */
58
+ codeVerifier: string;
59
+ /** Kept to check the ID token's `nonce`. */
60
+ nonce: string;
61
+ }
62
+ /** What a completed sign-in hands back. */
63
+ export interface CodeFlowTokens {
64
+ accessToken: string;
65
+ tokenType: string;
66
+ expiresIn?: number;
67
+ refreshToken?: string;
68
+ idToken?: string;
69
+ scope?: string;
70
+ /** The ID token's claims, checked as described above. */
71
+ claims?: Record<string, unknown>;
72
+ }
73
+ /**
74
+ * A sign-in that did not complete. `code` is the OAuth error the issuer
75
+ * answered (`access_denied`, `invalid_grant`, ...), or one of the checks the
76
+ * helper makes: `state_mismatch`, `issuer_mismatch`, `nonce_mismatch`,
77
+ * `audience_mismatch`, `id_token_expired`, `discovery_failed` (the issuer
78
+ * could not be reached or answered no discovery document),
79
+ * `token_request_failed` (the token endpoint could not be reached or answered
80
+ * no access token), `invalid_request` (the callback carries no code).
81
+ */
82
+ export declare class CodeFlowError extends Error {
83
+ readonly code: string;
84
+ readonly description?: string;
85
+ constructor(code: string, description?: string);
86
+ }
87
+ /**
88
+ * Begin a sign-in: the URL to send the browser to, and what to keep until
89
+ * the person comes back to the callback.
90
+ */
91
+ export declare function beginSignIn(config: CodeFlowConfig, options?: BeginSignInOptions): Promise<PendingSignIn>;
92
+ /**
93
+ * Complete a sign-in from its callback: check it against what `beginSignIn`
94
+ * returned, redeem the code at the token endpoint, and return the tokens.
95
+ * `callback` is the URL the browser came back to, or its query.
96
+ */
97
+ export declare function completeSignIn(config: CodeFlowConfig, callback: string | URL | URLSearchParams, pending: Pick<PendingSignIn, 'state' | 'codeVerifier' | 'nonce'>): Promise<CodeFlowTokens>;
@@ -1,5 +1,5 @@
1
1
  export type GrantReach = 'own' | 'named' | 'all';
2
- export type GrantTarget = 'none' | 'model' | 'operation';
2
+ export type GrantTarget = 'none' | 'model' | 'operation' | 'agent' | 'agent_tool';
3
3
  /**
4
4
  * One permission: its name, what a grant of it may name, the reaches it may be
5
5
  * granted at (none: only its bare form exists), and the grants a holder of it
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * A grant is written `<resource>:<verb>[.<reach>][:<target>]`. The reach is
5
5
  * `named` or `all`, and an unwritten reach means the holder's own rows. The
6
- * target narrows it to one model or one operation.
6
+ * target narrows it to one model, one operation, one agent, or one tool of a
7
+ * remote server registered on an agent (`<agent>.<server>.<tool>`).
7
8
  *
8
9
  * This is the platform's rule, answered from the platform's own declarations
9
10
  * (the table is written from the contract when this package is built) and held
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Hosted-login bridge — for storefronts that delegate sign-in to a Foir
3
- * white-label front door (the `loginWithFoir` flow) instead of
4
- * rendering their own login UI.
2
+ * The hosted session: for an app on the same registrable domain as its login
3
+ * domain, which trades the login domain's session cookie for a token instead
4
+ * of running the code flow itself (see code-flow.ts for that).
5
5
  *
6
6
  * The front door sets a first-party session cookie on the registrable domain;
7
7
  * because that domain is shared, the cookie reaches the storefront, which
@@ -37,6 +37,14 @@ export interface HostedLoginOptions {
37
37
  * of another project, and one that is not active.
38
38
  */
39
39
  org?: string;
40
+ /**
41
+ * Somebody new is expected: the page opens on the project's way in rather
42
+ * than the sign-in form, for a "Sign up" or "Join the waitlist" button. The
43
+ * project's sign-up still decides what that is: joining where it is public,
44
+ * the waitlist, or the invitation-only or closed notice. The URL is the
45
+ * login domain's `/signup`.
46
+ */
47
+ signUp?: boolean;
40
48
  }
41
49
  /**
42
50
  * Build the redirect URL to the hosted front door, forwarding an absolute
@@ -144,7 +152,7 @@ export type HostedSessionResult =
144
152
  */
145
153
  export declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSessionResult>;
146
154
  /**
147
- * What the hosted login said beside the code when it sent someone back to your
155
+ * What hosted sign-in said beside the code when it sent someone back to your
148
156
  * app. A sign-in it refused carries a stable code (`foir_error`); one that
149
157
  * connected a provider to the person's account on the way in names the
150
158
  * provider (`foir_connected`). Each comes with a sentence your app can show as
@@ -161,9 +169,9 @@ export type HostedSignInNotice = {
161
169
  message: string;
162
170
  };
163
171
  /**
164
- * Reads the notice off the address the hosted login sent the browser back to:
172
+ * Reads the notice off the address hosted sign-in sent the browser back to:
165
173
  * your callback URL, its search string, or its parameters. `names` maps a
166
174
  * provider's key to the name to show (from `getAuthProviders`); without it the
167
- * key is shown. Null when the hosted login said nothing.
175
+ * key is shown. Null when hosted sign-in said nothing.
168
176
  */
169
177
  export declare function hostedSignInNoticeOf(location: string | URL | URLSearchParams, names?: Record<string, string>): HostedSignInNotice | null;
@@ -10,7 +10,7 @@
10
10
  * the caller's own manifest advertises, through the same op transport
11
11
  * `@foir/sdk/data` uses, so `graphql` is not on this module's path at all.
12
12
  */
13
- export { login, register, requestOTP, loginWithOTP, refreshTokens, loginWithProvider, providerCallback, providerVerifyOTP, providerSignInRefusalOf, signInRefusalOf, confirmProviderLink, resendProviderLinkCode, getAuthProviders, getAuthConfig, getDefaultAuthProvider, signupContext, joinWaitlist, requestPasswordReset, resetPassword, updatePassword, currentUser, myProviderToken, logout, deleteMyAccount, leaveMembership, updateMyProfile, myAuthorizedApps, revokeAuthorizedApp, mySessions, endMySession, myAddresses, addMyAddress, proveMyAddress, makeMyAddressPrimary, removeMyAddress, exportMyData, signOutEverywhere, createPersonalAccessToken, mySignInMethods, connectSignInMethod, disconnectSignInMethod, myFactors, beginStepUp, completeStepUp, beginFactorEnrollment, completeFactorEnrollment, removeFactor, renameFactor, makeRecoveryCodes, myOrganizations, myOrganizationChoices, registerOrganization, saveSession, clearSession, getSessionToken, getSessionUser, signInWithPassword, signUpWithPassword, signInWithOTP, resetPasswordAndSignIn, refreshSession, refreshSessionOnce, signOut, authedRequest, authedOp, requireAuth, } from './auth';
13
+ export { login, register, requestOTP, loginWithOTP, refreshTokens, loginWithProvider, providerCallback, providerVerifyOTP, providerSignInRefusalOf, signInRefusalOf, confirmProviderLink, resendProviderLinkCode, getAuthProviders, getAuthConfig, getDefaultAuthProvider, signupContext, joinWaitlist, requestPasswordReset, resetPassword, updatePassword, currentUser, myProviderToken, logout, deleteMyAccount, deleteMyAccountAndSignOut, leaveMembership, updateMyProfile, myAuthorizedApps, revokeAuthorizedApp, mySessions, endMySession, myAddresses, addMyAddress, proveMyAddress, makeMyAddressPrimary, removeMyAddress, exportMyData, signOutEverywhere, createPersonalAccessToken, mySignInMethods, connectSignInMethod, disconnectSignInMethod, myFactors, beginStepUp, completeStepUp, beginFactorEnrollment, completeFactorEnrollment, removeFactor, renameFactor, makeRecoveryCodes, myOrganizations, myOrganizationChoices, registerOrganization, saveSession, clearSession, getSessionToken, getSessionUser, signInWithPassword, signUpWithPassword, signInWithOTP, resetPasswordAndSignIn, refreshSession, refreshSessionOnce, signOut, authedRequest, authedOp, requireAuth, } from './auth';
14
14
  export type { FoirAuthTokens, FoirUser, SessionAdapter, RequireAuthOptions, } from './auth';
15
15
  export { userAccess, getSessionAccess, hasRole, hasScope, requireRole, requireScope, FoirAccessDeniedError, } from './access';
16
16
  export type { FoirAccess, FoirAccessDenial } from './access';
@@ -18,6 +18,8 @@ export { FoirAuthDowngradedError, FoirSessionExpiredError } from '../graphql/foi
18
18
  export { AUTH_OPS } from './ops';
19
19
  export type { AuthOpName, AuthOpOptions } from './ops';
20
20
  export { hostedLoginUrl, hostedLogoutUrl, fetchHostedSession, hostedSignInNoticeOf } from './hosted';
21
+ export { beginSignIn, completeSignIn, CodeFlowError } from './code-flow';
22
+ export type { CodeFlowConfig, BeginSignInOptions, PendingSignIn, CodeFlowTokens } from './code-flow';
21
23
  export type { HostedLoginConfig, HostedLoginOptions, HostedSession, HostedSessionResult, HostedSignInNotice } from './hosted';
22
24
  export { createFoirSessionClient } from './session-client';
23
25
  export type { FoirSessionClient, FoirSessionConfig, FoirSessionUser, } from './session-client';
@@ -317,6 +317,11 @@ export interface AuthorizedApp {
317
317
  id: string;
318
318
  clientId: string;
319
319
  clientName: string;
320
+ /** The app's logo, where it has one, and what it shows. */
321
+ clientLogoUrl?: string | null;
322
+ clientLogoAlt?: string | null;
323
+ /** The app's homepage, where it has one. */
324
+ clientHomepageUrl?: string | null;
320
325
  /** What the app may carry for them, in the permission catalogue's names. */
321
326
  grants: string[];
322
327
  /** ISO-8601. */
@@ -11,7 +11,7 @@
11
11
  * How the pieces divide:
12
12
  *
13
13
  * cookie — long-lived (30d), HttpOnly, first-party on your registrable
14
- * domain, set by Foir when hosted login finalizes. JS cannot read
14
+ * domain, set by Foir when hosted sign-in finalizes. JS cannot read
15
15
  * it, so XSS cannot exfiltrate it.
16
16
  * bearer — 15 minutes, held in a closure here. Never written to
17
17
  * localStorage/sessionStorage/IndexedDB, never in a cookie of ours.
package/dist/auth.js CHANGED
@@ -1408,8 +1408,8 @@ async function getSessionUser(adapter) {
1408
1408
  async function signInWithPassword(adapter, env, vars) {
1409
1409
  return saveSession(adapter, await login(env, vars));
1410
1410
  }
1411
- async function signUpWithPassword(adapter, env, vars) {
1412
- return saveSession(adapter, await register(env, vars));
1411
+ async function signUpWithPassword(env, vars) {
1412
+ await register(env, vars);
1413
1413
  }
1414
1414
  async function signInWithOTP(adapter, env, vars) {
1415
1415
  return saveSession(adapter, await loginWithOTP(env, vars));
@@ -1417,6 +1417,11 @@ async function signInWithOTP(adapter, env, vars) {
1417
1417
  async function resetPasswordAndSignIn(adapter, env, vars) {
1418
1418
  return saveSession(adapter, await resetPassword(env, vars));
1419
1419
  }
1420
+ async function deleteMyAccountAndSignOut(adapter, env, vars) {
1421
+ const result = await authedOp(adapter, env, AUTH_OPS.deleteMyAccount, vars);
1422
+ await clearSession(adapter);
1423
+ return result;
1424
+ }
1420
1425
  async function signOut(adapter, env) {
1421
1426
  const token = await getSessionToken(adapter);
1422
1427
  if (token) {
@@ -1564,6 +1569,12 @@ var GRANT_TABLE = [
1564
1569
  ["operations:write", "none", [], []],
1565
1570
  ["operations:complete", "none", [], []],
1566
1571
  ["status:write", "none", [], []],
1572
+ ["agents:read", "none", [], []],
1573
+ ["agents:execute", "agent", [], []],
1574
+ ["agents:write", "none", [], []],
1575
+ ["agenttools:execute", "agent_tool", [], []],
1576
+ ["brief:read", "none", [], []],
1577
+ ["brief:write", "none", [], []],
1567
1578
  ["appconfig:read", "none", [], []],
1568
1579
  ["schedules:read", "none", [], []],
1569
1580
  ["schedules:write", "none", [], []],
@@ -1602,6 +1613,7 @@ var GRANT_TABLE = [
1602
1613
  ["apikeys:write", "none", [], []],
1603
1614
  ["organizations:read", "none", [], []],
1604
1615
  ["organizations:write", "none", [], []],
1616
+ ["orgsettings:write", "none", [], []],
1605
1617
  ["organizations:create", "none", [], []],
1606
1618
  ["organizations:delete", "none", [], []],
1607
1619
  ["organizations:reach", "none", [], []],
@@ -1612,7 +1624,7 @@ var GRANT_TABLE = [
1612
1624
  ["projects:read", "none", [], []],
1613
1625
  ["projects:write", "none", [], []],
1614
1626
  ["project:stand", "none", [], []],
1615
- ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "auth:login_with_foir", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
1627
+ ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "agents:read", "agents:execute", "agents:write", "agenttools:execute", "brief:read", "brief:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
1616
1628
  ["projects:create", "none", [], []],
1617
1629
  ["projects:delete", "none", [], []],
1618
1630
  ["tenants:read", "none", [], []],
@@ -1643,7 +1655,6 @@ var GRANT_TABLE = [
1643
1655
  ["usersecrets:list", "none", [], []],
1644
1656
  ["usersecrets:put", "none", [], []],
1645
1657
  ["usersecrets:delete", "none", [], []],
1646
- ["auth:login_with_foir", "none", [], []],
1647
1658
  ["signinproviders:read", "none", [], []],
1648
1659
  ["signinproviders:write", "none", [], []]
1649
1660
  ];
@@ -1807,6 +1818,9 @@ var DEFAULT_SESSION_PATH = "/session/token";
1807
1818
  var DEFAULT_END_SESSION_PATH = "/session/end-session";
1808
1819
  function hostedLoginUrl(config, returnTo, options = {}) {
1809
1820
  const dest = new URL(config.loginOrigin);
1821
+ if (options.signUp) {
1822
+ dest.pathname = `${dest.pathname.replace(/\/+$/, "")}/signup`;
1823
+ }
1810
1824
  if (returnTo && returnTo !== "/") {
1811
1825
  dest.searchParams.set("returnTo", returnTo);
1812
1826
  }
@@ -1913,6 +1927,169 @@ function forApp(clientId) {
1913
1927
  return clientId ? `?client_id=${encodeURIComponent(clientId)}` : "";
1914
1928
  }
1915
1929
 
1930
+ // src/auth/code-flow.ts
1931
+ var CodeFlowError = class extends Error {
1932
+ code;
1933
+ description;
1934
+ constructor(code, description) {
1935
+ super(description ? `${code}: ${description}` : code);
1936
+ this.name = "CodeFlowError";
1937
+ this.code = code;
1938
+ this.description = description;
1939
+ }
1940
+ };
1941
+ var discoveries = /* @__PURE__ */ new Map();
1942
+ function trimSlash(url) {
1943
+ return url.replace(/\/+$/, "");
1944
+ }
1945
+ async function discover(config) {
1946
+ const issuer = trimSlash(config.issuer);
1947
+ let found = discoveries.get(issuer);
1948
+ if (!found) {
1949
+ found = (async () => {
1950
+ let res;
1951
+ try {
1952
+ res = await (config.fetch ?? fetch)(`${issuer}/.well-known/openid-configuration`, {
1953
+ headers: { accept: "application/json" }
1954
+ });
1955
+ } catch (e) {
1956
+ throw new CodeFlowError("discovery_failed", `${issuer} could not be reached for its discovery document: ${String(e)}`);
1957
+ }
1958
+ if (!res.ok) {
1959
+ throw new CodeFlowError("discovery_failed", `${issuer} answered ${res.status} for its discovery document`);
1960
+ }
1961
+ const doc = await res.json().catch(() => null);
1962
+ if (!doc) {
1963
+ throw new CodeFlowError("discovery_failed", `${issuer} answered no JSON for its discovery document`);
1964
+ }
1965
+ if (!doc.issuer || trimSlash(doc.issuer) !== issuer) {
1966
+ throw new CodeFlowError("issuer_mismatch", `the discovery document at ${issuer} names ${doc.issuer ?? "no issuer"}`);
1967
+ }
1968
+ if (!doc.authorization_endpoint || !doc.token_endpoint) {
1969
+ throw new CodeFlowError("discovery_failed", `the discovery document at ${issuer} names no authorization or token endpoint`);
1970
+ }
1971
+ return doc;
1972
+ })();
1973
+ discoveries.set(issuer, found);
1974
+ found.catch(() => discoveries.delete(issuer));
1975
+ }
1976
+ return found;
1977
+ }
1978
+ function base64url(bytes) {
1979
+ let s = "";
1980
+ for (const b of bytes) s += String.fromCharCode(b);
1981
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1982
+ }
1983
+ function randomString(byteLength) {
1984
+ const bytes = new Uint8Array(byteLength);
1985
+ crypto.getRandomValues(bytes);
1986
+ return base64url(bytes);
1987
+ }
1988
+ async function s256(verifier) {
1989
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
1990
+ return base64url(new Uint8Array(digest));
1991
+ }
1992
+ async function beginSignIn(config, options = {}) {
1993
+ const endpoints = await discover(config);
1994
+ const state = randomString(24);
1995
+ const nonce = randomString(24);
1996
+ const codeVerifier = randomString(48);
1997
+ const url = new URL(endpoints.authorization_endpoint);
1998
+ const scope = Array.isArray(options.scope) ? options.scope.join(" ") : options.scope ?? "openid profile email";
1999
+ url.searchParams.set("client_id", config.clientId);
2000
+ url.searchParams.set("redirect_uri", config.redirectUri);
2001
+ url.searchParams.set("response_type", "code");
2002
+ url.searchParams.set("scope", scope);
2003
+ url.searchParams.set("state", state);
2004
+ url.searchParams.set("nonce", nonce);
2005
+ url.searchParams.set("code_challenge", await s256(codeVerifier));
2006
+ url.searchParams.set("code_challenge_method", "S256");
2007
+ if (options.org) url.searchParams.set("org", options.org);
2008
+ if (options.prompt) url.searchParams.set("prompt", options.prompt);
2009
+ if (options.loginHint) url.searchParams.set("login_hint", options.loginHint);
2010
+ return { url: url.toString(), state, codeVerifier, nonce };
2011
+ }
2012
+ function callbackParams(callback) {
2013
+ if (callback instanceof URLSearchParams) return callback;
2014
+ if (callback instanceof URL) return callback.searchParams;
2015
+ const query = callback.indexOf("?");
2016
+ return new URLSearchParams(query >= 0 ? callback.slice(query + 1).split("#")[0] : callback);
2017
+ }
2018
+ function claimsOf(idToken) {
2019
+ const payload = idToken.split(".")[1];
2020
+ if (!payload) throw new CodeFlowError("invalid_id_token", "the ID token is not a JWT");
2021
+ const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
2022
+ const bytes = Uint8Array.from(json, (c) => c.charCodeAt(0));
2023
+ return JSON.parse(new TextDecoder().decode(bytes));
2024
+ }
2025
+ async function completeSignIn(config, callback, pending) {
2026
+ const endpoints = await discover(config);
2027
+ const params = callbackParams(callback);
2028
+ if (params.get("state") !== pending.state) {
2029
+ throw new CodeFlowError("state_mismatch", "the callback is not for the sign-in this app began");
2030
+ }
2031
+ const refused = params.get("error");
2032
+ if (refused) {
2033
+ throw new CodeFlowError(refused, params.get("error_description") ?? void 0);
2034
+ }
2035
+ const code = params.get("code");
2036
+ if (!code) {
2037
+ throw new CodeFlowError("invalid_request", "the callback carries no code");
2038
+ }
2039
+ let res;
2040
+ try {
2041
+ res = await (config.fetch ?? fetch)(endpoints.token_endpoint, {
2042
+ method: "POST",
2043
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
2044
+ body: new URLSearchParams({
2045
+ grant_type: "authorization_code",
2046
+ code,
2047
+ redirect_uri: config.redirectUri,
2048
+ client_id: config.clientId,
2049
+ code_verifier: pending.codeVerifier
2050
+ }).toString()
2051
+ });
2052
+ } catch (e) {
2053
+ throw new CodeFlowError("token_request_failed", `the token endpoint could not be reached: ${String(e)}`);
2054
+ }
2055
+ const body = await res.json().catch(() => ({}));
2056
+ if (!res.ok) {
2057
+ throw new CodeFlowError(
2058
+ typeof body.error === "string" ? body.error : "token_request_failed",
2059
+ typeof body.error_description === "string" ? body.error_description : `the token endpoint answered ${res.status}`
2060
+ );
2061
+ }
2062
+ const tokens = {
2063
+ accessToken: String(body.access_token ?? ""),
2064
+ tokenType: String(body.token_type ?? "Bearer"),
2065
+ expiresIn: typeof body.expires_in === "number" ? body.expires_in : void 0,
2066
+ refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
2067
+ idToken: typeof body.id_token === "string" ? body.id_token : void 0,
2068
+ scope: typeof body.scope === "string" ? body.scope : void 0
2069
+ };
2070
+ if (!tokens.accessToken) {
2071
+ throw new CodeFlowError("token_request_failed", "the token endpoint answered no access token");
2072
+ }
2073
+ if (tokens.idToken) {
2074
+ const claims = claimsOf(tokens.idToken);
2075
+ if (typeof claims.iss !== "string" || trimSlash(claims.iss) !== trimSlash(endpoints.issuer)) {
2076
+ throw new CodeFlowError("issuer_mismatch", `the ID token's issuer is ${String(claims.iss)}`);
2077
+ }
2078
+ const audience = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
2079
+ if (!audience.includes(config.clientId)) {
2080
+ throw new CodeFlowError("audience_mismatch", "the ID token is not for this app");
2081
+ }
2082
+ if (claims.nonce !== pending.nonce) {
2083
+ throw new CodeFlowError("nonce_mismatch", "the ID token is not for the sign-in this app began");
2084
+ }
2085
+ if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
2086
+ throw new CodeFlowError("id_token_expired", "the ID token has expired");
2087
+ }
2088
+ tokens.claims = claims;
2089
+ }
2090
+ return tokens;
2091
+ }
2092
+
1916
2093
  // src/auth/session-client.ts
1917
2094
  var DEFAULT_SESSION_PATH2 = "/session/token";
1918
2095
  var DEFAULT_LOGOUT_PATH = "/session/logout";
@@ -1921,8 +2098,8 @@ var MIN_REFRESH_DELAY_MS = 5e3;
1921
2098
  var CSRF_HEADER = { "X-Foir-Session": "1" };
1922
2099
  function createFoirSessionClient(config) {
1923
2100
  const forApp2 = config.clientId ? `?client_id=${encodeURIComponent(config.clientId)}` : "";
1924
- const sessionUrl = `${trimSlash(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH2}${forApp2}`;
1925
- const logoutUrl = `${trimSlash(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}${forApp2}`;
2101
+ const sessionUrl = `${trimSlash2(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH2}${forApp2}`;
2102
+ const logoutUrl = `${trimSlash2(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}${forApp2}`;
1926
2103
  const skewMs = (config.refreshSkewSeconds ?? DEFAULT_REFRESH_SKEW_SECONDS) * 1e3;
1927
2104
  let cached = null;
1928
2105
  let inflight = null;
@@ -2029,7 +2206,7 @@ function createFoirSessionClient(config) {
2029
2206
  config.onSignOut?.();
2030
2207
  },
2031
2208
  signInUrl(returnTo, options = {}) {
2032
- const dest = new URL(trimSlash(config.loginOrigin));
2209
+ const dest = new URL(trimSlash2(config.loginOrigin));
2033
2210
  const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
2034
2211
  if (target && target !== "/") dest.searchParams.set("returnTo", target);
2035
2212
  if (options.org) dest.searchParams.set("org", options.org);
@@ -2046,11 +2223,12 @@ function createFoirSessionClient(config) {
2046
2223
  }
2047
2224
  };
2048
2225
  }
2049
- function trimSlash(s) {
2226
+ function trimSlash2(s) {
2050
2227
  return s.replace(/\/+$/, "");
2051
2228
  }
2052
2229
  export {
2053
2230
  AUTH_OPS,
2231
+ CodeFlowError,
2054
2232
  FoirAccessDeniedError,
2055
2233
  FoirAuthDowngradedError,
2056
2234
  FoirSessionExpiredError,
@@ -2058,9 +2236,11 @@ export {
2058
2236
  authedOp,
2059
2237
  authedRequest,
2060
2238
  beginFactorEnrollment,
2239
+ beginSignIn,
2061
2240
  beginStepUp,
2062
2241
  clearSession,
2063
2242
  completeFactorEnrollment,
2243
+ completeSignIn,
2064
2244
  completeStepUp,
2065
2245
  confirmProviderLink,
2066
2246
  connectSignInMethod,
@@ -2068,6 +2248,7 @@ export {
2068
2248
  createPersonalAccessToken,
2069
2249
  currentUser,
2070
2250
  deleteMyAccount,
2251
+ deleteMyAccountAndSignOut,
2071
2252
  disconnectSignInMethod,
2072
2253
  endMySession,
2073
2254
  exportMyData,
@@ -21,7 +21,7 @@ export declare class PlatformClientError extends Error {
21
21
  * });
22
22
  *
23
23
  * const { data } = await client.query(
24
- * `query GetProduct($id: ID!) { product(id: $id) { title } }`,
24
+ * `query GetProduct($id: String!) { product(id: $id) { title } }`,
25
25
  * { id: '123' }
26
26
  * );
27
27
  * ```
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Talking to a project's agents from a site: send a message, follow the run as
3
+ * it answers, and keep a visitor's conversation theirs.
4
+ *
5
+ * ```ts
6
+ * const reply = await askAgent(foir, {
7
+ * agent: 'ask_about_foir',
8
+ * message: 'How do releases work?',
9
+ * visitorToken: visitorToken(),
10
+ * onText: (delta) => render(delta),
11
+ * });
12
+ * // reply.threadId continues the conversation
13
+ * ```
14
+ *
15
+ * The run is the platform's: it charges `agents:execute` for the agent to the
16
+ * caller, signed in or a visitor through a key that gives it, and a refusal
17
+ * (a message too long, a limit reached, the safety check) comes back as the
18
+ * run's answer, not as an error. Only a call the platform could not take at
19
+ * all throws.
20
+ */
21
+ import type { FoirAgentAttachment, FoirAgentAttachmentUpload, FoirAgentRun, FoirAgentRunStatus, FoirCreateAgentAttachmentArgs, FoirStartAgentRunArgs } from './builtins';
22
+ import type { FoirAgentRunEvent, FoirAgentRunSubscribeOptions } from './subscriptions';
23
+ import type { FoirSubscription } from './ws-transport';
24
+ /** What `askAgent` needs of a client: the agents namespace a key that runs an agent is given. */
25
+ export interface FoirAgentsClient {
26
+ ns: {
27
+ agents: {
28
+ startAgentRun(args: FoirStartAgentRunArgs): Promise<FoirAgentRun>;
29
+ onRun(options: FoirAgentRunSubscribeOptions): FoirSubscription;
30
+ };
31
+ };
32
+ }
33
+ /** What `uploadAgentFile` needs of a client. */
34
+ export interface FoirAgentFilesClient {
35
+ ns: {
36
+ agents: {
37
+ createAgentAttachment(args: FoirCreateAgentAttachmentArgs): Promise<FoirAgentAttachmentUpload>;
38
+ };
39
+ };
40
+ }
41
+ /**
42
+ * A file to send an agent: a `File` from an input or a paste, or bytes with
43
+ * their type. An image (PNG, JPEG, GIF or WebP) of at most 5 MB, or a PDF of
44
+ * at most 10 MB; a visitor may send images only.
45
+ */
46
+ export type FoirAgentFile = Blob | {
47
+ data: Blob | ArrayBuffer | Uint8Array;
48
+ mediaType: string;
49
+ filename?: string;
50
+ };
51
+ /**
52
+ * Upload a file to send an agent, and answer with the attachment to pass as
53
+ * one of `attachmentIds`. The bytes go straight to the project's storage; the
54
+ * attachment is read only through the conversation it is sent in.
55
+ */
56
+ export declare function uploadAgentFile(foir: FoirAgentFilesClient | {
57
+ readonly ns: object;
58
+ }, options: {
59
+ agent: string;
60
+ file: FoirAgentFile;
61
+ visitorToken?: string;
62
+ fetch?: typeof fetch;
63
+ }): Promise<FoirAgentAttachment>;
64
+ export interface AskAgentOptions extends FoirStartAgentRunArgs {
65
+ /** Files sent with the message: each is uploaded first, then sent by its id. At most 5. */
66
+ files?: FoirAgentFile[];
67
+ /** Each piece of the answer as it is written. */
68
+ onText?: (delta: string) => void;
69
+ /** Every event of the run, for a caller that renders its steps. */
70
+ onEvent?: (event: FoirAgentRunEvent) => void;
71
+ /** Stop following the run; the run itself goes on. */
72
+ signal?: AbortSignal;
73
+ }
74
+ export interface AgentReply {
75
+ runId: string;
76
+ /** Pass as `threadId` to go on with the conversation. */
77
+ threadId: string | null;
78
+ status: FoirAgentRunStatus;
79
+ /** The answer, or the reason the run was refused. */
80
+ answer: string;
81
+ error: string | null;
82
+ /** An agent with an output schema: its typed answer, a value that fits the schema. */
83
+ output?: unknown;
84
+ }
85
+ /**
86
+ * Send an agent a message and follow its run until it has answered, or holds
87
+ * an act for someone's decision.
88
+ *
89
+ * It takes any client `createFoirClient` makes, generated or not: the agents
90
+ * are offered by the key the client was made with, and a client whose key runs
91
+ * no agent is refused here, by name.
92
+ */
93
+ export declare function askAgent(foir: FoirAgentsClient | {
94
+ readonly ns: object;
95
+ }, options: AskAgentOptions): Promise<AgentReply>;
96
+ /**
97
+ * The secret a visitor's browser keeps for their runs and threads: made once
98
+ * and kept, so the conversation they start is theirs when they come back. The
99
+ * platform keeps only its hash.
100
+ *
101
+ * Where nothing can be kept (a private window, a server render), a fresh one
102
+ * is made for the page, and the conversation lasts as long as it does.
103
+ */
104
+ export declare function visitorToken(storage?: Pick<Storage, 'getItem' | 'setItem'> | null, key?: string): string;