@foir/sdk 0.9.0 → 0.10.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +497 -27
  2. package/README.md +111 -0
  3. package/dist/auth/auth.d.ts +72 -1
  4. package/dist/auth/hosted.d.ts +71 -4
  5. package/dist/auth/index.d.ts +3 -3
  6. package/dist/auth/operations.d.ts +84 -0
  7. package/dist/auth/ops.d.ts +10 -0
  8. package/dist/auth.js +127 -19
  9. package/dist/client.js +63 -6
  10. package/dist/codegen/block-registry.d.ts +1 -1
  11. package/dist/codegen/block-scaffold.d.ts +1 -1
  12. package/dist/codegen/index.d.ts +1 -1
  13. package/dist/codegen/item-registry.d.ts +1 -1
  14. package/dist/codegen/item-scaffold.d.ts +1 -1
  15. package/dist/content/content-renderer.d.ts +4 -5
  16. package/dist/content/index.d.ts +1 -1
  17. package/dist/content/richtext.d.ts +1 -1
  18. package/dist/data/builtins.d.ts +64 -12
  19. package/dist/data/client.d.ts +82 -1
  20. package/dist/data/decimal.d.ts +212 -0
  21. package/dist/data/document.d.ts +1 -1
  22. package/dist/data/index.d.ts +10 -8
  23. package/dist/data/manifest.d.ts +9 -1
  24. package/dist/data/schema.d.ts +2 -2
  25. package/dist/data/subscriptions.d.ts +36 -10
  26. package/dist/data/transport.d.ts +120 -3
  27. package/dist/data.js +377 -16
  28. package/dist/graphql/cdn-route.d.ts +1 -1
  29. package/dist/graphql/foir-request.d.ts +1 -1
  30. package/dist/live/coalesce.d.ts +57 -0
  31. package/dist/live/drift.d.ts +59 -0
  32. package/dist/live/events.d.ts +13 -0
  33. package/dist/live/headers.d.ts +53 -0
  34. package/dist/live/index.d.ts +54 -0
  35. package/dist/live/keys.d.ts +72 -0
  36. package/dist/live/manager.d.ts +77 -0
  37. package/dist/live/react/index.d.ts +143 -0
  38. package/dist/live/reconcile.d.ts +82 -0
  39. package/dist/live/resync.d.ts +59 -0
  40. package/dist/live/store.d.ts +134 -0
  41. package/dist/live/tagIndex.d.ts +29 -0
  42. package/dist/live/tanstack.d.ts +127 -0
  43. package/dist/live/writetoken.d.ts +86 -0
  44. package/dist/live-react.js +395 -0
  45. package/dist/live.js +845 -0
  46. package/dist/next/cacheTags.d.ts +245 -0
  47. package/dist/next/index.d.ts +7 -0
  48. package/dist/next.js +179 -6
  49. package/dist/push.js +81 -8
  50. package/dist/react.js +76 -5
  51. package/dist/realtime/index.d.ts +4 -5
  52. package/dist/server/events-client.d.ts +54 -4
  53. package/dist/server/index.d.ts +3 -2
  54. package/dist/server/require-scoped-token.d.ts +1 -1
  55. package/dist/server.js +454 -2
  56. package/dist/transport/foir-http.d.ts +113 -10
  57. package/package.json +10 -2
package/README.md CHANGED
@@ -29,6 +29,8 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
29
29
  | `@foir/sdk/react` | Drop-in React auth — `<FoirProvider>` (token or `hostedSession` mode), `useFoirSession`/`useFoirUser`/`useFoirAuth`, `<SignIn>`, `<OAuthButtons>`, `<UserButton>` | `react`, `graphql` |
30
30
  | `@foir/sdk/next` | Next.js App Router BFF auth — `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `completeFoirProviderLogin`, `createFoirMiddleware` | `graphql` |
31
31
  | `@foir/sdk/server` | Server-side helpers for extension/config API backends — `createServerClient`, `verifyWebhookSignature`, `verifyScopedToken`, callback client | — |
32
+ | `@foir/sdk/live` | Live reads — the client half of Foir's invalidation graph: tag index, raw-payload store, `seq` ordering, record-change → tag mapping | **none** |
33
+ | `@foir/sdk/live/react` | `<FoirLive>` — one socket, refcounted per model, wired to a query client you already have | `react` |
32
34
  | `@foir/sdk/media` | Progressive media — `ProgressiveMedia`, `BlurhashCanvas`, `useProgressiveLoad`, variant-tier adapters | `react` (`hls.js`, `blurhash` optional) |
33
35
  | `@foir/sdk/content` | Content-rendering primitives — `createContentRenderer` block dispatch + Lexical `RichTextRenderer` | `react` |
34
36
  | `@foir/sdk/realtime` | Thin collaboration helper over `y-websocket` — canonical room addressing, presence, token refresh | `yjs`, `y-websocket` |
@@ -129,6 +131,16 @@ Already have `PLATFORM_API_URL` / `PLATFORM_API_KEY` / `FOIR_PREVIEW` wired for
129
131
  the GraphQL path? `createFoirClientFromEnv(env, { generated })` reuses them,
130
132
  including the preview `sk_` key and the draft schema channel.
131
133
 
134
+ A product built on organizations names the one a request acts in with `orgId`,
135
+ which the client sends as `X-Org-Id` on every request. This is for the PROJECT
136
+ credential — the back-office, or the app that has just resolved a subdomain to
137
+ an organization; a storefront keeps using the credential confined to its own
138
+ organization, which `orgId` cannot steer elsewhere:
139
+
140
+ ```ts
141
+ const foir = createFoirClient({ apiUrl, apiKey, generated, orgId });
142
+ ```
143
+
132
144
  ### Typed GraphQL from a server loader
133
145
 
134
146
  `foirRequest` is a typed `fetch` wrapper, not a GraphQL client: it carries the
@@ -305,6 +317,105 @@ function MyEditor() {
305
317
  }
306
318
  ```
307
319
 
320
+ ### Live reads
321
+
322
+ `@foir/sdk/live` binds a cached read to the server's own dependency graph. Every
323
+ cacheable response already tells you which records and collections it depends
324
+ on, and the change stream already tells you when one of them moved; this is the
325
+ join.
326
+
327
+ ```tsx
328
+ import { FoirLive, useFoirLive, useFoirLiveStatus } from '@foir/sdk/live/react';
329
+
330
+ <FoirLive
331
+ queryClient={queryClient}
332
+ subscribe={(model, options, handlers) => foir[model].onChange({ ...handlers, ...options })}
333
+ >
334
+ <ProductList />
335
+ </FoirLive>;
336
+
337
+ function ProductList() {
338
+ useFoirLive('product'); // watch, for as long as this is mounted
339
+ const { newCounts } = useFoirLiveStatus(); // "3 new products — show"
340
+ // ...
341
+ }
342
+ ```
343
+
344
+ An **adapter**, not a store: you keep the query library you have, and it owns
345
+ your data. What this owns is which entries a change invalidates — matched on the
346
+ record, not the model, so one edit does not refetch every list.
347
+
348
+ Live is **opt-in per model**. Nothing subscribes because the provider is
349
+ mounted: the shared CDN lane is a major asset, and blanket liveness pushes read
350
+ traffic off it onto the origin.
351
+
352
+ #### What it is not, and will not become
353
+
354
+ **No local mutation log, no offline mode, no conflict resolution, no optimistic
355
+ rollback.** The server is authoritative: mutations go over the wire and the
356
+ invalidation comes back.
357
+
358
+ That is a product boundary rather than a phase. Adding optimistic mutations with
359
+ rollback would make this a different category of library, with its own roadmap
360
+ and a permanent support surface. If you need one, use one — this is not it, and
361
+ its absence is not a bug.
362
+
363
+ #### Bursts, and the deploy that changes what a response means
364
+
365
+ A bulk import or a publish sweep fires a burst on one model. `<FoirLive>`
366
+ coalesces it: fifty invalidations of one query inside a 25ms window are one
367
+ refetch, at most four refetches run at once, and a query invalidated again
368
+ mid-refetch is owed exactly one more. Tune with `coalesceWindowMs` and
369
+ `maxConcurrentRefetches`.
370
+
371
+ Schema drift is separate and easy to miss, because nothing else catches it. A
372
+ deploy that renames a field leaves every cached entry current and quietly wrong
373
+ — no record changed, nothing is out of order, and the bytes describe a schema
374
+ that no longer exists. Wire it up once:
375
+
376
+ ```ts
377
+ import { createFoirDriftHandler } from '@foir/sdk/live';
378
+
379
+ createFoirClient({
380
+ // ...
381
+ onSchemaDrift: createFoirDriftHandler({
382
+ resync: () => {
383
+ store.resync();
384
+ queryClient.clear();
385
+ },
386
+ }),
387
+ });
388
+ ```
389
+
390
+ #### Known limitation: mutable sort keys
391
+
392
+ A query ordered by a mutable key — `updatedAt`, or a `position` a user drags —
393
+ can have a record move between pages when it is updated, so it may appear twice
394
+ or vanish until the next full refetch.
395
+
396
+ There is deliberately no detection for it: that needs client-side knowledge of
397
+ the sort fields, and the fully correct alternative turns a routine edit into an
398
+ N-page refetch on most feeds. Pass `mode="notify"` on screens where it matters
399
+ and refetch on the user's terms.
400
+
401
+ #### Pagination, and why the three cases differ
402
+
403
+ Foir is pure Relay cursor pagination with no `offset`, so loaded pages keep
404
+ their order and `endCursor` stays correct. Only three things break, and each
405
+ gets the treatment the data supports:
406
+
407
+ - **deleted** — the row is removed by id, with **no fetch**. The event carries
408
+ the id, so this is instantly correct.
409
+ - **created** — a **count**, not a refetch. A create cannot be spliced (record
410
+ events carry no body, and you could not place the row without knowing the
411
+ sort), and refetching the head page prepends under a reader mid-scroll.
412
+ - **updated** — the queries holding that record refetch. Membership is
413
+ unchanged, so cursors hold.
414
+
415
+ `totalCount` is flagged approximate rather than adjusted: it is the server's
416
+ count under the query's filter, and guessing would make a stale number into a
417
+ wrong one.
418
+
308
419
  ## License
309
420
 
310
421
  MIT
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { type FoirEnv } from '../graphql/foir-request';
18
18
  import { type AuthOpName, type AuthOpOptions } from './ops';
19
- import { type AuthConfig, type AuthProviderPublic, type CurrentUser, type CustomerAuthResult, type DefaultAuthProvider, type DeleteAccountResult, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type ProviderToken, type SignupContext, type WaitlistJoinResult } from './operations';
19
+ import { type AuthConfig, type AuthProviderPublic, type CurrentUser, type CustomerAuthResult, type DefaultAuthProvider, type DeleteAccountResult, type MyOrganization, type SelectedOrganization, type ProviderCallbackInput, type ProviderLoginInput, type ProviderLoginResult, type ProviderOTPVerifyInput, type ProviderToken, type SignupContext, type WaitlistJoinResult } from './operations';
20
20
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
21
21
  /** An access token + its rotating refresh token. */
22
22
  export interface FoirAuthTokens {
@@ -142,6 +142,77 @@ export declare function deleteMyAccount(env: FoirEnv, vars: {
142
142
  confirm: boolean;
143
143
  password?: string;
144
144
  }, options?: AuthOpOptions): Promise<DeleteAccountResult>;
145
+ /**
146
+ * The organizations this operator may stand in.
147
+ *
148
+ * Answered from the CALLING customer's own memberships: each organization they
149
+ * hold one in and every organization beneath it, so an operator is shown
150
+ * exactly the set they can stand in, and a customer with no memberships sees
151
+ * an empty list rather than a refusal. Requires the customer bearer; it is not
152
+ * the project's inventory and cannot be used as one.
153
+ */
154
+ export declare function myOrganizations(env: FoirEnv, vars?: {
155
+ first?: number;
156
+ after?: string;
157
+ }, options?: AuthOpOptions): Promise<MyOrganization[]>;
158
+ /**
159
+ * Stand in one of them, for this customer, until they choose otherwise.
160
+ *
161
+ * Set once — not per call. The selection is stored against the customer and
162
+ * resolved into the claim on every subsequent request, so nothing downstream
163
+ * takes an org argument. Pass an empty `orgId` to clear it and return to the
164
+ * project-shared view.
165
+ *
166
+ * The server authorises the choice against a live membership and refuses
167
+ * otherwise, so a caller cannot select their way into an organization they do
168
+ * not run.
169
+ *
170
+ * A MEMBERSHIP REACHES DOWNWARD, which is what "a live membership" means now
171
+ * that organizations nest. Someone who is a member of an organization may stand in any
172
+ * organization BENEATH it without holding a row of their own there, and
173
+ * {@link myOrganizations} offers exactly that set — so any id it returned can
174
+ * be passed here. The reverse never works: a membership at a branch stands in
175
+ * no ancestor and in no sibling branch, and both are refused as not-found,
176
+ * which is the same answer an organization that does not exist gets.
177
+ *
178
+ * WHAT STANDING THERE MEANS is every membership that reaches the organization
179
+ * selected. Reach a client through a membership above it and the role shown is
180
+ * that membership's; authority is the union of every membership that reaches
181
+ * it, together with the customer's own roles in the project.
182
+ * {@link MyOrganization.role} names ONE of those roles (the membership held in
183
+ * the organization itself, when there is one), so it is part of what the next
184
+ * request resolves and not the whole of it.
185
+ */
186
+ export declare function selectOrganization(env: FoirEnv, vars: {
187
+ orgId: string;
188
+ }, options?: AuthOpOptions): Promise<SelectedOrganization>;
189
+ /**
190
+ * Found an organization, as its first member.
191
+ *
192
+ * The caller decides the NAME and, optionally, where it hangs — and nothing
193
+ * else. The founder is the customer behind the bearer, the id is minted
194
+ * server-side the way a project's and a tenant's are, and the role they hold in
195
+ * the new organization is the one the PROJECT named in its settings
196
+ * (`operatorSignupRole`). The server creates the organization and that one
197
+ * membership together.
198
+ *
199
+ * `parentId` FOUNDS A CHILD, which is what lets your own customer create its own
200
+ * sub-tenants with no administrator in the loop — a reseller adding a client, a
201
+ * franchise adding a branch. It is resolved against the caller's own
202
+ * MEMBERSHIPS, not merely against the project: on this lane "an organization
203
+ * that exists" is not the question, "one you operate" is, and anything else is
204
+ * not-found. Omit it to found a root, which is what every registration did
205
+ * before organizations nested.
206
+ *
207
+ * Exists on the wire only while the project's `operatorSignupEnabled` setting
208
+ * is on; a project that never opted in has no such field, and this refuses
209
+ * naming it. Returns the same entry `myOrganizations` would then list, role
210
+ * included, so the application can go straight to {@link selectOrganization}.
211
+ */
212
+ export declare function registerOrganization(env: FoirEnv, vars: {
213
+ name: string;
214
+ parentId?: string;
215
+ }, options?: AuthOpOptions): Promise<MyOrganization>;
145
216
  /** Persist an auth result to the session; returns the customer identity. */
146
217
  export declare function saveSession(adapter: SessionAdapter, result: CustomerAuthResult): Promise<FoirCustomer>;
147
218
  /** Clear all Foir auth keys from the session. */
@@ -52,11 +52,78 @@ export interface HostedSession {
52
52
  token: string;
53
53
  customerId: string;
54
54
  email: string;
55
+ /**
56
+ * Seconds until `token` lapses, from the endpoint's `expires_in`.
57
+ *
58
+ * THIS FLOW HAS NO REFRESH TOKEN — the cookie is the durable credential and
59
+ * the bearer is re-exchanged when it dies. So a consumer has to know when
60
+ * that is, and without this the only options are to hardcode the platform's
61
+ * lifetime (coupling a storefront to a server constant it cannot see) or to
62
+ * decode `exp` off the JWT. Both re-derive a number the response already
63
+ * carried, and a consumer that does neither treats a 15-minute bearer as good
64
+ * forever — which reads as a customer being signed out mid-session.
65
+ *
66
+ * Store the death date, not this number: it is seconds from when the exchange
67
+ * ANSWERED, and a value held across requests is already stale.
68
+ *
69
+ * Zero when the endpoint did not report one. Treat that as "expires
70
+ * imminently" and re-exchange rather than as "never expires".
71
+ */
72
+ expiresIn: number;
55
73
  }
74
+ /**
75
+ * The outcome of a hosted-session exchange. The OUTCOME says what to do; the
76
+ * reason says what happened.
77
+ *
78
+ * Collapsing all of this to `null` is what made a transient blip on
79
+ * `/session/token` indistinguishable from a genuinely signed-out customer. The
80
+ * only safe-looking action on a bare `null` is to clear the session and send
81
+ * the customer to the front door, so one 502 signed out a customer whose 30-day
82
+ * cookie was perfectly valid — and the route's rate limiter is deliberately
83
+ * fail-open for exactly the opposite reason ("a Redis outage should degrade an
84
+ * authenticated app, not log it out").
85
+ */
86
+ export type HostedSessionResult =
87
+ /** Exchanged. Use `session`. */
88
+ {
89
+ outcome: 'active';
90
+ session: HostedSession;
91
+ }
92
+ /**
93
+ * The customer is signed out. Act on it: clear any local session and send
94
+ * them to `hostedLoginUrl`.
95
+ *
96
+ * `no-cookie` — nothing to exchange. `rejected` — the front door refused the
97
+ * cookie (401) and has already cleared it itself.
98
+ */
99
+ | {
100
+ outcome: 'signed-out';
101
+ reason: 'no-cookie' | 'rejected';
102
+ }
103
+ /**
104
+ * The exchange could not be completed, and it says NOTHING about whether the
105
+ * customer is signed in. KEEP the existing session, serve the request, try
106
+ * again on the next one.
107
+ *
108
+ * `forbidden` is the odd one: a 403 means the CSRF header did not arrive
109
+ * (a proxy stripping it, most likely), which is a misconfiguration rather
110
+ * than a blip. Retrying will not fix it — but signing the customer out would
111
+ * be wrong, so the safe action is the same and it is grouped by that.
112
+ */
113
+ | {
114
+ outcome: 'unavailable';
115
+ reason: 'forbidden' | 'server-error' | 'rate-limited' | 'network' | 'malformed';
116
+ /** Present when a response arrived at all. */
117
+ httpStatus?: number;
118
+ /** The thrown value, for `network`. */
119
+ cause?: unknown;
120
+ };
56
121
  /**
57
122
  * Exchange the hosted-login cookie on an incoming request for a customer
58
- * bearer + identity (server-to-server: forwards the cookie to the front
59
- * door's session endpoint with the CSRF header). Returns null when there's no
60
- * cookie or it's expired/revoked.
123
+ * bearer + identity (server-to-server: forwards the cookie to the front door's
124
+ * session endpoint with the CSRF header).
125
+ *
126
+ * See `HostedSessionResult` for why this reports an outcome rather than
127
+ * returning `null` for everything that is not a success.
61
128
  */
62
- export declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSession | null>;
129
+ export declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSessionResult>;
@@ -10,12 +10,12 @@
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 { customerLogin, customerRegister, customerRequestOTP, customerLoginOTP, customerRefreshToken, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, getAuthProviders, getAuthConfig, getDefaultAuthProvider, customerSignupContext, customerJoinWaitlist, customerRequestPasswordReset, customerResetPassword, customerUpdatePassword, customerVerifyEmail, currentUser, myProviderToken, customerLogout, deleteMyAccount, saveSession, clearSession, getSessionToken, getSessionCustomer, loginWithPassword, registerWithPassword, loginWithOTP, resetPasswordAndSignIn, refreshSession, signOut, authedRequest, authedOp, requireAuth, requireAdmin, } from './auth';
13
+ export { customerLogin, customerRegister, customerRequestOTP, customerLoginOTP, customerRefreshToken, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, getAuthProviders, getAuthConfig, getDefaultAuthProvider, customerSignupContext, customerJoinWaitlist, customerRequestPasswordReset, customerResetPassword, customerUpdatePassword, customerVerifyEmail, currentUser, myProviderToken, customerLogout, deleteMyAccount, myOrganizations, selectOrganization, registerOrganization, saveSession, clearSession, getSessionToken, getSessionCustomer, loginWithPassword, registerWithPassword, loginWithOTP, resetPasswordAndSignIn, refreshSession, signOut, authedRequest, authedOp, requireAuth, requireAdmin, } from './auth';
14
14
  export type { FoirAuthTokens, FoirCustomer, FoirUser, SessionAdapter, RequireAuthOptions, RequireAdminOptions, } from './auth';
15
15
  export { AUTH_OPS } from './ops';
16
16
  export type { AuthOpName, AuthOpOptions } from './ops';
17
17
  export { hostedLoginUrl, hostedLogoutUrl, fetchHostedSession } from './hosted';
18
- export type { HostedLoginConfig, HostedSession } from './hosted';
18
+ export type { HostedLoginConfig, HostedSession, HostedSessionResult } from './hosted';
19
19
  export { createFoirSessionClient } from './session-client';
20
20
  export type { FoirSessionClient, FoirSessionConfig, FoirSessionUser, } from './session-client';
21
- export type { CustomerAuthResult, ProviderLoginResult, AuthProviderPublic, ProviderLoginInput, ProviderCallbackInput, ProviderOTPVerifyInput, SignupContext, SignupMode, WaitlistJoinResult, AuthConfig, AuthConfigProvider, DefaultAuthProvider, CurrentUser, ProviderToken, DeleteAccountResult, } from './operations';
21
+ export type { CustomerAuthResult, ProviderLoginResult, AuthProviderPublic, ProviderLoginInput, ProviderCallbackInput, ProviderOTPVerifyInput, SignupContext, SignupMode, WaitlistJoinResult, AuthConfig, AuthConfigProvider, DefaultAuthProvider, CurrentUser, ProviderToken, DeleteAccountResult, MyOrganization, SelectedOrganization, } from './operations';
@@ -142,3 +142,87 @@ export interface DeleteAccountResult {
142
142
  recordsDeleted: number;
143
143
  filesDeleted: number;
144
144
  }
145
+ /**
146
+ * One organization the calling operator may stand in, as `myOrganizations`
147
+ * returns it.
148
+ *
149
+ * Identity, not configuration: the id to hand to {@link selectOrganization}, a
150
+ * name to render, the status, and where it sits. Only active organizations are
151
+ * returned today; `status` is here so that a widening is visible to the caller
152
+ * rather than silent.
153
+ *
154
+ * There is no `key`. An organization is identified by a server-minted id, the
155
+ * way a project and a tenant are; the subdomain a SaaS addresses its own
156
+ * tenants by is a record the SaaS models and reads for itself, and it was never
157
+ * this surface's to hand out.
158
+ *
159
+ * THE LIST IS A SUBTREE. A membership at an organization reaches everything
160
+ * beneath it, so an operator who runs a group is offered the group and every
161
+ * client under it. Draw it flat and a reseller's clients render as its
162
+ * siblings, which is what the three position fields are for.
163
+ */
164
+ export interface MyOrganization {
165
+ id: string;
166
+ name: string;
167
+ status: string;
168
+ /**
169
+ * The immediate parent, or null for a root.
170
+ *
171
+ * A root OF THE PROJECT, not of the caller's view: an operator whose
172
+ * membership sits three levels down sees their own organization with a parent
173
+ * they cannot read. Null means there is no parent; it is never the empty
174
+ * string, because that is a third value with no reading.
175
+ */
176
+ parentId: string | null;
177
+ /**
178
+ * Every ancestor, ROOT FIRST, ending at the immediate parent. Empty for a
179
+ * project root.
180
+ *
181
+ * IDS, not names, and the whole path — including ancestors this caller cannot
182
+ * read. An id is disclosed to everything below it; a name is row content an
183
+ * operator further down may not be entitled to. A path trimmed to the
184
+ * readable ancestors would have gaps or a false root, and two operators of
185
+ * one organization would then see different paths to it.
186
+ */
187
+ ancestorIds: string[];
188
+ /**
189
+ * Levels above this organization in the PROJECT's tree: 0 is a project root.
190
+ *
191
+ * The project's depth rather than the caller's, so two operators looking at
192
+ * the same organization agree about where it is. Indent by the difference
193
+ * from the shallowest entry in the list if you want the caller's own view.
194
+ */
195
+ depth: number;
196
+ /**
197
+ * The caller's own role in this organization, or null when their
198
+ * membership carries none (or a disabled one). Never another member's role.
199
+ *
200
+ * ONE ROLE, NOT THE WHOLE AUTHORITY. The platform resolves the operator's
201
+ * authority per request as the union of the roles on every membership that
202
+ * reaches this organization (one held in it and every one held above it),
203
+ * together with their own roles in the project. This field is the role of
204
+ * the membership held in the organization itself when there is one, and
205
+ * otherwise the role of a membership above it, so an application reading it
206
+ * sees part of what the next request is allowed and not the whole of it.
207
+ */
208
+ role: MyOrganizationRole | null;
209
+ }
210
+ /** A role as `myOrganizations` reports it: enough to match a policy and to render. */
211
+ export interface MyOrganizationRole {
212
+ id: string;
213
+ key: string;
214
+ name: string;
215
+ permissions: string[];
216
+ }
217
+ /**
218
+ * Where the caller now stands, as `selectMyOrganization` returns it.
219
+ *
220
+ * Two fields, because two are all there is to say. It is NOT a session context:
221
+ * an operator is a customer, and the platform's admin session shape — user,
222
+ * available tenants, effective permissions — describes a population this level
223
+ * exists to keep them out of.
224
+ */
225
+ export interface SelectedOrganization {
226
+ orgId: string;
227
+ orgName: string;
228
+ }
@@ -56,6 +56,9 @@ export declare const AUTH_OPS: {
56
56
  readonly customerUpdatePassword: "customerUpdatePassword";
57
57
  readonly customerVerifyEmail: "customerVerifyEmail";
58
58
  readonly deleteMyAccount: "deleteMyAccount";
59
+ readonly myOrganizations: "myOrganizations";
60
+ readonly selectMyOrganization: "selectMyOrganization";
61
+ readonly registerOperatorOrganization: "registerOperatorOrganization";
59
62
  readonly customerLoginWithProvider: "customerLoginWithProvider";
60
63
  readonly customerProviderCallback: "customerProviderCallback";
61
64
  readonly customerProviderVerifyOTP: "customerProviderVerifyOTP";
@@ -71,6 +74,13 @@ export interface AuthOpOptions {
71
74
  * authorised by. Ignored by the pre-credential ops, which mint it.
72
75
  */
73
76
  bearerToken?: string;
77
+ /**
78
+ * The organization the call acts in, sent as `X-Org-Id` — same contract as
79
+ * `FoirClientConfig.orgId`, because these ops ride the same transport. For a
80
+ * project credential acting inside one of its organizations; a credential
81
+ * already confined to an organization may only name its own.
82
+ */
83
+ orgId?: string;
74
84
  /**
75
85
  * `fetch` implementation. Defaults to `globalThis.fetch`. Injectable for
76
86
  * tests and for runtimes that hand you a bound `fetch` (Hydrogen/Oxygen).