@foir/sdk 0.8.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.
- package/CHANGELOG.md +550 -28
- package/README.md +111 -0
- package/dist/auth/auth.d.ts +72 -1
- package/dist/auth/hosted.d.ts +93 -4
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/operations.d.ts +84 -0
- package/dist/auth/ops.d.ts +10 -0
- package/dist/auth.js +138 -19
- package/dist/client.js +63 -6
- package/dist/codegen/block-registry.d.ts +1 -1
- package/dist/codegen/block-scaffold.d.ts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/item-registry.d.ts +1 -1
- package/dist/codegen/item-scaffold.d.ts +1 -1
- package/dist/content/content-renderer.d.ts +4 -5
- package/dist/content/index.d.ts +1 -1
- package/dist/content/richtext.d.ts +1 -1
- package/dist/data/builtins.d.ts +131 -12
- package/dist/data/client.d.ts +82 -1
- package/dist/data/decimal.d.ts +212 -0
- package/dist/data/document.d.ts +1 -1
- package/dist/data/index.d.ts +11 -9
- package/dist/data/manifest.d.ts +33 -6
- package/dist/data/schema.d.ts +2 -2
- package/dist/data/subscriptions.d.ts +36 -10
- package/dist/data/transport.d.ts +120 -3
- package/dist/data.js +377 -16
- package/dist/graphql/cdn-route.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/live/coalesce.d.ts +57 -0
- package/dist/live/drift.d.ts +59 -0
- package/dist/live/events.d.ts +13 -0
- package/dist/live/headers.d.ts +53 -0
- package/dist/live/index.d.ts +54 -0
- package/dist/live/keys.d.ts +72 -0
- package/dist/live/manager.d.ts +77 -0
- package/dist/live/react/index.d.ts +143 -0
- package/dist/live/reconcile.d.ts +82 -0
- package/dist/live/resync.d.ts +59 -0
- package/dist/live/store.d.ts +134 -0
- package/dist/live/tagIndex.d.ts +29 -0
- package/dist/live/tanstack.d.ts +127 -0
- package/dist/live/writetoken.d.ts +86 -0
- package/dist/live-react.js +395 -0
- package/dist/live.js +845 -0
- package/dist/next/cacheTags.d.ts +245 -0
- package/dist/next/index.d.ts +7 -0
- package/dist/next.js +179 -6
- package/dist/push.js +81 -8
- package/dist/react.js +76 -5
- package/dist/realtime/index.d.ts +4 -5
- package/dist/server/events-client.d.ts +54 -4
- package/dist/server/index.d.ts +3 -2
- package/dist/server/require-scoped-token.d.ts +1 -1
- package/dist/server.js +454 -2
- package/dist/transport/foir-http.d.ts +113 -10
- 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
|
package/dist/auth/auth.d.ts
CHANGED
|
@@ -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. */
|
package/dist/auth/hosted.d.ts
CHANGED
|
@@ -19,22 +19,111 @@ export interface HostedLoginConfig {
|
|
|
19
19
|
cookieName?: string;
|
|
20
20
|
/** Session-exchange path on the front door. Default `/session/token`. */
|
|
21
21
|
sessionPath?: string;
|
|
22
|
+
/** RP-initiated logout path on the front door. Default `/session/end-session`. */
|
|
23
|
+
endSessionPath?: string;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Build the redirect URL to the hosted front door, forwarding a best-effort
|
|
25
27
|
* absolute `returnTo` (honoured if the front door supports it).
|
|
26
28
|
*/
|
|
27
29
|
export declare function hostedLoginUrl(config: HostedLoginConfig, returnTo?: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Build the redirect URL that ENDS the hosted session — the mirror of
|
|
32
|
+
* `hostedLoginUrl`, and the only thing that actually signs a customer out on
|
|
33
|
+
* this flow.
|
|
34
|
+
*
|
|
35
|
+
* The cookie is the durable credential. Disposing of a bearer, clearing a
|
|
36
|
+
* local session, or calling the `customerLogout` mutation (which clears
|
|
37
|
+
* upstream provider tokens and nothing else) all leave it in place, and the
|
|
38
|
+
* next request re-establishes the customer from it. This URL points at the
|
|
39
|
+
* front door's end-session endpoint, which revokes the session row and clears
|
|
40
|
+
* the cookie before returning the browser to `returnTo`.
|
|
41
|
+
*
|
|
42
|
+
* A URL builder, deliberately: no transport, no CORS surface, no CSRF header,
|
|
43
|
+
* and no JavaScript required at the consumer. Server-side logout is
|
|
44
|
+
* `return redirect(hostedLogoutUrl(config, clientId, returnTo))`.
|
|
45
|
+
*
|
|
46
|
+
* `returnTo` must be an absolute URL on an origin registered for `clientId`;
|
|
47
|
+
* the platform refuses anything else. Omit it for a bare 204 with no redirect.
|
|
48
|
+
*/
|
|
49
|
+
export declare function hostedLogoutUrl(config: HostedLoginConfig, clientId: string, returnTo?: string): string;
|
|
28
50
|
/** The identity + bearer returned by a successful hosted-session exchange. */
|
|
29
51
|
export interface HostedSession {
|
|
30
52
|
token: string;
|
|
31
53
|
customerId: string;
|
|
32
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;
|
|
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;
|
|
33
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
|
+
};
|
|
34
121
|
/**
|
|
35
122
|
* Exchange the hosted-login cookie on an incoming request for a customer
|
|
36
|
-
* bearer + identity (server-to-server: forwards the cookie to the front
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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.
|
|
39
128
|
*/
|
|
40
|
-
export declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<
|
|
129
|
+
export declare function fetchHostedSession(request: Request, config: HostedLoginConfig): Promise<HostedSessionResult>;
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -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
|
-
export { hostedLoginUrl, fetchHostedSession } from './hosted';
|
|
18
|
-
export type { HostedLoginConfig, HostedSession } from './hosted';
|
|
17
|
+
export { hostedLoginUrl, hostedLogoutUrl, fetchHostedSession } 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
|
+
}
|
package/dist/auth/ops.d.ts
CHANGED
|
@@ -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).
|