@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.
@@ -59,7 +59,7 @@ export { planFoirModelCall, planFoirNamespaceCall } from './document';
59
59
  export type { FoirCall, FoirDocumentPlan, FoirNamespaceCall, FoirNamespaceSelections, FoirRecordPath, FoirSelectionModel, FoirSelectionNamespace, FoirSelections, FoirSelectionSource, FoirSelectionUnions, } from './document';
60
60
  export { createFoirSubjectRegistry, recordFoirSubjects } from './subjects';
61
61
  export type { FoirSubject, FoirSubjectEntry, FoirSubjectOptions, FoirSubjectOrigin, FoirSubjectRegistry, } from './subjects';
62
- export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirWriteNoteArgs, FoirUserNote, FoirUserNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteSubjectType, FoirPlatformType, FoirRecordOwner, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
62
+ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirWriteNoteArgs, FoirUserNote, FoirUserNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteReceipt, FoirNoteSubjectType, FoirPlatformType, FoirRecordOwner, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
63
63
  /**
64
64
  * Pre-rename spellings. The `<Name>Block` wrappers and the `<Name>Value`
65
65
  * split are the same thing as the types above — an item IS its value — so each
@@ -81,5 +81,5 @@ export type { FoirConnection, FoirFieldsShape, FoirModelDefinition, FoirNamespac
81
81
  export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmittableKey, FoirOpResult, FoirQuery, FoirRecordResult, FoirWriteResult, } from './selection';
82
82
  export { FoirDecimal, decimal } from './decimal';
83
83
  export type { FoirDecimalDivOptions, FoirDecimalInput, FoirDecimalRounding, } from './decimal';
84
- export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, FOIR_VISITOR_IP_HEADER, } from '../transport/foir-http';
85
- export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, FoirVisitor, FoirVisitorSource, } from '../transport/foir-http';
84
+ export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, FOIR_VISITOR_IP_HEADER, stepUpRequired, } from '../transport/foir-http';
85
+ export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, FoirStepUp, FoirVisitor, FoirVisitorSource, } from '../transport/foir-http';
package/dist/data.js CHANGED
@@ -10,6 +10,15 @@ var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
10
10
  "subselections_on_scalar",
11
11
  "missing_subselections"
12
12
  ]);
13
+ function stepUpRequired(error) {
14
+ if (!(error instanceof FoirRequestError)) return null;
15
+ for (const item of error.graphQLErrors ?? []) {
16
+ if (item.extensions?.code !== "STEP_UP_REQUIRED") continue;
17
+ const asked = item.extensions.stepUp;
18
+ if (asked === "second_factor" || asked === "sign_in_method") return asked;
19
+ }
20
+ return null;
21
+ }
13
22
  var FoirRequestError = class extends Error {
14
23
  /** HTTP status code, when the request failed at the HTTP layer. */
15
24
  status;
@@ -2686,5 +2695,6 @@ export {
2686
2695
  planFoirModelCall,
2687
2696
  planFoirNamespaceCall,
2688
2697
  printSubscriptionDocument,
2689
- recordFoirSubjects
2698
+ recordFoirSubjects,
2699
+ stepUpRequired
2690
2700
  };
package/dist/next.js CHANGED
@@ -1010,6 +1010,9 @@ var AUTH_OPS = {
1010
1010
  // names the organization it acts in (`orgId` or `orgUrl` below), and the
1011
1011
  // platform checks the caller stands there every time.
1012
1012
  myOrganizations: "myOrganizations",
1013
+ // Where they may go: the organizations they are a member of, are invited
1014
+ // to, may join, or asked to join. An organization switcher's choices.
1015
+ myOrganizationChoices: "myOrganizationChoices",
1013
1016
  // Founding one. Present in a manifest only when the project opted in.
1014
1017
  registerOrganization: "registerOrganization",
1015
1018
  // External identity providers.
@@ -113,6 +113,23 @@ export declare function placementFrameAncestors(origins: readonly string[]): str
113
113
  export declare class PlacementRefused extends Error {
114
114
  constructor(reason: string);
115
115
  }
116
+ /** One error of a GraphQL answer, as Foir's API sends it. */
117
+ export interface PlacementRequestError {
118
+ message: string;
119
+ path?: (string | number)[];
120
+ extensions?: Record<string, unknown>;
121
+ }
122
+ /**
123
+ * A request the page made on its token that Foir's API refused or failed.
124
+ * The message says each distinct refusal once: a read refused on a field is
125
+ * answered once per row, and one missing grant over five hundred rows would
126
+ * otherwise read five hundred times. `errors` keeps every one, with its path.
127
+ */
128
+ export declare class PlacementRequestFailed extends Error {
129
+ readonly errors: PlacementRequestError[];
130
+ readonly status: number;
131
+ constructor(errors: PlacementRequestError[], status: number);
132
+ }
116
133
  export interface ConnectPlacementOptions {
117
134
  /** Foir's API, e.g. `https://api.foir.dev`. */
118
135
  apiUrl: string;
package/dist/placement.js CHANGED
@@ -41,6 +41,8 @@ var GRANT_TABLE = [
41
41
  ["authclients:write", "none", [], []],
42
42
  ["domains:read", "none", [], []],
43
43
  ["domains:write", "none", [], []],
44
+ ["loginpages:read", "none", [], []],
45
+ ["loginpages:write", "none", [], []],
44
46
  ["secrets:read", "none", [], []],
45
47
  ["secrets:list", "none", [], []],
46
48
  ["secrets:put", "none", [], []],
@@ -78,7 +80,7 @@ var GRANT_TABLE = [
78
80
  ["projects:read", "none", [], []],
79
81
  ["projects:write", "none", [], []],
80
82
  ["project:stand", "none", [], []],
81
- ["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"]],
83
+ ["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"]],
82
84
  ["projects:create", "none", [], []],
83
85
  ["projects:delete", "none", [], []],
84
86
  ["tenants:read", "none", [], []],
@@ -266,6 +268,17 @@ var PlacementRefused = class extends Error {
266
268
  this.name = "PlacementRefused";
267
269
  }
268
270
  };
271
+ var PlacementRequestFailed = class extends Error {
272
+ errors;
273
+ status;
274
+ constructor(errors, status) {
275
+ const said = [...new Set(errors.map((e) => e.message))];
276
+ super(`[foir] placement request failed: ${said.length > 0 ? said.join("; ") : `HTTP ${status}`}`);
277
+ this.name = "PlacementRequestFailed";
278
+ this.errors = errors;
279
+ this.status = status;
280
+ }
281
+ };
269
282
  function isHostMessage(data) {
270
283
  if (typeof data !== "object" || data === null) return false;
271
284
  const t = data.type;
@@ -325,8 +338,7 @@ function connectPlacement(options) {
325
338
  });
326
339
  const body = await res.json().catch(() => ({}));
327
340
  if (!res.ok || body.errors && body.errors.length > 0) {
328
- const messages = body.errors?.map((e) => e.message).join("; ") || `HTTP ${res.status}`;
329
- throw new Error(`[foir] placement request failed: ${messages}`);
341
+ throw new PlacementRequestFailed(body.errors ?? [], res.status);
330
342
  }
331
343
  return body.data;
332
344
  },
@@ -381,6 +393,7 @@ export {
381
393
  FOIR_CONSOLE_ORIGINS,
382
394
  PLACEMENT_MESSAGE,
383
395
  PlacementRefused,
396
+ PlacementRequestFailed,
384
397
  connectPlacement,
385
398
  placementFrameAncestors
386
399
  };
package/dist/push.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.
@@ -1,4 +1,4 @@
1
- import type { FoirUserNote, FoirSubject, FoirSubjectRegistry } from '../data';
1
+ import type { FoirNoteReceipt, FoirUserNote, FoirSubject, FoirSubjectRegistry } from '../data';
2
2
  import { type FoirFeedbackClient } from './feedback-notes';
3
3
  export type { FoirFeedbackClient } from './feedback-notes';
4
4
  export interface FoirFeedbackProps {
@@ -41,12 +41,19 @@ export interface FoirFeedbackProps {
41
41
  /** Start with the panel open. Default false. */
42
42
  defaultOpen?: boolean;
43
43
  /**
44
- * Called when the visitor has to sign in first. Given a handler, the widget
45
- * renders a button that calls it; given none, it says so and stops.
44
+ * Called when the visitor has to sign in first: the project takes no notes
45
+ * from visitors who are not signed in. Given a handler, the widget renders a
46
+ * button that calls it; given none, it says so and stops.
46
47
  */
47
48
  onSignIn?: () => void;
48
- /** Called with the note once it is written. */
49
+ /** Called with a signed-in user's note once it is written. */
49
50
  onSubmitted?: (note: FoirUserNote) => void;
51
+ /**
52
+ * Called with the receipt once a visitor who is not signed in has sent a
53
+ * note: its id and when it was written, which is all a visitor's note
54
+ * answers with.
55
+ */
56
+ onVisitorSubmitted?: (receipt: FoirNoteReceipt) => void;
50
57
  /**
51
58
  * Called with anything that failed. The widget also logs it — this is for
52
59
  * routing it into your own error reporting, not for making it visible.
@@ -59,4 +66,4 @@ export interface FoirFeedbackProps {
59
66
  */
60
67
  className?: string;
61
68
  }
62
- export declare function FoirFeedback({ client, subjects, subject, label, triggerLabel, title, placeholder, defaultOpen, onSignIn, onSubmitted, onError, className, }: FoirFeedbackProps): import("react/jsx-runtime").JSX.Element;
69
+ export declare function FoirFeedback({ client, subjects, subject, label, triggerLabel, title, placeholder, defaultOpen, onSignIn, onSubmitted, onVisitorSubmitted, onError, className, }: FoirFeedbackProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { AuthProviderPublic, FoirAccess, FoirUser, FoirSessionConfig, SessionAdapter } from '../auth';
2
+ import type { AuthProviderPublic, FoirAccess, FoirUser, FoirSessionConfig, HostedLoginOptions, SessionAdapter } from '../auth';
3
3
  import type { ProviderCallbackInput, ProviderLoginInput } from '../auth';
4
4
  import type { FoirEnv } from '../graphql/foir-request';
5
5
  /** Where the session hydration stands. */
@@ -53,9 +53,10 @@ export interface FoirContextValue {
53
53
  /**
54
54
  * In `hostedSession` mode, where to send the browser to sign in — sign-in is a
55
55
  * redirect to your Foir login domain, not an in-page form. Null in token mode,
56
- * where you call {@link FoirContextValue.signIn} instead.
56
+ * where you call {@link FoirContextValue.signIn} instead. `options.org` names
57
+ * the organization the sign-in is for.
57
58
  */
58
- hostedSignInUrl: ((returnTo?: string) => string) | null;
59
+ hostedSignInUrl: ((returnTo?: string, options?: HostedLoginOptions) => string) | null;
59
60
  }
60
61
  export interface FoirProviderProps {
61
62
  /** API config: `{ PLATFORM_API_URL, PLATFORM_API_KEY }` (a publishable key). */
@@ -151,5 +152,5 @@ export declare function useFoirAuth(): {
151
152
  refresh: () => Promise<boolean>;
152
153
  getToken: () => Promise<string | null>;
153
154
  getProviders: () => Promise<AuthProviderPublic[]>;
154
- hostedSignInUrl: ((returnTo?: string) => string) | null;
155
+ hostedSignInUrl: ((returnTo?: string, options?: HostedLoginOptions) => string) | null;
155
156
  };
@@ -7,7 +7,7 @@
7
7
  * shape (see `data/selection.ts`) — so every response here has to be read rather
8
8
  * than cast, and reading is the part worth testing on its own.
9
9
  */
10
- import type { FoirUserNote, FoirNoteConfig, FoirSubject } from '../data';
10
+ import type { FoirUserNote, FoirNoteConfig, FoirNoteReceipt, FoirSubject } from '../data';
11
11
  import type { FoirScreenCapture } from '../media/screen-capture';
12
12
  /**
13
13
  * The client the widget needs, and no more of it.
@@ -15,9 +15,9 @@ import type { FoirScreenCapture } from '../media/screen-capture';
15
15
  * Structural rather than `FoirClient<S>`, for two reasons. `FoirClient` is
16
16
  * generic in the caller's generated schema, so naming it here would force the
17
17
  * component's props to be generic in a parameter nothing else reads. And what
18
- * the widget actually depends on is three root fields — narrowing to them means
19
- * a client whose key cannot reach `files` still satisfies this type, which is
20
- * the honest shape: a project with screenshots off never uploads anything.
18
+ * the widget actually depends on is a handful of root fields — narrowing to them
19
+ * means a client whose key cannot reach `files` still satisfies this type, which
20
+ * is the honest shape: a project with screenshots off never uploads anything.
21
21
  *
22
22
  * The signatures are the ones `foir types` emits for every namespace op —
23
23
  * `Record<string, unknown>` in, `unknown` out — so a real generated client
@@ -27,6 +27,14 @@ export interface FoirFeedbackClient {
27
27
  readonly ns: {
28
28
  readonly notes: {
29
29
  noteConfig(args?: Record<string, unknown>): Promise<unknown>;
30
+ /**
31
+ * The one note create, for a signed-in user and a visitor alike. Optional
32
+ * so a client typed before it still satisfies this; without it a
33
+ * signed-in user's note goes through `createMyNote`, and a visitor's
34
+ * cannot be sent.
35
+ */
36
+ writeNote?(args?: Record<string, unknown>): Promise<unknown>;
37
+ /** The older name of `writeNote` for a signed-in user. */
30
38
  createMyNote(args?: Record<string, unknown>): Promise<unknown>;
31
39
  };
32
40
  /**
@@ -77,6 +85,12 @@ export declare function classifyNotesFault(err: unknown): FoirFeedbackFault;
77
85
  * `screenshotsEnabled` to false. Defaulting would be the quiet version of a
78
86
  * project's setting being ignored: the offer would disappear from every surface
79
87
  * at once and the only symptom would be a missing button.
88
+ *
89
+ * `signedIn` and `anonymousAccepted` are read as the older contract when they
90
+ * are absent, which is not a guess: a data API without them answers only a
91
+ * signed-in user, and refuses everyone else. A generated module that predates
92
+ * them selects neither from a data API that has both, and the client's drift
93
+ * check names that module as the thing to regenerate.
80
94
  */
81
95
  export declare function readNoteConfig(client: FoirFeedbackClient): Promise<FoirNoteConfig>;
82
96
  /** What one submission carries. */
@@ -101,3 +115,23 @@ export interface FoirFeedbackSubmission {
101
115
  * they wrote ("as you can see in the screenshot") stops making sense.
102
116
  */
103
117
  export declare function submitFeedbackNote(client: FoirFeedbackClient, submission: FoirFeedbackSubmission): Promise<FoirUserNote>;
118
+ /** What a visitor's submission carries. */
119
+ export interface FoirVisitorSubmission {
120
+ /** What the note is about, as for a signed-in user's. */
121
+ subject: FoirSubject;
122
+ body: string;
123
+ /**
124
+ * Where the project's team may reply, as the visitor typed it. Sent only
125
+ * when it is not blank, and never verified by anyone.
126
+ */
127
+ contactEmail?: string;
128
+ }
129
+ /**
130
+ * Write a visitor's note, and hand back its receipt.
131
+ *
132
+ * It asks for the note's `id` and `createdAt` and nothing else, because that is
133
+ * all a visitor's note answers with: asked for more, the data API refuses the
134
+ * write before anything is written. And it takes no attachment, which a
135
+ * visitor's note cannot carry.
136
+ */
137
+ export declare function submitVisitorNote(client: FoirFeedbackClient, submission: FoirVisitorSubmission): Promise<FoirNoteReceipt>;
package/dist/react.js CHANGED
@@ -1004,6 +1004,9 @@ var AUTH_OPS = {
1004
1004
  // names the organization it acts in (`orgId` or `orgUrl` below), and the
1005
1005
  // platform checks the caller stands there every time.
1006
1006
  myOrganizations: "myOrganizations",
1007
+ // Where they may go: the organizations they are a member of, are invited
1008
+ // to, may join, or asked to join. An organization switcher's choices.
1009
+ myOrganizationChoices: "myOrganizationChoices",
1007
1010
  // Founding one. Present in a manifest only when the project opted in.
1008
1011
  registerOrganization: "registerOrganization",
1009
1012
  // External identity providers.
@@ -1248,6 +1251,8 @@ var GRANT_TABLE = [
1248
1251
  ["authclients:write", "none", [], []],
1249
1252
  ["domains:read", "none", [], []],
1250
1253
  ["domains:write", "none", [], []],
1254
+ ["loginpages:read", "none", [], []],
1255
+ ["loginpages:write", "none", [], []],
1251
1256
  ["secrets:read", "none", [], []],
1252
1257
  ["secrets:list", "none", [], []],
1253
1258
  ["secrets:put", "none", [], []],
@@ -1285,7 +1290,7 @@ var GRANT_TABLE = [
1285
1290
  ["projects:read", "none", [], []],
1286
1291
  ["projects:write", "none", [], []],
1287
1292
  ["project:stand", "none", [], []],
1288
- ["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"]],
1293
+ ["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"]],
1289
1294
  ["projects:create", "none", [], []],
1290
1295
  ["projects:delete", "none", [], []],
1291
1296
  ["tenants:read", "none", [], []],
@@ -1549,10 +1554,11 @@ function createFoirSessionClient(config) {
1549
1554
  forget();
1550
1555
  config.onSignOut?.();
1551
1556
  },
1552
- signInUrl(returnTo) {
1557
+ signInUrl(returnTo, options = {}) {
1553
1558
  const dest = new URL(trimSlash(config.loginOrigin));
1554
1559
  const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
1555
1560
  if (target && target !== "/") dest.searchParams.set("returnTo", target);
1561
+ if (options.org) dest.searchParams.set("org", options.org);
1556
1562
  return dest.toString();
1557
1563
  },
1558
1564
  subscribe(listener) {
@@ -1779,7 +1785,7 @@ function FoirProvider({
1779
1785
  }, [refresh, loadAccess, accessWasRead]);
1780
1786
  const getProviders = useCallback(() => getAuthProviders(envRef.current), []);
1781
1787
  const hostedSignInUrl = useMemo(
1782
- () => hosted ? (returnTo) => hosted.signInUrl(returnTo) : null,
1788
+ () => hosted ? (returnTo, options) => hosted.signInUrl(returnTo, options) : null,
1783
1789
  [hosted]
1784
1790
  );
1785
1791
  const value = useMemo(
@@ -2243,7 +2249,14 @@ async function readNoteConfig(client) {
2243
2249
  "[foir] noteConfig answered without a boolean `screenshotsEnabled`. The widget reads it to decide whether to offer screenshot capture, and will not guess."
2244
2250
  );
2245
2251
  }
2246
- return { screenshotsEnabled };
2252
+ const signedIn = result?.signedIn ?? true;
2253
+ const anonymousAccepted = result?.anonymousAccepted ?? false;
2254
+ if (typeof signedIn !== "boolean" || typeof anonymousAccepted !== "boolean") {
2255
+ throw new Error(
2256
+ "[foir] noteConfig answered `signedIn` or `anonymousAccepted` with something other than a boolean. The widget reads them to decide whether this visitor writes as themselves, and will not guess."
2257
+ );
2258
+ }
2259
+ return { screenshotsEnabled, anonymousAccepted, signedIn };
2247
2260
  }
2248
2261
  async function submitFeedbackNote(client, submission) {
2249
2262
  const attachmentFileIds = [];
@@ -2252,14 +2265,42 @@ async function submitFeedbackNote(client, submission) {
2252
2265
  await uploadFeedbackAttachment(client, submission.screenshot, submission.fetch)
2253
2266
  );
2254
2267
  }
2255
- const args = {
2268
+ const args = noteArgs(submission.subject, submission.body);
2269
+ if (attachmentFileIds.length > 0) args.attachmentFileIds = attachmentFileIds;
2270
+ const { notes } = client.ns;
2271
+ return readCreatedNote(
2272
+ await (notes.writeNote ? notes.writeNote(args) : notes.createMyNote(args))
2273
+ );
2274
+ }
2275
+ async function submitVisitorNote(client, submission) {
2276
+ const { notes } = client.ns;
2277
+ if (!notes.writeNote) {
2278
+ throw new Error(
2279
+ "[foir] this client has no `notes.writeNote`, which is the only create that takes a note from a visitor who is not signed in. Re-run `foir types`."
2280
+ );
2281
+ }
2282
+ const args = noteArgs(submission.subject, submission.body);
2283
+ const contactEmail = submission.contactEmail?.trim();
2284
+ if (contactEmail) args.contactEmail = contactEmail;
2285
+ args.pick = ["id", "createdAt"];
2286
+ const result = await notes.writeNote(args);
2287
+ if (!result || typeof result.id !== "string" || result.id === "") {
2288
+ throw new Error(
2289
+ "[foir] writeNote answered a visitor with no receipt. Nothing was saved \u2014 if it had been, the surface would have returned its id."
2290
+ );
2291
+ }
2292
+ return {
2293
+ id: result.id,
2294
+ createdAt: typeof result.createdAt === "string" ? result.createdAt : ""
2295
+ };
2296
+ }
2297
+ function noteArgs(subject, body) {
2298
+ return {
2256
2299
  subjectType: "record",
2257
- subjectId: submission.subject.recordId,
2258
- subjectModelKey: submission.subject.modelKey,
2259
- body: submission.body
2300
+ subjectId: subject.recordId,
2301
+ subjectModelKey: subject.modelKey,
2302
+ body
2260
2303
  };
2261
- if (attachmentFileIds.length > 0) args.attachmentFileIds = attachmentFileIds;
2262
- return readCreatedNote(await client.ns.notes.createMyNote(args));
2263
2304
  }
2264
2305
  async function uploadFeedbackAttachment(client, screenshot, fetchImpl = globalThis.fetch) {
2265
2306
  const files = client.ns.files;
@@ -2311,7 +2352,7 @@ function readCreatedNote(result) {
2311
2352
  const note = result;
2312
2353
  if (!note || typeof note.id !== "string" || note.id === "") {
2313
2354
  throw new Error(
2314
- "[foir] createMyNote answered with no note. Nothing was saved \u2014 if it had been, the surface would have returned it."
2355
+ "[foir] writeNote answered with no note. Nothing was saved \u2014 if it had been, the surface would have returned it."
2315
2356
  );
2316
2357
  }
2317
2358
  return {
@@ -2361,6 +2402,8 @@ var COPY = {
2361
2402
  loading: "One moment\u2026",
2362
2403
  sendFailed: "Your feedback could not be sent. Please try again.",
2363
2404
  sent: "Thanks. Your feedback has been sent.",
2405
+ reference: "Reference",
2406
+ contactEmail: "Your email, if you would like a reply (optional)",
2364
2407
  send: "Send feedback",
2365
2408
  sending: "Sending\u2026",
2366
2409
  capture: "Add a screenshot",
@@ -2397,6 +2440,13 @@ var textareaStyle = {
2397
2440
  minHeight: 96,
2398
2441
  resize: "vertical"
2399
2442
  };
2443
+ var inputStyle2 = {
2444
+ padding: "8px 10px",
2445
+ borderRadius: 6,
2446
+ border: "1px solid currentColor",
2447
+ background: "transparent",
2448
+ font: "inherit"
2449
+ };
2400
2450
  var headingStyle = { margin: 0, fontSize: 15, fontWeight: 600 };
2401
2451
  var subjectStyle = { margin: 0, fontSize: 13, opacity: 0.75 };
2402
2452
  var noteStyle = { margin: 0, fontSize: 13 };
@@ -2421,12 +2471,15 @@ function FoirFeedback({
2421
2471
  defaultOpen = false,
2422
2472
  onSignIn,
2423
2473
  onSubmitted,
2474
+ onVisitorSubmitted,
2424
2475
  onError,
2425
2476
  className
2426
2477
  }) {
2427
2478
  const [open, setOpen] = useState4(defaultOpen);
2428
2479
  const [probe, setProbe] = useState4({ status: "loading" });
2429
2480
  const [body, setBody] = useState4("");
2481
+ const [contactEmail, setContactEmail] = useState4("");
2482
+ const [receipt, setReceipt] = useState4(null);
2430
2483
  const [screenshot, setScreenshot] = useState4(null);
2431
2484
  const [capturing, setCapturing] = useState4(false);
2432
2485
  const [busy, setBusy] = useState4(false);
@@ -2445,7 +2498,11 @@ function FoirFeedback({
2445
2498
  try {
2446
2499
  const config = await readNoteConfig(client);
2447
2500
  if (!cancelled) {
2448
- setProbe({ status: "ready", screenshotsEnabled: config.screenshotsEnabled });
2501
+ setProbe({
2502
+ status: "ready",
2503
+ screenshotsEnabled: config.screenshotsEnabled,
2504
+ signedIn: config.signedIn
2505
+ });
2449
2506
  }
2450
2507
  } catch (err) {
2451
2508
  if (cancelled) return;
@@ -2476,19 +2533,32 @@ function FoirFeedback({
2476
2533
  const onSubmit = async (event) => {
2477
2534
  event.preventDefault();
2478
2535
  const prose = body.trim();
2479
- if (!resolved || !prose || busy) return;
2536
+ if (!resolved || !prose || busy || probe.status !== "ready") return;
2480
2537
  setBusy(true);
2481
2538
  setFailure(null);
2482
2539
  try {
2483
- const note = await submitFeedbackNote(client, {
2484
- subject: resolved,
2485
- body: prose,
2486
- screenshot
2487
- });
2488
- setBody("");
2489
- setScreenshot(null);
2490
- setSent(true);
2491
- onSubmitted?.(note);
2540
+ if (probe.signedIn) {
2541
+ const note = await submitFeedbackNote(client, {
2542
+ subject: resolved,
2543
+ body: prose,
2544
+ screenshot
2545
+ });
2546
+ setBody("");
2547
+ setScreenshot(null);
2548
+ setSent(true);
2549
+ onSubmitted?.(note);
2550
+ } else {
2551
+ const written = await submitVisitorNote(client, {
2552
+ subject: resolved,
2553
+ body: prose,
2554
+ contactEmail
2555
+ });
2556
+ setBody("");
2557
+ setContactEmail("");
2558
+ setReceipt(written);
2559
+ setSent(true);
2560
+ onVisitorSubmitted?.(written);
2561
+ }
2492
2562
  } catch (err) {
2493
2563
  const fault = classifyNotesFault(err);
2494
2564
  reportFault(fault, err, "sending feedback failed");
@@ -2504,6 +2574,7 @@ function FoirFeedback({
2504
2574
  return;
2505
2575
  }
2506
2576
  setSent(false);
2577
+ setReceipt(null);
2507
2578
  setFailure(null);
2508
2579
  setOpen(true);
2509
2580
  };
@@ -2564,11 +2635,28 @@ function FoirFeedback({
2564
2635
  disabled: busy,
2565
2636
  onChange: (event) => {
2566
2637
  setBody(event.target.value);
2567
- if (sent) setSent(false);
2638
+ if (sent) {
2639
+ setSent(false);
2640
+ setReceipt(null);
2641
+ }
2568
2642
  },
2569
2643
  style: styled ? textareaStyle : void 0
2570
2644
  }
2571
2645
  ),
2646
+ !probe.signedIn && /* @__PURE__ */ jsx5(
2647
+ "input",
2648
+ {
2649
+ type: "email",
2650
+ "data-foir-feedback-contact": true,
2651
+ "aria-label": COPY.contactEmail,
2652
+ placeholder: COPY.contactEmail,
2653
+ autoComplete: "email",
2654
+ value: contactEmail,
2655
+ disabled: busy,
2656
+ onChange: (event) => setContactEmail(event.target.value),
2657
+ style: styled ? inputStyle2 : void 0
2658
+ }
2659
+ ),
2572
2660
  probe.screenshotsEnabled && isFoirScreenCaptureSupported() && /* @__PURE__ */ jsx5(Fragment2, { children: screenshot ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
2573
2661
  /* @__PURE__ */ jsx5(
2574
2662
  "img",
@@ -2611,6 +2699,11 @@ function FoirFeedback({
2611
2699
  children: COPY.sent
2612
2700
  }
2613
2701
  ),
2702
+ sent && !failure && receipt && /* @__PURE__ */ jsxs4("p", { "data-foir-feedback-receipt": true, style: styled ? subjectStyle : void 0, children: [
2703
+ COPY.reference,
2704
+ ": ",
2705
+ receipt.id
2706
+ ] }),
2614
2707
  /* @__PURE__ */ jsx5(
2615
2708
  "button",
2616
2709
  {
@@ -12,5 +12,5 @@ export type { CallbackClient, CallbackResult } from './callback-client';
12
12
  export { createEventsClient, FoirEventCursorExpiredError, isFoirEventCursorExpired, FOIR_EVENTS_PATH, FOIR_EVENTS_SCOPE, FOIR_EVENTS_RETENTION_DAYS, } from './events-client';
13
13
  export type { FoirEvent, FoirEventActor, FoirEventPage, FoirEventRetention, FoirEventsClient, FoirEventsClientConfig, FoirEventsListParams, FoirEventsStreamParams, FoirEventsSubscribeParams, } from './events-client';
14
14
  export type { FoirSubscription, FoirSubscriptionHandlers } from '../data/ws-transport';
15
- export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, isFoirForbidden, isFoirNotFound, isFoirRateLimited, isFoirUnauthenticated, } from '../transport/foir-http';
15
+ export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, isFoirForbidden, isFoirNotFound, isFoirRateLimited, isFoirUnauthenticated, stepUpRequired, } from '../transport/foir-http';
16
16
  export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem, FoirAuthDowngrade, } from '../transport/foir-http';
package/dist/server.js CHANGED
@@ -115,6 +115,8 @@ var GRANT_TABLE = [
115
115
  ["authclients:write", "none", [], []],
116
116
  ["domains:read", "none", [], []],
117
117
  ["domains:write", "none", [], []],
118
+ ["loginpages:read", "none", [], []],
119
+ ["loginpages:write", "none", [], []],
118
120
  ["secrets:read", "none", [], []],
119
121
  ["secrets:list", "none", [], []],
120
122
  ["secrets:put", "none", [], []],
@@ -152,7 +154,7 @@ var GRANT_TABLE = [
152
154
  ["projects:read", "none", [], []],
153
155
  ["projects:write", "none", [], []],
154
156
  ["project:stand", "none", [], []],
155
- ["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"]],
157
+ ["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"]],
156
158
  ["projects:create", "none", [], []],
157
159
  ["projects:delete", "none", [], []],
158
160
  ["tenants:read", "none", [], []],
@@ -697,6 +699,15 @@ function continueRun(continuation) {
697
699
  }
698
700
 
699
701
  // src/transport/foir-http.ts
702
+ function stepUpRequired(error) {
703
+ if (!(error instanceof FoirRequestError)) return null;
704
+ for (const item of error.graphQLErrors ?? []) {
705
+ if (item.extensions?.code !== "STEP_UP_REQUIRED") continue;
706
+ const asked = item.extensions.stepUp;
707
+ if (asked === "second_factor" || asked === "sign_in_method") return asked;
708
+ }
709
+ return null;
710
+ }
700
711
  var FoirRequestError = class extends Error {
701
712
  /** HTTP status code, when the request failed at the HTTP layer. */
702
713
  status;
@@ -1750,6 +1761,7 @@ export {
1750
1761
  recordsReadModels,
1751
1762
  recordsWriteModels,
1752
1763
  requireScopedToken,
1764
+ stepUpRequired,
1753
1765
  verifyScopedToken,
1754
1766
  verifyWebhookSignature
1755
1767
  };