@foir/sdk 0.22.0 → 0.24.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.
@@ -35,18 +35,20 @@ export declare function appUserUnlinkedOf(error: unknown): {
35
35
  identityProvider: string;
36
36
  } | null;
37
37
  /**
38
- * Why a project cannot send a user an email. `NO_APP_BASE_URL`: no App base
39
- * URL is set for the project, or for the user's organization. A reason
40
- * this build does not know arrives as the platform spelled it.
38
+ * Why a project cannot send a user an email. `NO_APP_BASE_URL`: the email
39
+ * links into the app with `{base}`, and no App base URL is set for the project
40
+ * or for the user's organization. A reason this build does not know arrives as
41
+ * the platform spelled it.
41
42
  */
42
43
  export type AppUserEmailUnavailableReason = 'NO_APP_BASE_URL' | (string & {});
43
44
  /** The reason as the platform sent it, or null when it sent none. */
44
45
  export declare function userEmailUnavailableReasonOf(value: unknown): AppUserEmailUnavailableReason | null;
45
46
  /**
46
47
  * A request refused because it would send a user an email the project
47
- * cannot send, or null for any other failure. Nothing was written and nothing
48
- * was queued. An opt-in to a category confirmed by email is refused this way,
49
- * and so is a notification that carries an email.
48
+ * cannot build, or null for any other failure. Nothing was written and nothing
49
+ * was queued. A notification whose email links into the app with `{base}` is
50
+ * refused this way where no App base URL is set, and so is an opt-in whose
51
+ * confirmation email the project's copy links into the app.
50
52
  *
51
53
  * It is about the project, not the user: the project's team fixes it,
52
54
  * and the same request goes once they have.
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { type FoirEnv } from '../graphql/foir-request';
19
19
  import { type AuthOpName, type AuthOpOptions } from './ops';
20
- import { type AuthConfig, type AuthProviderPublic, type CurrentUser, type AuthResult, type DefaultAuthProvider, type AuthorizedApp, type DeleteAccountResult, type MyOrganization, type MyProfile, type MyProfileChange, type MySession, type MyFactor, type StepUp, type FactorEnrollment, type MySignInMethod, type NewPersonalAccessToken, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type ProviderToken, type SignupContext, type WaitlistJoinResult } from './operations';
20
+ import { type AuthConfig, type AuthProviderPublic, type CurrentUser, type AuthResult, type DefaultAuthProvider, type AuthorizedApp, type DeleteAccountResult, type MyOrganization, type MyProfile, type MyProfileChange, type MySession, type MyFactor, type StepUp, type FactorEnrollment, type MySignInMethod, type NewPersonalAccessToken, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type ProviderToken, type SignupContext } from './operations';
21
21
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
22
22
  /** An access token + its rotating refresh token. */
23
23
  export interface FoirAuthTokens {
@@ -79,6 +79,16 @@ export declare function providerSignInRefusalOf(error: unknown): {
79
79
  code: string;
80
80
  pending?: string;
81
81
  } | null;
82
+ /**
83
+ * Why a sign-in your app's own form sent was refused, as the platform said it:
84
+ * a stable code your app shows its own words for. `password_sign_in_off` from
85
+ * {@link login} or {@link register} where the project, or the app the sign-in
86
+ * came through, takes no password; `code_sign_in_off` from {@link requestOTP}
87
+ * or {@link loginWithOTP} where it takes no code. Null for any other failure.
88
+ */
89
+ export declare function signInRefusalOf(error: unknown): {
90
+ code: string;
91
+ } | null;
82
92
  /**
83
93
  * Finish a provider sign-in that is waiting for the code sent to its address,
84
94
  * connecting the provider to the account that holds the address. See
@@ -115,14 +125,26 @@ export declare function getDefaultAuthProvider(env: FoirEnv): Promise<DefaultAut
115
125
  * so a self-hosted frontend can render the right signup surface proactively.
116
126
  */
117
127
  export declare function signupContext(env: FoirEnv): Promise<SignupContext>;
118
- /** Join the project's signup waitlist. The project must be in waitlist mode. */
119
- export declare function joinWaitlist(env: FoirEnv, email: string): Promise<WaitlistJoinResult>;
128
+ /**
129
+ * Join the project's sign-up waitlist. The project must be in waitlist mode.
130
+ * Resolves `true` for every address: the server answers identically whether
131
+ * the address was queued, was already let in, or already has an account, on
132
+ * purpose, so this endpoint cannot be used to test what an address holds. What
133
+ * differs is only the email the address is sent.
134
+ */
135
+ export declare function joinWaitlist(env: FoirEnv, email: string): Promise<boolean>;
120
136
  /**
121
137
  * Send a password-reset email. Resolves `true` whether or not the address has
122
138
  * an account: the server answers identically either way, on purpose, so this
123
139
  * endpoint cannot be used to test whether someone has signed up.
140
+ *
141
+ * `clientId` names your app. Where the email's link goes to Foir's hosted
142
+ * reset page (the project has no App base URL), the page then sends the user
143
+ * to your app's initiate login URL to sign in.
124
144
  */
125
- export declare function requestPasswordReset(env: FoirEnv, email: string): Promise<boolean>;
145
+ export declare function requestPasswordReset(env: FoirEnv, email: string, options?: {
146
+ clientId?: string;
147
+ }): Promise<boolean>;
126
148
  /**
127
149
  * Redeem an emailed reset token and set a new password. Returns a fresh session
128
150
  * (resetting a password signs the user in), so pair it with
@@ -132,8 +154,6 @@ export declare function resetPassword(env: FoirEnv, vars: {
132
154
  token: string;
133
155
  newPassword: string;
134
156
  }): Promise<AuthResult>;
135
- /** Redeem an emailed verification token. */
136
- export declare function verifyEmail(env: FoirEnv, token: string): Promise<boolean>;
137
157
  /**
138
158
  * Change the calling user's password. Requires the current password as well
139
159
  * as the bearer, so a stolen token alone cannot take the account over.
@@ -148,14 +168,25 @@ export declare function updatePassword(env: FoirEnv, vars: {
148
168
  */
149
169
  export declare function currentUser(env: FoirEnv, options?: AuthOpOptions): Promise<CurrentUser | null>;
150
170
  /**
151
- * The stored upstream provider token for the calling user, refreshed
152
- * transparently by the server when it has expired. `null` when there is no
153
- * user token on the request or none stored for that provider.
171
+ * The token of the provider the calling user signed in through, kept for the
172
+ * sign-in the request belongs to and renewed by the server when it is due:
173
+ * near its stated expiry, or with no expiry stated. `null` when there is no
174
+ * user token on the request; `linked: false` with `reauthenticate: true` when
175
+ * nothing is kept for this sign-in (it did not pass through the provider, or
176
+ * its token was ended), and the user signs in through the provider again.
177
+ *
178
+ * Pass `forceRefresh` after the provider refused the token it was last given:
179
+ * the server renews it then, at most once a minute for one user and provider.
180
+ * `refresh` says what the read did, and `reauthenticate` says when only the
181
+ * user signing in with the provider again mends the connection.
154
182
  */
155
- export declare function myProviderToken(env: FoirEnv, providerKey: string, options?: AuthOpOptions): Promise<ProviderToken | null>;
183
+ export declare function myProviderToken(env: FoirEnv, providerKey: string, options?: AuthOpOptions & {
184
+ forceRefresh?: boolean;
185
+ }): Promise<ProviderToken | null>;
156
186
  /**
157
- * End the session server-side, revoking stored provider tokens. This does NOT
158
- * clear local session state — {@link signOut} is the pairing that does both.
187
+ * End the sign-in's provider tokens server-side: this device's, and no other
188
+ * device's. This does NOT clear local session state — {@link signOut} is the
189
+ * pairing that does both.
159
190
  */
160
191
  export declare function logout(env: FoirEnv, options?: AuthOpOptions): Promise<boolean>;
161
192
  /**
@@ -275,8 +306,9 @@ export declare function makeRecoveryCodes(env: FoirEnv, elevatedHandle: string,
275
306
  * Answered from the calling user's own memberships: each organization they
276
307
  * hold one in and every organization beneath it, so the list is exactly the
277
308
  * set a request of theirs may name, and a user with no memberships sees an
278
- * empty list rather than a refusal. Requires the user's bearer; it is not the
279
- * project's inventory and cannot be used as one.
309
+ * empty list rather than a refusal. Requires the user's bearer. A user whose
310
+ * role holds `organizations:read` in the project is listed every organization
311
+ * of it instead, suspended ones included.
280
312
  *
281
313
  * A MEMBERSHIP REACHES DOWNWARD. Someone who is a member of an organization may
282
314
  * stand in any organization beneath it without holding a row of their own
@@ -295,21 +327,27 @@ export declare function myOrganizations(env: FoirEnv, vars?: {
295
327
  /**
296
328
  * Found an organization, as its first member.
297
329
  *
298
- * The caller decides the NAME and, optionally, where it hangs — and nothing
299
- * else. The founder is the user behind the bearer, the id is minted
330
+ * The caller decides the NAME and, optionally, where it hangs and where it
331
+ * lives — and nothing else. The founder is the user behind the bearer, the id is minted
300
332
  * server-side the way a project's and a tenant's are, and the role they hold in
301
333
  * the new organization is the one the PROJECT named in its settings
302
334
  * (`organizationSignupRole`). The server creates the organization and that one
303
335
  * membership together.
304
336
  *
305
337
  * `parentId` FOUNDS A CHILD, which is what lets the people you serve create their own
306
- * sub-tenants with no administrator in the loop — a reseller adding a client, a
338
+ * sub-tenants with no manager in the loop — a reseller adding a client, a
307
339
  * franchise adding a branch. It is resolved against the caller's own
308
340
  * MEMBERSHIPS, not merely against the project: on this lane "an organization
309
341
  * that exists" is not the question, "one you operate" is, and anything else is
310
342
  * not-found. Omit it to found a root, which is what every registration did
311
343
  * before organizations nested.
312
344
  *
345
+ * `appBaseUrl` is where the organization's app lives, so an app that finds its
346
+ * organization from the page address (`orgUrl`) finds this one at once. It is
347
+ * held to the rule every organization address is: under the project's own app
348
+ * address or a domain the project verified, and not one another organization
349
+ * of the project holds.
350
+ *
313
351
  * Exists on the wire only while the project's `organizationSignupEnabled` setting
314
352
  * is on; a project that never opted in has no such field, and this refuses
315
353
  * naming it. Returns the same entry `myOrganizations` would then list, role
@@ -318,6 +356,7 @@ export declare function myOrganizations(env: FoirEnv, vars?: {
318
356
  export declare function registerOrganization(env: FoirEnv, vars: {
319
357
  name: string;
320
358
  parentId?: string;
359
+ appBaseUrl?: string;
321
360
  }, options?: AuthOpOptions): Promise<MyOrganization>;
322
361
  /**
323
362
  * Persist an auth result to the session; returns the signed-in user.
@@ -1,12 +1,13 @@
1
1
  export type GrantReach = 'own' | 'named' | 'all';
2
+ export type GrantTarget = 'none' | 'model' | 'operation';
2
3
  /**
3
- * One permission: its name, whether a grant of it may name a target, the
4
- * reaches it may be granted at (none: only its bare form exists), and the
5
- * grants a holder of it also holds.
4
+ * One permission: its name, what a grant of it may name, the reaches it may be
5
+ * granted at (none: only its bare form exists), and the grants a holder of it
6
+ * also holds.
6
7
  */
7
8
  export type GrantTableRow = readonly [
8
9
  name: string,
9
- takesTarget: boolean,
10
+ target: GrantTarget,
10
11
  reaches: readonly GrantReach[],
11
12
  implies: readonly string[]
12
13
  ];
@@ -21,6 +21,11 @@ export interface HostedLoginConfig {
21
21
  sessionPath?: string;
22
22
  /** RP-initiated logout path on the front door. Default `/session/end-session`. */
23
23
  endSessionPath?: string;
24
+ /**
25
+ * The app the bearer is for, when it is not the app the login domain fronts.
26
+ * Default: the domain's own app.
27
+ */
28
+ clientId?: string;
24
29
  }
25
30
  /**
26
31
  * Build the redirect URL to the hosted front door, forwarding a best-effort
@@ -127,3 +132,27 @@ export type HostedSessionResult =
127
132
  * returning `null` for everything that is not a success.
128
133
  */
129
134
  export declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSessionResult>;
135
+ /**
136
+ * What the hosted login said beside the code when it sent someone back to your
137
+ * app. A sign-in it refused carries a stable code (`foir_error`); one that
138
+ * connected a provider to the person's account on the way in names the
139
+ * provider (`foir_connected`). Each comes with a sentence your app can show as
140
+ * it is, so an app that does nothing more still shows words and never a code.
141
+ * Show your own words for a code by switching on `code`.
142
+ */
143
+ export type HostedSignInNotice = {
144
+ kind: 'refused';
145
+ code: string;
146
+ message: string;
147
+ } | {
148
+ kind: 'connected';
149
+ provider: string;
150
+ message: string;
151
+ };
152
+ /**
153
+ * Reads the notice off the address the hosted login sent the browser back to:
154
+ * your callback URL, its search string, or its parameters. `names` maps a
155
+ * provider's key to the name to show (from `getAuthProviders`); without it the
156
+ * key is shown. Null when the hosted login said nothing.
157
+ */
158
+ export declare function hostedSignInNoticeOf(location: string | URL | URLSearchParams, names?: Record<string, string>): HostedSignInNotice | null;
@@ -10,15 +10,15 @@
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, confirmProviderLink, resendProviderLinkCode, getAuthProviders, getAuthConfig, getDefaultAuthProvider, signupContext, joinWaitlist, requestPasswordReset, resetPassword, updatePassword, verifyEmail, currentUser, myProviderToken, logout, deleteMyAccount, updateMyProfile, myAuthorizedApps, revokeAuthorizedApp, mySessions, endMySession, signOutEverywhere, createPersonalAccessToken, mySignInMethods, connectSignInMethod, disconnectSignInMethod, myFactors, beginStepUp, completeStepUp, beginFactorEnrollment, completeFactorEnrollment, removeFactor, makeRecoveryCodes, myOrganizations, 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, updateMyProfile, myAuthorizedApps, revokeAuthorizedApp, mySessions, endMySession, signOutEverywhere, createPersonalAccessToken, mySignInMethods, connectSignInMethod, disconnectSignInMethod, myFactors, beginStepUp, completeStepUp, beginFactorEnrollment, completeFactorEnrollment, removeFactor, makeRecoveryCodes, myOrganizations, 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';
17
17
  export { FoirAuthDowngradedError, FoirSessionExpiredError } from '../graphql/foir-request';
18
18
  export { AUTH_OPS } from './ops';
19
19
  export type { AuthOpName, AuthOpOptions } from './ops';
20
- export { hostedLoginUrl, hostedLogoutUrl, fetchHostedSession } from './hosted';
21
- export type { HostedLoginConfig, HostedSession, HostedSessionResult } from './hosted';
20
+ export { hostedLoginUrl, hostedLogoutUrl, fetchHostedSession, hostedSignInNoticeOf } from './hosted';
21
+ export type { HostedLoginConfig, HostedSession, HostedSessionResult, HostedSignInNotice } from './hosted';
22
22
  export { createFoirSessionClient } from './session-client';
23
23
  export type { FoirSessionClient, FoirSessionConfig, FoirSessionUser, } from './session-client';
24
- export type { AuthResult, ProviderLoginResult, AuthProviderPublic, ProviderLoginInput, ProviderCallbackInput, ProviderOTPVerifyInput, SignupContext, SignupMode, WaitlistJoinResult, AuthConfig, AuthConfigProvider, DefaultAuthProvider, CurrentUser, ProviderToken, DeleteAccountResult, MyOrganization, MyProfile, MyProfileChange, AuthorizedApp, MySession, NewPersonalAccessToken, MySignInMethod, MyFactor, StepUp, FactorEnrollment, } from './operations';
24
+ export type { AuthResult, ProviderLoginResult, AuthProviderPublic, ProviderLoginInput, ProviderCallbackInput, ProviderOTPVerifyInput, SignupContext, SignupMode, AuthConfig, AuthConfigProvider, DefaultAuthProvider, CurrentUser, ProviderToken, ProviderTokenRefresh, DeleteAccountResult, MyOrganization, MyProfile, MyProfileChange, AuthorizedApp, MySession, NewPersonalAccessToken, MySignInMethod, MyFactor, StepUp, FactorEnrollment, } from './operations';
@@ -76,13 +76,6 @@ export type SignupMode = 'PUBLIC' | 'RESTRICTED' | 'WAITLIST' | 'CLOSED';
76
76
  export interface SignupContext {
77
77
  signupMode: SignupMode;
78
78
  }
79
- /** The queued (or existing pending) entry a waitlist join returns. */
80
- export interface WaitlistJoinResult {
81
- id: string;
82
- email: string;
83
- /** Waitlist lifecycle status; PENDING on a fresh join. */
84
- status: string;
85
- }
86
79
  /** One provider as it appears inside {@link AuthConfig}. */
87
80
  export interface AuthConfigProvider {
88
81
  key: string;
@@ -123,18 +116,39 @@ export interface CurrentUser {
123
116
  /** ISO-8601. */
124
117
  createdAt: string;
125
118
  }
119
+ /** What a provider token read did about the token's freshness. */
120
+ export type ProviderTokenRefresh = 'NOT_ATTEMPTED' | 'REFRESHED' | 'FAILED' | 'HELD_BACK';
126
121
  /**
127
- * A stored upstream provider token for the calling user. `null` when there
128
- * is no user token on the request, or none stored for that provider.
122
+ * The token of the provider the calling user signed in through. It belongs to
123
+ * the sign-in that obtained it: a sign-in on another device keeps its own, and
124
+ * signing out ends it. `myProviderToken` answers `null` when there is no user
125
+ * token on the request, and `linked: false` with `reauthenticate: true` when
126
+ * nothing is kept for this sign-in.
129
127
  */
130
128
  export interface ProviderToken {
131
129
  providerKey: string;
132
- accessToken: string;
130
+ /** False when nothing is kept for the sign-in the request belongs to; every token field is then null. */
131
+ linked: boolean;
132
+ accessToken: string | null;
133
133
  refreshToken: string | null;
134
134
  /** ISO-8601. */
135
135
  expiresAt: string | null;
136
136
  /** Space-separated provider scopes, as the provider issued them. */
137
137
  scopes: string | null;
138
+ /**
139
+ * What the read did: `REFRESHED` renewed it, `FAILED` tried and served the
140
+ * stored token, `HELD_BACK` had tried within the last minute, and
141
+ * `NOT_ATTEMPTED` found it fresh or had nothing to renew it with.
142
+ */
143
+ refresh: ProviderTokenRefresh | null;
144
+ /** Where a failed refresh failed, such as `"refresh grant"`. */
145
+ refreshFailure: string | null;
146
+ /**
147
+ * Only the user signing in with the provider again mends the connection:
148
+ * nothing is kept for this sign-in, or the provider will not renew it. Show
149
+ * them the way to sign in through the provider, not an empty page.
150
+ */
151
+ reauthenticate: boolean;
138
152
  }
139
153
  /** What a self-serve account deletion removed. */
140
154
  export interface DeleteAccountResult {
@@ -205,7 +219,10 @@ export interface MyFactor {
205
219
  kind: string;
206
220
  /** What {@link removeFactor} takes. */
207
221
  id: string;
208
- /** A security key's or passkey's name. */
222
+ /**
223
+ * A security key's or passkey's name. A passkey made on the project's own
224
+ * login domain is named by that domain, the only one it signs in on.
225
+ */
209
226
  name: string | null;
210
227
  /** ISO-8601. */
211
228
  addedAt: string | null;
@@ -314,6 +331,12 @@ export interface MyOrganization {
314
331
  * sees part of what the next request is allowed and not the whole of it.
315
332
  */
316
333
  role: MyOrganizationRole | null;
334
+ /**
335
+ * Where the list goes on from: pass the last entry's cursor as `after` to
336
+ * read the next page. Null on an entry no list gave, such as the one
337
+ * `registerOrganization` returns.
338
+ */
339
+ cursor: string | null;
317
340
  }
318
341
  /** A role as `myOrganizations` reports it: enough to match a policy and to render. */
319
342
  export interface MyOrganizationRole {
@@ -55,7 +55,6 @@ export declare const AUTH_OPS: {
55
55
  readonly requestPasswordReset: "requestPasswordReset";
56
56
  readonly resetPassword: "resetPassword";
57
57
  readonly updatePassword: "updatePassword";
58
- readonly verifyEmail: "verifyEmail";
59
58
  readonly deleteMyAccount: "deleteMyAccount";
60
59
  readonly updateMyProfile: "updateMyProfile";
61
60
  readonly myAuthorizedApps: "myAuthorizedApps";
@@ -37,6 +37,11 @@ export interface FoirSessionConfig {
37
37
  sessionPath?: string;
38
38
  /** Logout path. Default `/session/logout`. */
39
39
  logoutPath?: string;
40
+ /**
41
+ * The app the bearer is for, when it is not the app the login domain fronts.
42
+ * Default: the domain's own app.
43
+ */
44
+ clientId?: string;
40
45
  /** Seconds before expiry to re-exchange. Default 60. */
41
46
  refreshSkewSeconds?: number;
42
47
  /**
@@ -5,8 +5,9 @@ var DEFAULT_REFRESH_SKEW_SECONDS = 60;
5
5
  var MIN_REFRESH_DELAY_MS = 5e3;
6
6
  var CSRF_HEADER = { "X-Foir-Session": "1" };
7
7
  function createFoirSessionClient(config) {
8
- const sessionUrl = `${trimSlash(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH}`;
9
- const logoutUrl = `${trimSlash(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}`;
8
+ const forApp = config.clientId ? `?client_id=${encodeURIComponent(config.clientId)}` : "";
9
+ const sessionUrl = `${trimSlash(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH}${forApp}`;
10
+ const logoutUrl = `${trimSlash(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}${forApp}`;
10
11
  const skewMs = (config.refreshSkewSeconds ?? DEFAULT_REFRESH_SKEW_SECONDS) * 1e3;
11
12
  let cached = null;
12
13
  let inflight = null;