@foir/sdk 0.25.0 → 0.27.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/CHANGELOG.md CHANGED
@@ -108,6 +108,90 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
108
108
 
109
109
  Changes on `dev` that have not been published to npm.
110
110
 
111
+ ## [0.27.0] — 2026-10-20
112
+
113
+ ### Added
114
+
115
+ - **`organizations.startPool({ movePeople, dryRun })`** (`@foir/sdk/app`):
116
+ starting the run's organization's own pool takes its people along when
117
+ asked. Somebody who stands only there moves whole, and somebody who also
118
+ stands elsewhere is given a new user there with their memberships, roles and
119
+ what they own there. `dryRun` counts what would move and changes nothing.
120
+ The `AppOrganizationPoolMove` and `AppOrganizationPoolStart` types.
121
+ - **`stepUpRequired(error)`** and the `STEP_UP_REQUIRED` error code: what an
122
+ organization's sign-in rules ask of the signed-in user before a request
123
+ standing there can succeed, `second_factor` or `sign_in_method`.
124
+ - **`myOrganizationChoices`**: the organizations the signed-in user can enter,
125
+ as an organization switcher offers them: member, invited, may join or
126
+ requested, each marked available or not, and whether they stand at the
127
+ project. Requires the user's bearer.
128
+ - **`organizations.provision({ joining })`** (`@foir/sdk/app`): who may join
129
+ the new organization without an invitation. `'invitation'` takes only
130
+ invitations and staff adding people, `'open'` lets someone who can already
131
+ sign in at its page join by signing in there, and `'request'` makes signing
132
+ in there a request its staff approve or decline. Left out, the nearest
133
+ organization above that chose, then the project.
134
+ - **`organizations.provision({ users })`** (`@foir/sdk/app`): where somebody
135
+ who signs up at the new organization stands. `'joined'` makes them a user of
136
+ that organization, and `'everyone'` a user of the project, so every
137
+ organization that shares its sign-in can find, email and sign them in. Left
138
+ out, the nearest organization above that chose, then the project.
139
+ - **`<FoirFeedback>` takes a note from a visitor who is not signed in**
140
+ (`@foir/sdk/react`), on a project that takes visitors' notes ("Accept
141
+ anonymous notes") through a key that gives them `notes:create`. The visitor
142
+ gets the composer rather than a sign-in prompt, with an optional email to
143
+ reply to, and is shown the note's `id` as a reference. The new
144
+ `onVisitorSubmitted` prop is called with the receipt; `onSubmitted` stays the
145
+ signed-in user's note. A project that does not take visitors' notes still
146
+ asks them to sign in.
147
+ - **`FoirNoteReceipt`** (`@foir/sdk/data`): `{ id, createdAt }`, what
148
+ `writeNote` answers a visitor with.
149
+ - **`FoirNoteConfig.signedIn` and `FoirNoteConfig.anonymousAccepted`**:
150
+ whether the caller writes as a signed-in user, and whether the project takes
151
+ visitors' notes. `screenshotsEnabled` is false for a visitor, whose note
152
+ carries no attachment.
153
+ - **`FoirFeedbackClient` carries an optional `writeNote`**, so a client typed
154
+ before it still satisfies the prop.
155
+
156
+ ### Changed
157
+
158
+ - **`<FoirFeedback>` writes a signed-in user's note through `writeNote`**, the
159
+ one note create, rather than `createMyNote`, its older name. The same note
160
+ on the same data API. A client whose generated module predates `writeNote`
161
+ is refused it, saying to re-run `foir types`; a client typed without it at
162
+ all still writes through `createMyNote`.
163
+ - **Breaking: `organizations.startPool()` answers `{ pool, moved }`**
164
+ (`AppOrganizationPoolStart`): the pool it signs its people in to now, and
165
+ what moved into it by kind. It answered the pool alone.
166
+ - **`organizations.provision({ ownPool: false })` is sent.** It used to be
167
+ dropped, so it read as leaving the choice to the project; it now says the
168
+ organization shares the pool above it, whatever the project chose. Left out,
169
+ it still takes the project's choice for a top-level organization and its
170
+ parent's pool for one beneath another.
171
+
172
+ ## [0.26.0] — 2026-10-19
173
+
174
+ ### Changed
175
+
176
+ - **`session.request` in `@foir/sdk/placement` throws `PlacementRequestFailed`.**
177
+ Its message says each distinct refusal once, where it repeated a read
178
+ refused on a field once per row, and its `errors` keeps every error with its
179
+ `path`. It is still an `Error`, so a caller that reads `message` is
180
+ unaffected.
181
+
182
+ ### Added
183
+
184
+ - **A hosted sign-in can name the organization it is for** (`@foir/sdk/auth`,
185
+ `@foir/sdk/react`): `hostedLoginUrl(config, returnTo, { org })`, a session
186
+ client's `signInUrl(returnTo, { org })` and the provider's
187
+ `hostedSignInUrl(returnTo, { org })`. Use it when one auth client serves an
188
+ address per organization. The login domain's front door passes it on, and
189
+ the sign-in takes that organization's branding, providers and return
190
+ addresses. The platform refuses an organization other than the one the
191
+ domain or app is bound to, one of another project, and one that is not
192
+ active.
193
+ - **`HostedLoginOptions`** (`@foir/sdk/auth`, type).
194
+
111
195
  ## [0.25.0] — 2026-10-18
112
196
 
113
197
  ### Changed
@@ -241,9 +241,26 @@ export interface AppOrganizationProvision {
241
241
  * The organisation signs its own people in, to a pool of its own, apart
242
242
  * from its parent's and the project's. Its descendants share that pool
243
243
  * unless one of them starts another. One that exists already starts one
244
- * with `organizations.startPool`.
244
+ * with `organizations.startPool`. Left out, the project's choice for one at
245
+ * the top, and its parent's pool for one beneath another.
245
246
  */
246
247
  ownPool?: boolean;
248
+ /**
249
+ * Where somebody who signs up at it stands: `'joined'` makes them a user of
250
+ * this organisation, and `'everyone'` a user of the project, so every
251
+ * organisation that shares its sign-in can find, email and sign them in.
252
+ * Left out, the nearest organisation above that chose, then the project.
253
+ * Its first member is a member of it either way.
254
+ */
255
+ users?: 'joined' | 'everyone';
256
+ /**
257
+ * Who may join it without an invitation: `'invitation'` (only an invitation
258
+ * or staff adding them), `'open'` (someone who can already sign in at its
259
+ * page joins by signing in there) or `'request'` (signing in there asks, and
260
+ * its staff approve or decline). Left out, the nearest organisation above
261
+ * that chose, then the project.
262
+ */
263
+ joining?: 'invitation' | 'open' | 'request';
247
264
  }
248
265
  /**
249
266
  * The pool an organisation's people sign in to, and where it comes from: its
@@ -258,6 +275,44 @@ export interface AppOrganizationPool {
258
275
  /** How many people the pool holds, when it is the organisation's own. Null for any other. */
259
276
  people: number | null;
260
277
  }
278
+ /** What starting an organisation's own pool moved, or for a dry run would move, by kind. */
279
+ export interface AppOrganizationPoolMove {
280
+ /** Whether nothing was changed. */
281
+ dryRun: boolean;
282
+ /** People who stood only there, moved whole into the new pool. */
283
+ peopleMoved: number;
284
+ /** People who also stand elsewhere, each given a new user in the new pool. */
285
+ peopleMade: number;
286
+ /** Of those, how many are emailed a link to set up their account. */
287
+ peopleTold: number;
288
+ /** Memberships of the people given a new user, moved to that new user. */
289
+ membershipsMoved: number;
290
+ /** Active roles held in those memberships. */
291
+ rolesMoved: number;
292
+ recordsMoved: number;
293
+ filesMoved: number;
294
+ /** Notes assigned to them there, now assigned to the new user. A note stays its author's. */
295
+ notesReassigned: number;
296
+ notificationsMoved: number;
297
+ /** Their choices of what they are told there. */
298
+ notificationChoicesMoved: number;
299
+ /** The access they were given to single resources there. */
300
+ accessGrantsMoved: number;
301
+ /** Open invitations there, now to the new pool. */
302
+ invitationsMoved: number;
303
+ /** Requests to join from people it no longer signs in, withdrawn. */
304
+ joinRequestsWithdrawn: number;
305
+ /** Apps that now sign in the new pool's people. */
306
+ appsMoved: number;
307
+ /** Sign-ins through those apps of anybody not in the new pool, ended. */
308
+ signInsEnded: number;
309
+ }
310
+ /** Starting an organisation's own pool: the pool it signs its people in to now, and what moved into it. */
311
+ export interface AppOrganizationPoolStart {
312
+ /** Its pool, now its own; for a dry run, the pool it signs in to still. */
313
+ pool: AppOrganizationPool;
314
+ moved: AppOrganizationPoolMove;
315
+ }
261
316
  /**
262
317
  * Who a consent change is for: a user whose id the app holds, or an
263
318
  * address.
@@ -483,9 +538,12 @@ export interface AppClient {
483
538
  };
484
539
  users: {
485
540
  /**
486
- * The user who has this email at the install's level, made when there
487
- * is none. The same user every time for one email. Needs
488
- * `users:create`.
541
+ * The user who has this email in the sign-in the install's place uses,
542
+ * made when nobody has it. The same user every time for one email. One
543
+ * the place does not reach is answered only where its organization lets
544
+ * them in (open, or their verified address at its open domain), joining
545
+ * it first; anybody else is refused without saying who has the address.
546
+ * Needs `users:create`.
489
547
  *
490
548
  * `verified` says whether that address is one the user has proved is
491
549
  * theirs. A user this call makes has not, and neither has one an
@@ -586,17 +644,27 @@ export interface AppClient {
586
644
  pool(): Promise<AppOrganizationPool>;
587
645
  /**
588
646
  * Start the own pool of the organisation this run acts in, which it and
589
- * everything beneath it sign their people in to. Needs
590
- * `organizations:write`. Refused while
591
- * anybody of the pool it signs in to now holds a membership in it or
592
- * beneath it, or an invitation there is open to that pool, and the
593
- * refusal says how many: nobody is moved between pools, so those
594
- * memberships are removed first and those people sign up again in the
595
- * new pool. Its apps, and those beneath it, then sign their people in
596
- * there, and anybody signed in through them is signed out. Safe to run
597
- * again: it answers the pool it has.
647
+ * everything beneath it that shares its pool sign their people in to.
648
+ * Needs `organizations:write`.
649
+ *
650
+ * Without `movePeople`, refused while anybody of the pool it signs in to
651
+ * now holds a membership in it or beneath it, or an invitation there is
652
+ * open to that pool, and the refusal says how many. With it, somebody who
653
+ * stands only there moves whole, with their addresses, sign-ins and
654
+ * sessions, and somebody who also stands elsewhere is given a new user
655
+ * there, with their memberships, roles and what they own there, who
656
+ * proves their address before they sign in; refused while the new pool
657
+ * would hold an address twice. `dryRun` answers what would move and
658
+ * changes nothing.
659
+ *
660
+ * Its apps, and those beneath it, then sign their people in there, and
661
+ * anybody signed in through them who is not in the new pool is signed
662
+ * out. Safe to run again: it answers the pool it has and moves nobody.
598
663
  */
599
- startPool(): Promise<AppOrganizationPool>;
664
+ startPool(options?: {
665
+ movePeople?: boolean;
666
+ dryRun?: boolean;
667
+ }): Promise<AppOrganizationPoolStart>;
600
668
  /**
601
669
  * Drop the own pool of the organisation this run acts in, so it signs its
602
670
  * people in to the pool above it. Needs `organizations:write`. Refused
@@ -21,7 +21,7 @@
21
21
  * The manifest's own messages are on `@foir/sdk/app/contract`.
22
22
  */
23
23
  export { createAppClient } from './client';
24
- export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppConsentSubject, AppUser, AppUserEraseResult, AppUserList, AppUserSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppOperationStart, AppOrganizationPool, AppOrganizationProvision, AppRecord, AppRecordList, AppStartedOperation, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
24
+ export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppConsentSubject, AppUser, AppUserEraseResult, AppUserList, AppUserSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppOperationStart, AppOrganizationPool, AppOrganizationPoolMove, AppOrganizationPoolStart, AppOrganizationProvision, AppRecord, AppRecordList, AppStartedOperation, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
25
25
  export { appUserEmailUnavailableOf, appUserUnlinkedOf, appLookupConflictOf, isAppFieldWithheld } from './errors';
26
26
  export type { AppUserEmailUnavailableReason, AppLookupConflict } from './errors';
27
27
  export { appModelFieldName, appModelTypeName, appSelectorEnumValue } from './naming';
@@ -3,7 +3,7 @@
3
3
  * manifest's hook may subscribe to (an event of the project, that something
4
4
  * emits) is the platform's to say: `validateAppManifest` answers it.
5
5
  */
6
- export declare const APP_HOOK_EVENTS: readonly ["RECORD_CREATED", "RECORD_UPDATED", "RECORD_DELETED", "RECORD_PUBLISHED", "RECORD_UNPUBLISHED", "USER_CREATED", "USER_UPDATED", "USER_DELETED", "USER_MERGED", "SESSION_REVOKED", "OPERATION_COMPLETED", "OPERATION_FAILED", "OPERATION_CANCELLED", "OPERATION_TIMED_OUT", "MEMBERSHIP_GRANTED", "MEMBERSHIP_REVOKED", "MEMBERSHIP_ROLE_CHANGED", "TENANT_PLAN_CHANGED", "TENANT_SUBSCRIPTION_STATUS_CHANGED", "PROJECT_CREATED", "PROJECT_ARCHIVED", "INVITATION_ACCEPTED", "IMPORT_COMPLETED", "MIGRATION_APPLIED", "BULK_UPDATE_COMPLETED", "BULK_PUBLISH_COMPLETED", "USER_PREFERENCE_CHANGED", "ORGANIZATION_CREATED", "ORGANIZATION_UPDATED", "ORGANIZATION_MOVED", "ORGANIZATION_SUSPENDED", "ORGANIZATION_RESUMED", "ORGANIZATION_DELETION_SCHEDULED", "ORGANIZATION_DELETION_CANCELLED", "ORGANIZATION_DELETED", "ORGANIZATION_MEMBER_ADDED", "ORGANIZATION_MEMBER_REMOVED", "ORGANIZATION_MEMBER_ROLE_CHANGED", "PROJECT_REACTIVATED"];
6
+ export declare const APP_HOOK_EVENTS: readonly ["RECORD_CREATED", "RECORD_UPDATED", "RECORD_DELETED", "RECORD_PUBLISHED", "RECORD_UNPUBLISHED", "USER_CREATED", "USER_UPDATED", "USER_DELETED", "USER_MERGED", "SESSION_REVOKED", "OPERATION_COMPLETED", "OPERATION_FAILED", "OPERATION_CANCELLED", "OPERATION_TIMED_OUT", "MEMBERSHIP_GRANTED", "MEMBERSHIP_REVOKED", "MEMBERSHIP_ROLE_CHANGED", "TENANT_PLAN_CHANGED", "TENANT_SUBSCRIPTION_STATUS_CHANGED", "PROJECT_CREATED", "PROJECT_ARCHIVED", "INVITATION_ACCEPTED", "IMPORT_COMPLETED", "MIGRATION_APPLIED", "BULK_UPDATE_COMPLETED", "BULK_PUBLISH_COMPLETED", "USER_PREFERENCE_CHANGED", "ORGANIZATION_CREATED", "ORGANIZATION_UPDATED", "ORGANIZATION_MOVED", "ORGANIZATION_SUSPENDED", "ORGANIZATION_RESUMED", "ORGANIZATION_DELETION_SCHEDULED", "ORGANIZATION_DELETION_CANCELLED", "ORGANIZATION_DELETED", "ORGANIZATION_MEMBER_ADDED", "ORGANIZATION_MEMBER_REMOVED", "ORGANIZATION_MEMBER_ROLE_CHANGED", "PROJECT_REACTIVATED", "JOIN_REQUESTED", "JOIN_REQUEST_APPROVED", "JOIN_REQUEST_DECLINED"];
7
7
  /** An event a hook can name. */
8
8
  export type AppHookEvent = (typeof APP_HOOK_EVENTS)[number];
9
9
  /**
package/dist/app.js CHANGED
@@ -353,8 +353,8 @@ function createAppClient(config) {
353
353
  throw new FoirRequestError(`[foir] ${message}`);
354
354
  };
355
355
  const POOL_FIELDS = "organizationId poolId source sourceOrganizationId people";
356
- async function poolOf(name, document) {
357
- const answer = await field(name, document, {});
356
+ const POOL_MOVE_FIELDS = "dryRun peopleMoved peopleMade peopleTold membershipsMoved rolesMoved recordsMoved filesMoved notesReassigned notificationsMoved notificationChoicesMoved accessGrantsMoved invitationsMoved joinRequestsWithdrawn appsMoved signInsEnded";
357
+ function poolFrom(name, answer) {
358
358
  if (!answer?.poolId || !answer.organizationId || answer.source !== "OWN" && answer.source !== "ANCESTOR" && answer.source !== "PROJECT") {
359
359
  return refuse(`${name} reported no error and answered no pool`);
360
360
  }
@@ -366,6 +366,9 @@ function createAppClient(config) {
366
366
  people: answer.people ?? null
367
367
  };
368
368
  }
369
+ async function poolOf(name, document) {
370
+ return poolFrom(name, await field(name, document, {}));
371
+ }
369
372
  let surface;
370
373
  function served() {
371
374
  surface ??= withFoirRetry(async () => {
@@ -854,7 +857,9 @@ function createAppClient(config) {
854
857
  input.firstMember = { userId: first.userId };
855
858
  }
856
859
  }
857
- if (organization.ownPool === true) input.ownPool = true;
860
+ if (typeof organization.ownPool === "boolean") input.ownPool = organization.ownPool;
861
+ if (organization.users !== void 0) input.users = organization.users;
862
+ if (organization.joining !== void 0) input.joining = organization.joining;
858
863
  const answered = settings === void 0 ? "organizationId status" : "organizationId settingsRecordId status";
859
864
  const answer = await field(
860
865
  "provisionOrganization",
@@ -882,8 +887,18 @@ function createAppClient(config) {
882
887
  async pool() {
883
888
  return poolOf("organizationPool", `query AppOrganizationPool { organizationPool { ${POOL_FIELDS} } }`);
884
889
  },
885
- async startPool() {
886
- return poolOf("startOrganizationPool", `mutation AppStartOrganizationPool { startOrganizationPool { ${POOL_FIELDS} } }`);
890
+ async startPool(options = {}) {
891
+ const asks = `movePeople: ${options.movePeople === true}, dryRun: ${options.dryRun === true}`;
892
+ const answer = await field(
893
+ "startOrganizationPool",
894
+ `mutation AppStartOrganizationPool { startOrganizationPool(${asks}) { pool { ${POOL_FIELDS} } moved { ${POOL_MOVE_FIELDS} } } }`,
895
+ {}
896
+ );
897
+ const pool = poolFrom("startOrganizationPool", answer?.pool);
898
+ if (!answer?.moved) {
899
+ return refuse("startOrganizationPool reported no error and answered nothing of what moved");
900
+ }
901
+ return { pool, moved: answer.moved };
887
902
  },
888
903
  async dropPool() {
889
904
  return poolOf("dropOrganizationPool", `mutation AppDropOrganizationPool { dropOrganizationPool { ${POOL_FIELDS} } }`);
@@ -1218,7 +1233,10 @@ var APP_HOOK_EVENTS = [
1218
1233
  "ORGANIZATION_MEMBER_ADDED",
1219
1234
  "ORGANIZATION_MEMBER_REMOVED",
1220
1235
  "ORGANIZATION_MEMBER_ROLE_CHANGED",
1221
- "PROJECT_REACTIVATED"
1236
+ "PROJECT_REACTIVATED",
1237
+ "JOIN_REQUESTED",
1238
+ "JOIN_REQUEST_APPROVED",
1239
+ "JOIN_REQUEST_DECLINED"
1222
1240
  ];
1223
1241
  var APP_GRANTS = [
1224
1242
  "appconfig:read",
@@ -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 MyAddress, type MyDataExport, type MyFactor, type StepUp, type FactorEnrollment, type MySignInMethod, type NewPersonalAccessToken, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type ProviderToken, type SignupContext } from './operations';
20
+ import { type AuthConfig, type AuthProviderPublic, type CurrentUser, type AuthResult, type DefaultAuthProvider, type AuthorizedApp, type DeleteAccountResult, type MyOrganization, type MyOrganizationChoices, type MyProfile, type MyProfileChange, type MySession, type MyAddress, type MyDataExport, 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 {
@@ -380,6 +380,15 @@ export declare function myOrganizations(env: FoirEnv, vars?: {
380
380
  first?: number;
381
381
  after?: string;
382
382
  }, options?: AuthOpOptions): Promise<MyOrganization[]>;
383
+ /**
384
+ * The organizations the signed-in user can enter, as an organization switcher
385
+ * offers them: those they are a member of, are invited to, may join by signing
386
+ * in there, or asked to join, in their own sign-in. It is theirs alone. An
387
+ * organization that lets them in no way is never named, and one that may not
388
+ * act now is listed with `available: false` rather than left out. Requires the
389
+ * user's bearer.
390
+ */
391
+ export declare function myOrganizationChoices(env: FoirEnv, options?: AuthOpOptions): Promise<MyOrganizationChoices>;
383
392
  /**
384
393
  * Found an organization, as its first member.
385
394
  *
@@ -27,11 +27,22 @@ export interface HostedLoginConfig {
27
27
  */
28
28
  clientId?: string;
29
29
  }
30
+ /** What a sign-in started at the hosted front door is for. */
31
+ export interface HostedLoginOptions {
32
+ /**
33
+ * The organization the person signs in to, when one auth client serves an
34
+ * address per organization. It decides the branding, the pool, and which
35
+ * addresses the person may come back to. The platform refuses an
36
+ * organization other than the one the login domain or app is bound to, one
37
+ * of another project, and one that is not active.
38
+ */
39
+ org?: string;
40
+ }
30
41
  /**
31
- * Build the redirect URL to the hosted front door, forwarding a best-effort
32
- * absolute `returnTo` (honoured if the front door supports it).
42
+ * Build the redirect URL to the hosted front door, forwarding an absolute
43
+ * `returnTo` and, where the app names one, the organization the sign-in is for.
33
44
  */
34
- export declare function hostedLoginUrl(config: HostedLoginConfig, returnTo?: string): string;
45
+ export declare function hostedLoginUrl(config: HostedLoginConfig, returnTo?: string, options?: HostedLoginOptions): string;
35
46
  /**
36
47
  * Build the redirect URL that ENDS the hosted session — the mirror of
37
48
  * `hostedLoginUrl`, and the only thing that actually signs a user out on
@@ -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, 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, 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,7 +18,7 @@ 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 type { HostedLoginConfig, HostedSession, HostedSessionResult, HostedSignInNotice } from './hosted';
21
+ export type { HostedLoginConfig, HostedLoginOptions, 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, AuthConfig, AuthConfigProvider, DefaultAuthProvider, CurrentUser, ProviderToken, ProviderTokenRefresh, DeleteAccountResult, MyOrganization, MyProfile, MyProfileChange, AuthorizedApp, MySession, MyAddress, MyDataExport, 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, MyOrganizationChoice, MyOrganizationChoices, MyProfile, MyProfileChange, AuthorizedApp, MySession, MyAddress, MyDataExport, NewPersonalAccessToken, MySignInMethod, MyFactor, StepUp, FactorEnrollment, } from './operations';
@@ -322,6 +322,38 @@ export interface AuthorizedApp {
322
322
  /** ISO-8601. */
323
323
  grantedAt: string | null;
324
324
  }
325
+ /**
326
+ * An organization the calling user can enter, and how, as
327
+ * `myOrganizationChoices` lists it. In their own sign-in only; an organization
328
+ * that lets them in no way is never named.
329
+ */
330
+ export interface MyOrganizationChoice {
331
+ orgId: string;
332
+ name: string;
333
+ displayName: string | null;
334
+ parentId: string | null;
335
+ /** `member`, `invited`, `may_join` or `requested`. */
336
+ relation: 'member' | 'invited' | 'may_join' | 'requested';
337
+ /** False for an organization that may not act now: suspended, or due to be deleted. */
338
+ available: boolean;
339
+ /** For `may_join`: `open`, signing in there joins; `request`, it asks. */
340
+ joining: 'open' | 'request' | null;
341
+ /** For `may_join`: a verified address of theirs is what lets them. */
342
+ byAddress: boolean;
343
+ /** For `invited`: the invitation waiting for them. */
344
+ invitationId: string | null;
345
+ /** For `requested`: their waiting request. */
346
+ requestId: string | null;
347
+ }
348
+ /** What `myOrganizationChoices` answers. */
349
+ export interface MyOrganizationChoices {
350
+ organizations: Array<MyOrganizationChoice>;
351
+ /**
352
+ * They stand at the project, and so at every organization sharing its
353
+ * sign-in; those are not listed one by one.
354
+ */
355
+ projectMember: boolean;
356
+ }
325
357
  /**
326
358
  * One organization the calling user may stand in, as `myOrganizations`
327
359
  * returns it.
@@ -82,6 +82,7 @@ export declare const AUTH_OPS: {
82
82
  readonly renameFactor: "renameFactor";
83
83
  readonly makeRecoveryCodes: "makeRecoveryCodes";
84
84
  readonly myOrganizations: "myOrganizations";
85
+ readonly myOrganizationChoices: "myOrganizationChoices";
85
86
  readonly registerOrganization: "registerOrganization";
86
87
  readonly loginWithProvider: "loginWithProvider";
87
88
  readonly providerCallback: "providerCallback";
@@ -27,6 +27,7 @@
27
27
  * api.foir.dev but a browser cannot reach them there — the cookie would be
28
28
  * cross-site, and that CORS surface deliberately refuses credentials.
29
29
  */
30
+ import type { HostedLoginOptions } from './hosted';
30
31
  export interface FoirSessionConfig {
31
32
  /**
32
33
  * Your verified custom login domain, e.g. `https://login.acme.com`. Must share
@@ -66,8 +67,12 @@ export interface FoirSessionClient {
66
67
  getUser(): Promise<FoirSessionUser | null>;
67
68
  /** Revoke the session server-side and clear the cookie. Idempotent. */
68
69
  signOut(): Promise<void>;
69
- /** Where to send the browser to sign in. */
70
- signInUrl(returnTo?: string): string;
70
+ /**
71
+ * Where to send the browser to sign in. Name the organization the sign-in is
72
+ * for with `options.org` when one auth client serves an address per
73
+ * organization.
74
+ */
75
+ signInUrl(returnTo?: string, options?: HostedLoginOptions): string;
71
76
  /** Observe session changes. Returns an unsubscribe function. */
72
77
  subscribe(listener: (user: FoirSessionUser | null) => void): () => void;
73
78
  /** Cancel the refresh timer. Call on unmount. */
@@ -113,10 +113,11 @@ function createFoirSessionClient(config) {
113
113
  forget();
114
114
  config.onSignOut?.();
115
115
  },
116
- signInUrl(returnTo) {
116
+ signInUrl(returnTo, options = {}) {
117
117
  const dest = new URL(trimSlash(config.loginOrigin));
118
118
  const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
119
119
  if (target && target !== "/") dest.searchParams.set("returnTo", target);
120
+ if (options.org) dest.searchParams.set("org", options.org);
120
121
  return dest.toString();
121
122
  },
122
123
  subscribe(listener) {
package/dist/auth.js CHANGED
@@ -1130,6 +1130,9 @@ var AUTH_OPS = {
1130
1130
  // names the organization it acts in (`orgId` or `orgUrl` below), and the
1131
1131
  // platform checks the caller stands there every time.
1132
1132
  myOrganizations: "myOrganizations",
1133
+ // Where they may go: the organizations they are a member of, are invited
1134
+ // to, may join, or asked to join. An organization switcher's choices.
1135
+ myOrganizationChoices: "myOrganizationChoices",
1133
1136
  // Founding one. Present in a manifest only when the project opted in.
1134
1137
  registerOrganization: "registerOrganization",
1135
1138
  // External identity providers.
@@ -1371,6 +1374,9 @@ async function makeRecoveryCodes(env, elevatedHandle, options) {
1371
1374
  async function myOrganizations(env, vars, options) {
1372
1375
  return authOp(env, AUTH_OPS.myOrganizations, vars ?? {}, options);
1373
1376
  }
1377
+ async function myOrganizationChoices(env, options) {
1378
+ return authOp(env, AUTH_OPS.myOrganizationChoices, {}, options);
1379
+ }
1374
1380
  async function registerOrganization(env, vars, options) {
1375
1381
  return authOp(env, AUTH_OPS.registerOrganization, vars, options);
1376
1382
  }
@@ -1567,6 +1573,8 @@ var GRANT_TABLE = [
1567
1573
  ["authclients:write", "none", [], []],
1568
1574
  ["domains:read", "none", [], []],
1569
1575
  ["domains:write", "none", [], []],
1576
+ ["loginpages:read", "none", [], []],
1577
+ ["loginpages:write", "none", [], []],
1570
1578
  ["secrets:read", "none", [], []],
1571
1579
  ["secrets:list", "none", [], []],
1572
1580
  ["secrets:put", "none", [], []],
@@ -1604,7 +1612,7 @@ var GRANT_TABLE = [
1604
1612
  ["projects:read", "none", [], []],
1605
1613
  ["projects:write", "none", [], []],
1606
1614
  ["project:stand", "none", [], []],
1607
- ["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", "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"]],
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"]],
1608
1616
  ["projects:create", "none", [], []],
1609
1617
  ["projects:delete", "none", [], []],
1610
1618
  ["tenants:read", "none", [], []],
@@ -1797,11 +1805,14 @@ async function requireScope(adapter, env, scope) {
1797
1805
  var DEFAULT_COOKIE_NAME = "foir_session";
1798
1806
  var DEFAULT_SESSION_PATH = "/session/token";
1799
1807
  var DEFAULT_END_SESSION_PATH = "/session/end-session";
1800
- function hostedLoginUrl(config, returnTo) {
1808
+ function hostedLoginUrl(config, returnTo, options = {}) {
1801
1809
  const dest = new URL(config.loginOrigin);
1802
1810
  if (returnTo && returnTo !== "/") {
1803
1811
  dest.searchParams.set("returnTo", returnTo);
1804
1812
  }
1813
+ if (options.org) {
1814
+ dest.searchParams.set("org", options.org);
1815
+ }
1805
1816
  return dest.toString();
1806
1817
  }
1807
1818
  function hostedLogoutUrl(config, clientId, returnTo) {
@@ -2017,10 +2028,11 @@ function createFoirSessionClient(config) {
2017
2028
  forget();
2018
2029
  config.onSignOut?.();
2019
2030
  },
2020
- signInUrl(returnTo) {
2031
+ signInUrl(returnTo, options = {}) {
2021
2032
  const dest = new URL(trimSlash(config.loginOrigin));
2022
2033
  const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
2023
2034
  if (target && target !== "/") dest.searchParams.set("returnTo", target);
2035
+ if (options.org) dest.searchParams.set("org", options.org);
2024
2036
  return dest.toString();
2025
2037
  },
2026
2038
  subscribe(listener) {
@@ -2082,6 +2094,7 @@ export {
2082
2094
  myAddresses,
2083
2095
  myAuthorizedApps,
2084
2096
  myFactors,
2097
+ myOrganizationChoices,
2085
2098
  myOrganizations,
2086
2099
  myProviderToken,
2087
2100
  mySessions,
@@ -517,22 +517,50 @@ export interface FoirUserNote {
517
517
  createdAt: string;
518
518
  }
519
519
  /**
520
- * What a note surface has to know about the project before it renders anything
521
- * — `foir.ns.notes.noteConfig`.
522
- *
523
- * Charged `notes:create` rather than `notes:read`, and refused to a caller with
524
- * no user session, because the only caller it is useful to is one that can
525
- * submit. So a composer can read it as its single readiness probe: an answer
526
- * means this visitor may write, and a refusal names which half is missing.
520
+ * What a note surface has to know before it renders anything —
521
+ * `foir.ns.notes.noteConfig`.
522
+ *
523
+ * Charged `notes:create` rather than `notes:read`, and answered to exactly the
524
+ * callers `writeNote` takes a note from: a signed-in user, and a visitor with
525
+ * no session on a project that takes visitors' notes, through a key that gives
526
+ * them `notes:create`. Everyone else is refused. So a composer can read it as
527
+ * its single readiness probe: an answer means this caller may write, and says
528
+ * how; a refusal names which half is missing.
527
529
  */
528
530
  export interface FoirNoteConfig {
529
531
  /**
530
- * Whether this project's note surfaces may OFFER to capture a screenshot.
532
+ * Whether this caller's composer may OFFER to capture a screenshot: the
533
+ * project allows it, and the caller is signed in. Always false for a
534
+ * visitor, whose note carries no attachment.
531
535
  *
532
536
  * It gates the offer, not the storing. Capture stays opt-in per submission —
533
537
  * the browser's own picker is the consent gate and it cannot be bypassed.
534
538
  */
535
539
  screenshotsEnabled: boolean;
540
+ /**
541
+ * Whether the project takes notes from visitors who are not signed in
542
+ * (Notes settings → "Accept anonymous notes").
543
+ */
544
+ anonymousAccepted: boolean;
545
+ /**
546
+ * Whether the note this caller writes is a signed-in user's. False is a
547
+ * visitor's: `writeNote` answers it with a {@link FoirNoteReceipt} and nothing
548
+ * more, and it may carry a `contactEmail`.
549
+ */
550
+ signedIn: boolean;
551
+ }
552
+ /**
553
+ * What `writeNote` answers a visitor with: the note's `id` and `createdAt`,
554
+ * and nothing else, because nobody is signed in to read the note back as. Ask
555
+ * for more and the write is refused before anything is written.
556
+ *
557
+ * Show the `id` to the visitor so they can quote it. Nothing on the public
558
+ * plane takes it to read the note.
559
+ */
560
+ export interface FoirNoteReceipt {
561
+ id: string;
562
+ /** ISO-8601. */
563
+ createdAt: string;
536
564
  }
537
565
  /** `myNotes` arguments. Forward-only cursor pagination, like every list read. */
538
566
  export interface FoirMyNotesArgs {
@@ -558,11 +586,8 @@ export type FoirMyNotesResult = FoirConnection<FoirUserNote>;
558
586
  * A signed-in user's note is theirs and comes back in `myNotes`. A
559
587
  * visitor's, through a key that gives `notes:create` to whoever comes through
560
588
  * it, on a project that takes visitors' notes, is nobody's: ask for its `id`
561
- * and `createdAt` and nothing else, because nothing reads it back.
562
- *
563
- * Served by a data API that has the field. `<FoirFeedback>` still writes
564
- * through `createMyNote`, the same create under its older name, which every
565
- * data API serves.
589
+ * and `createdAt` and nothing else (`pick: ['id', 'createdAt']`), because
590
+ * nothing reads it back. What comes back is a {@link FoirNoteReceipt}.
566
591
  */
567
592
  export interface FoirWriteNoteArgs {
568
593
  /** A visitor may write about a record and nothing else. */