@foir/sdk 0.28.0 → 0.29.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 +61 -0
- package/dist/auth/auth.d.ts +14 -5
- package/dist/auth/code-flow.d.ts +97 -0
- package/dist/auth/hosted.d.ts +11 -3
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/operations.d.ts +5 -0
- package/dist/auth.js +177 -8
- package/dist/data/transport.d.ts +1 -0
- package/dist/data/ws-transport.d.ts +9 -0
- package/dist/data.js +44 -3
- package/dist/media/ProgressiveMedia.d.ts +27 -1
- package/dist/media.js +39 -10
- package/dist/next/session.d.ts +6 -2
- package/dist/next.js +4 -9
- package/dist/placement.js +2 -2
- package/dist/react/FoirProvider.d.ts +6 -2
- package/dist/react.js +7 -9
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime.js +2 -1
- package/dist/server.js +54 -5
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,47 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
|
|
|
108
108
|
|
|
109
109
|
Changes on `dev` that have not been published to npm.
|
|
110
110
|
|
|
111
|
+
## [0.29.0] — 2026-10-21
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
|
|
115
|
+
- **`AuthorizedApp` says whose app it is**: `clientLogoUrl`, `clientLogoAlt`
|
|
116
|
+
and `clientHomepageUrl`, where the app set them.
|
|
117
|
+
- **`beginSignIn` and `completeSignIn`** (`@foir/sdk/auth`): the OpenID Connect
|
|
118
|
+
code flow for a server app with no OpenID Connect library. `beginSignIn`
|
|
119
|
+
finds the issuer's endpoints from its discovery document and returns the
|
|
120
|
+
authorization URL, with a PKCE S256 challenge, a state and a nonce, and what
|
|
121
|
+
to keep until the person comes back. `completeSignIn` checks the callback
|
|
122
|
+
against it, redeems the code at the token endpoint and returns the tokens,
|
|
123
|
+
with the ID token's issuer, audience, nonce and expiry checked. A refusal is
|
|
124
|
+
a `CodeFlowError` naming the OAuth error. It uses `fetch` and Web Crypto
|
|
125
|
+
only, so it runs in Workers, Oxygen and Node 18 or later.
|
|
126
|
+
- **A link for somebody new** (`@foir/sdk/auth`): `hostedLoginUrl`,
|
|
127
|
+
`signInUrl` and `useFoir().hostedSignInUrl` take `{ signUp: true }`, which
|
|
128
|
+
opens the hosted sign-in at the login domain's `/signup`, on the project's
|
|
129
|
+
way in rather than the sign-in form: joining where sign-up is public, the
|
|
130
|
+
waitlist in waitlist mode, or the invitation-only or closed notice. For a
|
|
131
|
+
"Sign up" or "Join the waitlist" button. In the code flow,
|
|
132
|
+
`beginSignIn(config, { prompt: 'create' })` asks for the same.
|
|
133
|
+
- **`<ProgressiveMedia>` video props** (`@foir/sdk/media`):
|
|
134
|
+
- `objectPosition` keeps a focal point in view when the image, the video or
|
|
135
|
+
its thumbnail is cropped to fit;
|
|
136
|
+
- `controls` shows the browser's controls, which default to `!autoplay` as
|
|
137
|
+
before. A video with controls shows from the start, its poster standing
|
|
138
|
+
until it plays;
|
|
139
|
+
- `preload` sets how much the browser fetches before a video plays;
|
|
140
|
+
- `videoRef` hands over the `<video>` element, for your own play and pause
|
|
141
|
+
controls;
|
|
142
|
+
- `onVideoError` is called when a video's source cannot play.
|
|
143
|
+
|
|
144
|
+
### Fixed
|
|
145
|
+
|
|
146
|
+
- **A video that cannot play leaves its thumbnail**, labelled with `alt`, where
|
|
147
|
+
it left a blank box.
|
|
148
|
+
- **A video's `alt` labels the `<video>`**; it was dropped.
|
|
149
|
+
- **A video of unknown size takes its own shape** once its metadata loads,
|
|
150
|
+
where it was cropped to 16:10.
|
|
151
|
+
|
|
111
152
|
## [0.28.0] — 2026-10-20
|
|
112
153
|
|
|
113
154
|
### Added
|
|
@@ -152,6 +193,17 @@ Changes on `dev` that have not been published to npm.
|
|
|
152
193
|
|
|
153
194
|
### Added
|
|
154
195
|
|
|
196
|
+
- **`createCollabSession({ orgId })`** (`@foir/sdk/realtime`): the
|
|
197
|
+
organization the signed-in user stands at in the room, sent as the
|
|
198
|
+
connection's `org`. The user joins there only as a member of it; omitted,
|
|
199
|
+
the join stands at the project.
|
|
200
|
+
- **The subscription socket honours close codes 4402 and 4403.** On 4402,
|
|
201
|
+
the socket's credential is no longer honoured: the bearer is renewed through
|
|
202
|
+
`refreshBearer` and the socket reconnects at once, failing its subscriptions
|
|
203
|
+
with `FoirSessionExpiredError` when the session is over. On 4403, the
|
|
204
|
+
signed-in user is no member of the organization the client names: the
|
|
205
|
+
subscriptions fail rather than reconnecting into the same refusal.
|
|
206
|
+
|
|
155
207
|
- **`organizations.startPool({ movePeople, dryRun })`** (`@foir/sdk/app`):
|
|
156
208
|
starting the run's organization's own pool takes its people along when
|
|
157
209
|
asked. Somebody who stands only there moves whole, and somebody who also
|
|
@@ -195,6 +247,15 @@ Changes on `dev` that have not been published to npm.
|
|
|
195
247
|
|
|
196
248
|
### Changed
|
|
197
249
|
|
|
250
|
+
- **Breaking: signing up with a password proves the address first.**
|
|
251
|
+
`register()` answers `true` and `signUpWithPassword(env, vars)` answers
|
|
252
|
+
nothing, and neither signs anybody in: the address is mailed a code, and
|
|
253
|
+
`signInWithOTP` / `loginWithOTP` with it finishes the account and starts the
|
|
254
|
+
session, also where the project takes no code to sign in. The answer is the
|
|
255
|
+
same whether or not the address has an account already, or the project takes
|
|
256
|
+
nobody new; whoever holds the address is told by email. `signUpWithPassword`
|
|
257
|
+
no longer takes a session adapter, `useFoirAuth().register()` answers
|
|
258
|
+
nothing, and `foirRegisterWithPassword` takes no cookie store.
|
|
198
259
|
- **`<FoirFeedback>` writes a signed-in user's note through `writeNote`**, the
|
|
199
260
|
one note create, rather than `createMyNote`, its older name. The same note
|
|
200
261
|
on the same data API. A client whose generated module predates `writeNote`
|
package/dist/auth/auth.d.ts
CHANGED
|
@@ -43,11 +43,16 @@ export declare function login(env: FoirEnv, vars: {
|
|
|
43
43
|
email: string;
|
|
44
44
|
password: string;
|
|
45
45
|
}): Promise<AuthResult>;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Start an account with email + password. The address is mailed a code, and
|
|
48
|
+
* signing in with it ({@link loginWithOTP}) proves the address and finishes
|
|
49
|
+
* the account. Answered the same whether or not the address has an account
|
|
50
|
+
* already, or the project takes nobody new: its holder is told by email.
|
|
51
|
+
*/
|
|
47
52
|
export declare function register(env: FoirEnv, vars: {
|
|
48
53
|
email: string;
|
|
49
54
|
password: string;
|
|
50
|
-
}): Promise<
|
|
55
|
+
}): Promise<boolean>;
|
|
51
56
|
/** Request a one-time password be emailed to the user. */
|
|
52
57
|
export declare function requestOTP(env: FoirEnv, email: string): Promise<boolean>;
|
|
53
58
|
/** Complete an OTP login. */
|
|
@@ -444,11 +449,15 @@ export declare function signInWithPassword(adapter: SessionAdapter, env: FoirEnv
|
|
|
444
449
|
email: string;
|
|
445
450
|
password: string;
|
|
446
451
|
}): Promise<FoirUser>;
|
|
447
|
-
/**
|
|
448
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Start a sign-up with email + password: the address is mailed a code, and
|
|
454
|
+
* {@link signInWithOTP} with it finishes the account and persists the
|
|
455
|
+
* session. Nobody is signed in until then.
|
|
456
|
+
*/
|
|
457
|
+
export declare function signUpWithPassword(env: FoirEnv, vars: {
|
|
449
458
|
email: string;
|
|
450
459
|
password: string;
|
|
451
|
-
}): Promise<
|
|
460
|
+
}): Promise<void>;
|
|
452
461
|
/** Complete an OTP login and persist the session. */
|
|
453
462
|
export declare function signInWithOTP(adapter: SessionAdapter, env: FoirEnv, vars: {
|
|
454
463
|
email: string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign a person in through the OpenID Connect code flow, from a server app
|
|
3
|
+
* that has no OpenID Connect library of its own.
|
|
4
|
+
*
|
|
5
|
+
* `beginSignIn` finds the issuer's endpoints from its discovery document and
|
|
6
|
+
* builds the authorization URL, with a PKCE S256 challenge, a state and a
|
|
7
|
+
* nonce. It returns the URL to send the browser to and what the app keeps
|
|
8
|
+
* until the person comes back: keep it on the server, or in an http-only
|
|
9
|
+
* cookie scoped to the callback. `completeSignIn` checks the callback against
|
|
10
|
+
* it, redeems the code at the token endpoint and returns the tokens.
|
|
11
|
+
*
|
|
12
|
+
* These are the same endpoints any OpenID Connect library uses
|
|
13
|
+
* (`/oauth/authorize` and `/oauth/token`), so an app can move to a library
|
|
14
|
+
* later without changing anything in Foir. Only `fetch` and Web Crypto are
|
|
15
|
+
* used, so the helper runs in Workers, Oxygen and Node 18 or later.
|
|
16
|
+
*
|
|
17
|
+
* The ID token is checked for its issuer, audience, nonce and expiry. Its
|
|
18
|
+
* signature is not checked: it came straight from the issuer's token
|
|
19
|
+
* endpoint over TLS, which OpenID Connect accepts in place of the signature
|
|
20
|
+
* (Core 1.0, section 3.1.3.7).
|
|
21
|
+
*/
|
|
22
|
+
/** Where the app signs people in. */
|
|
23
|
+
export interface CodeFlowConfig {
|
|
24
|
+
/**
|
|
25
|
+
* The issuer: Foir's own (`https://api.foir.io`) or a project's login
|
|
26
|
+
* domain (`https://login.example.com`), which is the issuer of the people
|
|
27
|
+
* who sign in through it.
|
|
28
|
+
*/
|
|
29
|
+
issuer: string;
|
|
30
|
+
/** The auth client's id. */
|
|
31
|
+
clientId: string;
|
|
32
|
+
/** The callback, exactly as the auth client lists it. */
|
|
33
|
+
redirectUri: string;
|
|
34
|
+
/** A fetch to use in place of the global one. */
|
|
35
|
+
fetch?: typeof fetch;
|
|
36
|
+
}
|
|
37
|
+
/** What one sign-in asks for. */
|
|
38
|
+
export interface BeginSignInOptions {
|
|
39
|
+
/**
|
|
40
|
+
* What the sign-in asks to carry. Default `openid profile email`. Add the
|
|
41
|
+
* permission names the app's list holds to ask for fewer than all of them.
|
|
42
|
+
*/
|
|
43
|
+
scope?: string | string[];
|
|
44
|
+
/** The organization the person signs in to, when the app serves several. */
|
|
45
|
+
org?: string;
|
|
46
|
+
/** `login` asks the person to sign in again, `consent` to agree again. */
|
|
47
|
+
prompt?: string;
|
|
48
|
+
/** The address to start the sign-in with. */
|
|
49
|
+
loginHint?: string;
|
|
50
|
+
}
|
|
51
|
+
/** A sign-in begun: where to send the browser, and what to keep for the callback. */
|
|
52
|
+
export interface PendingSignIn {
|
|
53
|
+
/** The authorization URL. */
|
|
54
|
+
url: string;
|
|
55
|
+
/** Kept to check the callback's `state`. */
|
|
56
|
+
state: string;
|
|
57
|
+
/** Kept, and sent with the code. Never put it in a URL. */
|
|
58
|
+
codeVerifier: string;
|
|
59
|
+
/** Kept to check the ID token's `nonce`. */
|
|
60
|
+
nonce: string;
|
|
61
|
+
}
|
|
62
|
+
/** What a completed sign-in hands back. */
|
|
63
|
+
export interface CodeFlowTokens {
|
|
64
|
+
accessToken: string;
|
|
65
|
+
tokenType: string;
|
|
66
|
+
expiresIn?: number;
|
|
67
|
+
refreshToken?: string;
|
|
68
|
+
idToken?: string;
|
|
69
|
+
scope?: string;
|
|
70
|
+
/** The ID token's claims, checked as described above. */
|
|
71
|
+
claims?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A sign-in that did not complete. `code` is the OAuth error the issuer
|
|
75
|
+
* answered (`access_denied`, `invalid_grant`, ...), or one of the checks the
|
|
76
|
+
* helper makes: `state_mismatch`, `issuer_mismatch`, `nonce_mismatch`,
|
|
77
|
+
* `audience_mismatch`, `id_token_expired`, `discovery_failed` (the issuer
|
|
78
|
+
* could not be reached or answered no discovery document),
|
|
79
|
+
* `token_request_failed` (the token endpoint could not be reached or answered
|
|
80
|
+
* no access token), `invalid_request` (the callback carries no code).
|
|
81
|
+
*/
|
|
82
|
+
export declare class CodeFlowError extends Error {
|
|
83
|
+
readonly code: string;
|
|
84
|
+
readonly description?: string;
|
|
85
|
+
constructor(code: string, description?: string);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Begin a sign-in: the URL to send the browser to, and what to keep until
|
|
89
|
+
* the person comes back to the callback.
|
|
90
|
+
*/
|
|
91
|
+
export declare function beginSignIn(config: CodeFlowConfig, options?: BeginSignInOptions): Promise<PendingSignIn>;
|
|
92
|
+
/**
|
|
93
|
+
* Complete a sign-in from its callback: check it against what `beginSignIn`
|
|
94
|
+
* returned, redeem the code at the token endpoint, and return the tokens.
|
|
95
|
+
* `callback` is the URL the browser came back to, or its query.
|
|
96
|
+
*/
|
|
97
|
+
export declare function completeSignIn(config: CodeFlowConfig, callback: string | URL | URLSearchParams, pending: Pick<PendingSignIn, 'state' | 'codeVerifier' | 'nonce'>): Promise<CodeFlowTokens>;
|
package/dist/auth/hosted.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The hosted session: for an app on the same registrable domain as its login
|
|
3
|
+
* domain, which trades the login domain's session cookie for a token instead
|
|
4
|
+
* of running the code flow itself (see code-flow.ts for that).
|
|
5
5
|
*
|
|
6
6
|
* The front door sets a first-party session cookie on the registrable domain;
|
|
7
7
|
* because that domain is shared, the cookie reaches the storefront, which
|
|
@@ -37,6 +37,14 @@ export interface HostedLoginOptions {
|
|
|
37
37
|
* of another project, and one that is not active.
|
|
38
38
|
*/
|
|
39
39
|
org?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Somebody new is expected: the page opens on the project's way in rather
|
|
42
|
+
* than the sign-in form, for a "Sign up" or "Join the waitlist" button. The
|
|
43
|
+
* project's sign-up still decides what that is: joining where it is public,
|
|
44
|
+
* the waitlist, or the invitation-only or closed notice. The URL is the
|
|
45
|
+
* login domain's `/signup`.
|
|
46
|
+
*/
|
|
47
|
+
signUp?: boolean;
|
|
40
48
|
}
|
|
41
49
|
/**
|
|
42
50
|
* Build the redirect URL to the hosted front door, forwarding an absolute
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export { FoirAuthDowngradedError, FoirSessionExpiredError } from '../graphql/foi
|
|
|
18
18
|
export { AUTH_OPS } from './ops';
|
|
19
19
|
export type { AuthOpName, AuthOpOptions } from './ops';
|
|
20
20
|
export { hostedLoginUrl, hostedLogoutUrl, fetchHostedSession, hostedSignInNoticeOf } from './hosted';
|
|
21
|
+
export { beginSignIn, completeSignIn, CodeFlowError } from './code-flow';
|
|
22
|
+
export type { CodeFlowConfig, BeginSignInOptions, PendingSignIn, CodeFlowTokens } from './code-flow';
|
|
21
23
|
export type { HostedLoginConfig, HostedLoginOptions, HostedSession, HostedSessionResult, HostedSignInNotice } from './hosted';
|
|
22
24
|
export { createFoirSessionClient } from './session-client';
|
|
23
25
|
export type { FoirSessionClient, FoirSessionConfig, FoirSessionUser, } from './session-client';
|
|
@@ -317,6 +317,11 @@ export interface AuthorizedApp {
|
|
|
317
317
|
id: string;
|
|
318
318
|
clientId: string;
|
|
319
319
|
clientName: string;
|
|
320
|
+
/** The app's logo, where it has one, and what it shows. */
|
|
321
|
+
clientLogoUrl?: string | null;
|
|
322
|
+
clientLogoAlt?: string | null;
|
|
323
|
+
/** The app's homepage, where it has one. */
|
|
324
|
+
clientHomepageUrl?: string | null;
|
|
320
325
|
/** What the app may carry for them, in the permission catalogue's names. */
|
|
321
326
|
grants: string[];
|
|
322
327
|
/** ISO-8601. */
|
package/dist/auth.js
CHANGED
|
@@ -1408,8 +1408,8 @@ async function getSessionUser(adapter) {
|
|
|
1408
1408
|
async function signInWithPassword(adapter, env, vars) {
|
|
1409
1409
|
return saveSession(adapter, await login(env, vars));
|
|
1410
1410
|
}
|
|
1411
|
-
async function signUpWithPassword(
|
|
1412
|
-
|
|
1411
|
+
async function signUpWithPassword(env, vars) {
|
|
1412
|
+
await register(env, vars);
|
|
1413
1413
|
}
|
|
1414
1414
|
async function signInWithOTP(adapter, env, vars) {
|
|
1415
1415
|
return saveSession(adapter, await loginWithOTP(env, vars));
|
|
@@ -1602,6 +1602,7 @@ var GRANT_TABLE = [
|
|
|
1602
1602
|
["apikeys:write", "none", [], []],
|
|
1603
1603
|
["organizations:read", "none", [], []],
|
|
1604
1604
|
["organizations:write", "none", [], []],
|
|
1605
|
+
["orgsettings:write", "none", [], []],
|
|
1605
1606
|
["organizations:create", "none", [], []],
|
|
1606
1607
|
["organizations:delete", "none", [], []],
|
|
1607
1608
|
["organizations:reach", "none", [], []],
|
|
@@ -1612,7 +1613,7 @@ var GRANT_TABLE = [
|
|
|
1612
1613
|
["projects:read", "none", [], []],
|
|
1613
1614
|
["projects:write", "none", [], []],
|
|
1614
1615
|
["project:stand", "none", [], []],
|
|
1615
|
-
["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "
|
|
1616
|
+
["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", "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", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
|
|
1616
1617
|
["projects:create", "none", [], []],
|
|
1617
1618
|
["projects:delete", "none", [], []],
|
|
1618
1619
|
["tenants:read", "none", [], []],
|
|
@@ -1643,7 +1644,6 @@ var GRANT_TABLE = [
|
|
|
1643
1644
|
["usersecrets:list", "none", [], []],
|
|
1644
1645
|
["usersecrets:put", "none", [], []],
|
|
1645
1646
|
["usersecrets:delete", "none", [], []],
|
|
1646
|
-
["auth:login_with_foir", "none", [], []],
|
|
1647
1647
|
["signinproviders:read", "none", [], []],
|
|
1648
1648
|
["signinproviders:write", "none", [], []]
|
|
1649
1649
|
];
|
|
@@ -1807,6 +1807,9 @@ var DEFAULT_SESSION_PATH = "/session/token";
|
|
|
1807
1807
|
var DEFAULT_END_SESSION_PATH = "/session/end-session";
|
|
1808
1808
|
function hostedLoginUrl(config, returnTo, options = {}) {
|
|
1809
1809
|
const dest = new URL(config.loginOrigin);
|
|
1810
|
+
if (options.signUp) {
|
|
1811
|
+
dest.pathname = `${dest.pathname.replace(/\/+$/, "")}/signup`;
|
|
1812
|
+
}
|
|
1810
1813
|
if (returnTo && returnTo !== "/") {
|
|
1811
1814
|
dest.searchParams.set("returnTo", returnTo);
|
|
1812
1815
|
}
|
|
@@ -1913,6 +1916,169 @@ function forApp(clientId) {
|
|
|
1913
1916
|
return clientId ? `?client_id=${encodeURIComponent(clientId)}` : "";
|
|
1914
1917
|
}
|
|
1915
1918
|
|
|
1919
|
+
// src/auth/code-flow.ts
|
|
1920
|
+
var CodeFlowError = class extends Error {
|
|
1921
|
+
code;
|
|
1922
|
+
description;
|
|
1923
|
+
constructor(code, description) {
|
|
1924
|
+
super(description ? `${code}: ${description}` : code);
|
|
1925
|
+
this.name = "CodeFlowError";
|
|
1926
|
+
this.code = code;
|
|
1927
|
+
this.description = description;
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
var discoveries = /* @__PURE__ */ new Map();
|
|
1931
|
+
function trimSlash(url) {
|
|
1932
|
+
return url.replace(/\/+$/, "");
|
|
1933
|
+
}
|
|
1934
|
+
async function discover(config) {
|
|
1935
|
+
const issuer = trimSlash(config.issuer);
|
|
1936
|
+
let found = discoveries.get(issuer);
|
|
1937
|
+
if (!found) {
|
|
1938
|
+
found = (async () => {
|
|
1939
|
+
let res;
|
|
1940
|
+
try {
|
|
1941
|
+
res = await (config.fetch ?? fetch)(`${issuer}/.well-known/openid-configuration`, {
|
|
1942
|
+
headers: { accept: "application/json" }
|
|
1943
|
+
});
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
throw new CodeFlowError("discovery_failed", `${issuer} could not be reached for its discovery document: ${String(e)}`);
|
|
1946
|
+
}
|
|
1947
|
+
if (!res.ok) {
|
|
1948
|
+
throw new CodeFlowError("discovery_failed", `${issuer} answered ${res.status} for its discovery document`);
|
|
1949
|
+
}
|
|
1950
|
+
const doc = await res.json().catch(() => null);
|
|
1951
|
+
if (!doc) {
|
|
1952
|
+
throw new CodeFlowError("discovery_failed", `${issuer} answered no JSON for its discovery document`);
|
|
1953
|
+
}
|
|
1954
|
+
if (!doc.issuer || trimSlash(doc.issuer) !== issuer) {
|
|
1955
|
+
throw new CodeFlowError("issuer_mismatch", `the discovery document at ${issuer} names ${doc.issuer ?? "no issuer"}`);
|
|
1956
|
+
}
|
|
1957
|
+
if (!doc.authorization_endpoint || !doc.token_endpoint) {
|
|
1958
|
+
throw new CodeFlowError("discovery_failed", `the discovery document at ${issuer} names no authorization or token endpoint`);
|
|
1959
|
+
}
|
|
1960
|
+
return doc;
|
|
1961
|
+
})();
|
|
1962
|
+
discoveries.set(issuer, found);
|
|
1963
|
+
found.catch(() => discoveries.delete(issuer));
|
|
1964
|
+
}
|
|
1965
|
+
return found;
|
|
1966
|
+
}
|
|
1967
|
+
function base64url(bytes) {
|
|
1968
|
+
let s = "";
|
|
1969
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
1970
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1971
|
+
}
|
|
1972
|
+
function randomString(byteLength) {
|
|
1973
|
+
const bytes = new Uint8Array(byteLength);
|
|
1974
|
+
crypto.getRandomValues(bytes);
|
|
1975
|
+
return base64url(bytes);
|
|
1976
|
+
}
|
|
1977
|
+
async function s256(verifier) {
|
|
1978
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
1979
|
+
return base64url(new Uint8Array(digest));
|
|
1980
|
+
}
|
|
1981
|
+
async function beginSignIn(config, options = {}) {
|
|
1982
|
+
const endpoints = await discover(config);
|
|
1983
|
+
const state = randomString(24);
|
|
1984
|
+
const nonce = randomString(24);
|
|
1985
|
+
const codeVerifier = randomString(48);
|
|
1986
|
+
const url = new URL(endpoints.authorization_endpoint);
|
|
1987
|
+
const scope = Array.isArray(options.scope) ? options.scope.join(" ") : options.scope ?? "openid profile email";
|
|
1988
|
+
url.searchParams.set("client_id", config.clientId);
|
|
1989
|
+
url.searchParams.set("redirect_uri", config.redirectUri);
|
|
1990
|
+
url.searchParams.set("response_type", "code");
|
|
1991
|
+
url.searchParams.set("scope", scope);
|
|
1992
|
+
url.searchParams.set("state", state);
|
|
1993
|
+
url.searchParams.set("nonce", nonce);
|
|
1994
|
+
url.searchParams.set("code_challenge", await s256(codeVerifier));
|
|
1995
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
1996
|
+
if (options.org) url.searchParams.set("org", options.org);
|
|
1997
|
+
if (options.prompt) url.searchParams.set("prompt", options.prompt);
|
|
1998
|
+
if (options.loginHint) url.searchParams.set("login_hint", options.loginHint);
|
|
1999
|
+
return { url: url.toString(), state, codeVerifier, nonce };
|
|
2000
|
+
}
|
|
2001
|
+
function callbackParams(callback) {
|
|
2002
|
+
if (callback instanceof URLSearchParams) return callback;
|
|
2003
|
+
if (callback instanceof URL) return callback.searchParams;
|
|
2004
|
+
const query = callback.indexOf("?");
|
|
2005
|
+
return new URLSearchParams(query >= 0 ? callback.slice(query + 1).split("#")[0] : callback);
|
|
2006
|
+
}
|
|
2007
|
+
function claimsOf(idToken) {
|
|
2008
|
+
const payload = idToken.split(".")[1];
|
|
2009
|
+
if (!payload) throw new CodeFlowError("invalid_id_token", "the ID token is not a JWT");
|
|
2010
|
+
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
|
|
2011
|
+
const bytes = Uint8Array.from(json, (c) => c.charCodeAt(0));
|
|
2012
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
2013
|
+
}
|
|
2014
|
+
async function completeSignIn(config, callback, pending) {
|
|
2015
|
+
const endpoints = await discover(config);
|
|
2016
|
+
const params = callbackParams(callback);
|
|
2017
|
+
if (params.get("state") !== pending.state) {
|
|
2018
|
+
throw new CodeFlowError("state_mismatch", "the callback is not for the sign-in this app began");
|
|
2019
|
+
}
|
|
2020
|
+
const refused = params.get("error");
|
|
2021
|
+
if (refused) {
|
|
2022
|
+
throw new CodeFlowError(refused, params.get("error_description") ?? void 0);
|
|
2023
|
+
}
|
|
2024
|
+
const code = params.get("code");
|
|
2025
|
+
if (!code) {
|
|
2026
|
+
throw new CodeFlowError("invalid_request", "the callback carries no code");
|
|
2027
|
+
}
|
|
2028
|
+
let res;
|
|
2029
|
+
try {
|
|
2030
|
+
res = await (config.fetch ?? fetch)(endpoints.token_endpoint, {
|
|
2031
|
+
method: "POST",
|
|
2032
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
2033
|
+
body: new URLSearchParams({
|
|
2034
|
+
grant_type: "authorization_code",
|
|
2035
|
+
code,
|
|
2036
|
+
redirect_uri: config.redirectUri,
|
|
2037
|
+
client_id: config.clientId,
|
|
2038
|
+
code_verifier: pending.codeVerifier
|
|
2039
|
+
}).toString()
|
|
2040
|
+
});
|
|
2041
|
+
} catch (e) {
|
|
2042
|
+
throw new CodeFlowError("token_request_failed", `the token endpoint could not be reached: ${String(e)}`);
|
|
2043
|
+
}
|
|
2044
|
+
const body = await res.json().catch(() => ({}));
|
|
2045
|
+
if (!res.ok) {
|
|
2046
|
+
throw new CodeFlowError(
|
|
2047
|
+
typeof body.error === "string" ? body.error : "token_request_failed",
|
|
2048
|
+
typeof body.error_description === "string" ? body.error_description : `the token endpoint answered ${res.status}`
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
const tokens = {
|
|
2052
|
+
accessToken: String(body.access_token ?? ""),
|
|
2053
|
+
tokenType: String(body.token_type ?? "Bearer"),
|
|
2054
|
+
expiresIn: typeof body.expires_in === "number" ? body.expires_in : void 0,
|
|
2055
|
+
refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
|
|
2056
|
+
idToken: typeof body.id_token === "string" ? body.id_token : void 0,
|
|
2057
|
+
scope: typeof body.scope === "string" ? body.scope : void 0
|
|
2058
|
+
};
|
|
2059
|
+
if (!tokens.accessToken) {
|
|
2060
|
+
throw new CodeFlowError("token_request_failed", "the token endpoint answered no access token");
|
|
2061
|
+
}
|
|
2062
|
+
if (tokens.idToken) {
|
|
2063
|
+
const claims = claimsOf(tokens.idToken);
|
|
2064
|
+
if (typeof claims.iss !== "string" || trimSlash(claims.iss) !== trimSlash(endpoints.issuer)) {
|
|
2065
|
+
throw new CodeFlowError("issuer_mismatch", `the ID token's issuer is ${String(claims.iss)}`);
|
|
2066
|
+
}
|
|
2067
|
+
const audience = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
2068
|
+
if (!audience.includes(config.clientId)) {
|
|
2069
|
+
throw new CodeFlowError("audience_mismatch", "the ID token is not for this app");
|
|
2070
|
+
}
|
|
2071
|
+
if (claims.nonce !== pending.nonce) {
|
|
2072
|
+
throw new CodeFlowError("nonce_mismatch", "the ID token is not for the sign-in this app began");
|
|
2073
|
+
}
|
|
2074
|
+
if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) {
|
|
2075
|
+
throw new CodeFlowError("id_token_expired", "the ID token has expired");
|
|
2076
|
+
}
|
|
2077
|
+
tokens.claims = claims;
|
|
2078
|
+
}
|
|
2079
|
+
return tokens;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1916
2082
|
// src/auth/session-client.ts
|
|
1917
2083
|
var DEFAULT_SESSION_PATH2 = "/session/token";
|
|
1918
2084
|
var DEFAULT_LOGOUT_PATH = "/session/logout";
|
|
@@ -1921,8 +2087,8 @@ var MIN_REFRESH_DELAY_MS = 5e3;
|
|
|
1921
2087
|
var CSRF_HEADER = { "X-Foir-Session": "1" };
|
|
1922
2088
|
function createFoirSessionClient(config) {
|
|
1923
2089
|
const forApp2 = config.clientId ? `?client_id=${encodeURIComponent(config.clientId)}` : "";
|
|
1924
|
-
const sessionUrl = `${
|
|
1925
|
-
const logoutUrl = `${
|
|
2090
|
+
const sessionUrl = `${trimSlash2(config.loginOrigin)}${config.sessionPath ?? DEFAULT_SESSION_PATH2}${forApp2}`;
|
|
2091
|
+
const logoutUrl = `${trimSlash2(config.loginOrigin)}${config.logoutPath ?? DEFAULT_LOGOUT_PATH}${forApp2}`;
|
|
1926
2092
|
const skewMs = (config.refreshSkewSeconds ?? DEFAULT_REFRESH_SKEW_SECONDS) * 1e3;
|
|
1927
2093
|
let cached = null;
|
|
1928
2094
|
let inflight = null;
|
|
@@ -2029,7 +2195,7 @@ function createFoirSessionClient(config) {
|
|
|
2029
2195
|
config.onSignOut?.();
|
|
2030
2196
|
},
|
|
2031
2197
|
signInUrl(returnTo, options = {}) {
|
|
2032
|
-
const dest = new URL(
|
|
2198
|
+
const dest = new URL(trimSlash2(config.loginOrigin));
|
|
2033
2199
|
const target = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
2034
2200
|
if (target && target !== "/") dest.searchParams.set("returnTo", target);
|
|
2035
2201
|
if (options.org) dest.searchParams.set("org", options.org);
|
|
@@ -2046,11 +2212,12 @@ function createFoirSessionClient(config) {
|
|
|
2046
2212
|
}
|
|
2047
2213
|
};
|
|
2048
2214
|
}
|
|
2049
|
-
function
|
|
2215
|
+
function trimSlash2(s) {
|
|
2050
2216
|
return s.replace(/\/+$/, "");
|
|
2051
2217
|
}
|
|
2052
2218
|
export {
|
|
2053
2219
|
AUTH_OPS,
|
|
2220
|
+
CodeFlowError,
|
|
2054
2221
|
FoirAccessDeniedError,
|
|
2055
2222
|
FoirAuthDowngradedError,
|
|
2056
2223
|
FoirSessionExpiredError,
|
|
@@ -2058,9 +2225,11 @@ export {
|
|
|
2058
2225
|
authedOp,
|
|
2059
2226
|
authedRequest,
|
|
2060
2227
|
beginFactorEnrollment,
|
|
2228
|
+
beginSignIn,
|
|
2061
2229
|
beginStepUp,
|
|
2062
2230
|
clearSession,
|
|
2063
2231
|
completeFactorEnrollment,
|
|
2232
|
+
completeSignIn,
|
|
2064
2233
|
completeStepUp,
|
|
2065
2234
|
confirmProviderLink,
|
|
2066
2235
|
connectSignInMethod,
|
package/dist/data/transport.d.ts
CHANGED
|
@@ -725,4 +725,5 @@ export declare function executeFoirDocument<T>(config: FoirClientConfig, plan: F
|
|
|
725
725
|
export declare function executeFoirRawDocument<T>(config: FoirClientConfig, document: string, mutation: boolean, variables?: Record<string, unknown>, options?: {
|
|
726
726
|
strict?: boolean;
|
|
727
727
|
}): Promise<T>;
|
|
728
|
+
export declare function renewBearerOnce(refresh: () => Promise<string | null>): Promise<string | null>;
|
|
728
729
|
export {};
|
|
@@ -149,6 +149,7 @@ export declare class FoirSocketTransport {
|
|
|
149
149
|
private disposed;
|
|
150
150
|
private keepAlive;
|
|
151
151
|
private retryTimer;
|
|
152
|
+
private renewedBearer;
|
|
152
153
|
constructor(config: FoirRequestConfig);
|
|
153
154
|
/**
|
|
154
155
|
* Start a subscription.
|
|
@@ -167,6 +168,14 @@ export declare class FoirSocketTransport {
|
|
|
167
168
|
private ensureConnected;
|
|
168
169
|
private sendConnectionInit;
|
|
169
170
|
private handleClose;
|
|
171
|
+
/**
|
|
172
|
+
* Reconnect after the socket's credential lapsed: with the bearer renewed
|
|
173
|
+
* through the client's `refreshBearer` where it has one, failing every
|
|
174
|
+
* subscription with `FoirSessionExpiredError` when the session is over. A
|
|
175
|
+
* client without one reconnects at once, presenting what its bearer
|
|
176
|
+
* provider gives now.
|
|
177
|
+
*/
|
|
178
|
+
private reconnectWithARenewedBearer;
|
|
170
179
|
private scheduleReconnect;
|
|
171
180
|
private teardownSocket;
|
|
172
181
|
private receive;
|
package/dist/data.js
CHANGED
|
@@ -1380,6 +1380,8 @@ function printSubscriptionDocument(input) {
|
|
|
1380
1380
|
}
|
|
1381
1381
|
|
|
1382
1382
|
// src/data/ws-transport.ts
|
|
1383
|
+
var CLOSE_CREDENTIAL_ENDED = 4402;
|
|
1384
|
+
var CLOSE_FORBIDDEN = 4403;
|
|
1383
1385
|
var GQL = {
|
|
1384
1386
|
connectionInit: "connection_init",
|
|
1385
1387
|
connectionAck: "connection_ack",
|
|
@@ -1414,6 +1416,9 @@ var FoirSocketTransport = class {
|
|
|
1414
1416
|
disposed = false;
|
|
1415
1417
|
keepAlive = null;
|
|
1416
1418
|
retryTimer = null;
|
|
1419
|
+
// A bearer renewed after the socket's lapsed (4402), presented from the next
|
|
1420
|
+
// connect on in place of the client's own.
|
|
1421
|
+
renewedBearer = null;
|
|
1417
1422
|
constructor(config) {
|
|
1418
1423
|
if (config.dispatch !== void 0) {
|
|
1419
1424
|
throw new FoirRequestError(
|
|
@@ -1486,12 +1491,14 @@ var FoirSocketTransport = class {
|
|
|
1486
1491
|
socket.onmessage = (event) => this.receive(event.data);
|
|
1487
1492
|
socket.onerror = () => {
|
|
1488
1493
|
};
|
|
1489
|
-
socket.onclose = () => this.handleClose();
|
|
1494
|
+
socket.onclose = (event) => this.handleClose(event);
|
|
1490
1495
|
}
|
|
1491
1496
|
async sendConnectionInit(socket) {
|
|
1492
1497
|
this.connecting = false;
|
|
1493
1498
|
try {
|
|
1494
|
-
const { headers } = await resolveFoirAuth(
|
|
1499
|
+
const { headers } = await resolveFoirAuth(
|
|
1500
|
+
this.renewedBearer ? { ...this.config, bearerToken: this.renewedBearer } : this.config
|
|
1501
|
+
);
|
|
1495
1502
|
const { "Content-Type": _contentType, ...params } = headers;
|
|
1496
1503
|
const extra = await this.config.connectionParams?.() ?? {};
|
|
1497
1504
|
this.send(socket, {
|
|
@@ -1505,19 +1512,53 @@ var FoirSocketTransport = class {
|
|
|
1505
1512
|
this.teardownSocket(true);
|
|
1506
1513
|
}
|
|
1507
1514
|
}
|
|
1508
|
-
handleClose() {
|
|
1515
|
+
handleClose(event) {
|
|
1516
|
+
const code = event?.code;
|
|
1509
1517
|
const wasAcknowledged = this.acknowledged;
|
|
1510
1518
|
this.teardownSocket(false);
|
|
1511
1519
|
if (this.disposed || this.registrations.size === 0) return;
|
|
1512
1520
|
for (const registration of this.registrations.values()) {
|
|
1513
1521
|
registration.sent = false;
|
|
1514
1522
|
}
|
|
1523
|
+
if (code === CLOSE_FORBIDDEN) {
|
|
1524
|
+
this.failAll(
|
|
1525
|
+
new FoirRequestError(
|
|
1526
|
+
"[foir] the subscription socket was refused where it asked to stand: the signed-in user is no member of the organization this client names"
|
|
1527
|
+
)
|
|
1528
|
+
);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
if (code === CLOSE_CREDENTIAL_ENDED) {
|
|
1532
|
+
void this.reconnectWithARenewedBearer();
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1515
1535
|
this.scheduleReconnect(
|
|
1516
1536
|
new FoirRequestError(
|
|
1517
1537
|
wasAcknowledged ? "[foir] the subscription socket closed and could not be re-established" : "[foir] the subscription socket closed before the credential was accepted"
|
|
1518
1538
|
)
|
|
1519
1539
|
);
|
|
1520
1540
|
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Reconnect after the socket's credential lapsed: with the bearer renewed
|
|
1543
|
+
* through the client's `refreshBearer` where it has one, failing every
|
|
1544
|
+
* subscription with `FoirSessionExpiredError` when the session is over. A
|
|
1545
|
+
* client without one reconnects at once, presenting what its bearer
|
|
1546
|
+
* provider gives now.
|
|
1547
|
+
*/
|
|
1548
|
+
async reconnectWithARenewedBearer() {
|
|
1549
|
+
const refresh = this.config.refreshBearer;
|
|
1550
|
+
if (refresh) {
|
|
1551
|
+
const token = await renewBearerOnce(refresh).catch(() => null);
|
|
1552
|
+
if (this.disposed) return;
|
|
1553
|
+
if (!token) {
|
|
1554
|
+
this.failAll(new FoirSessionExpiredError());
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
this.renewedBearer = token;
|
|
1558
|
+
}
|
|
1559
|
+
this.attempt = 0;
|
|
1560
|
+
this.ensureConnected();
|
|
1561
|
+
}
|
|
1521
1562
|
scheduleReconnect(error) {
|
|
1522
1563
|
if (this.disposed || this.registrations.size === 0) return;
|
|
1523
1564
|
if (this.attempt >= this.policy.maxAttempts) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
1
2
|
import type { ProgressiveImageSource, ProgressiveVideoSource, VariantTierKey } from './media';
|
|
2
3
|
export interface ProgressiveMediaProps {
|
|
3
4
|
image?: ProgressiveImageSource;
|
|
@@ -25,6 +26,31 @@ export interface ProgressiveMediaProps {
|
|
|
25
26
|
muted?: boolean;
|
|
26
27
|
/** Video playback — loop on end. Default true. */
|
|
27
28
|
loop?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Video playback: show the browser's controls. Defaults to `!autoplay`. A
|
|
31
|
+
* video with controls shows from the start, its poster standing until it
|
|
32
|
+
* plays, so the controls can always be reached.
|
|
33
|
+
*/
|
|
34
|
+
controls?: boolean;
|
|
35
|
+
/** Video: how much the browser fetches before it plays (`<video preload>`). */
|
|
36
|
+
preload?: 'none' | 'metadata' | 'auto';
|
|
37
|
+
/**
|
|
38
|
+
* Where the media sits in its box when it is cropped to fit, as CSS
|
|
39
|
+
* `object-position` (`'30% 60%'`), for a focal point. Applies to the image,
|
|
40
|
+
* the video and its thumbnail. Default centre.
|
|
41
|
+
*/
|
|
42
|
+
objectPosition?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The `<video>` element, once it mounts: for a caller's own play and pause
|
|
45
|
+
* controls, and the events that keep them in step.
|
|
46
|
+
*/
|
|
47
|
+
videoRef?: Ref<HTMLVideoElement>;
|
|
48
|
+
/**
|
|
49
|
+
* Called once when a video's source cannot be played. The video is then
|
|
50
|
+
* removed, and its thumbnail, labelled with `alt`, stands in for it over
|
|
51
|
+
* the colour and blurhash layers.
|
|
52
|
+
*/
|
|
53
|
+
onVideoError?: () => void;
|
|
28
54
|
/**
|
|
29
55
|
* When false, skip loading the full media layer (no `<video>`/full `<img>`
|
|
30
56
|
* mount, no HLS attach, no full-image preload). The lower layers — colour,
|
|
@@ -53,4 +79,4 @@ export interface ProgressiveMediaProps {
|
|
|
53
79
|
*/
|
|
54
80
|
variantTiers?: readonly VariantTierKey[];
|
|
55
81
|
}
|
|
56
|
-
export declare function ProgressiveMedia({ image, video, alt, className, fill, fit, placeholder, autoplay, muted, loop, active, sizes, priority, variantTiers, }: ProgressiveMediaProps): import("react/jsx-runtime").JSX.Element | null;
|
|
82
|
+
export declare function ProgressiveMedia({ image, video, alt, className, fill, fit, placeholder, autoplay, muted, loop, controls, preload, objectPosition, videoRef: externalVideoRef, onVideoError, active, sizes, priority, variantTiers, }: ProgressiveMediaProps): import("react/jsx-runtime").JSX.Element | null;
|
package/dist/media.js
CHANGED
|
@@ -213,6 +213,11 @@ function ProgressiveMedia({
|
|
|
213
213
|
autoplay = true,
|
|
214
214
|
muted = true,
|
|
215
215
|
loop = true,
|
|
216
|
+
controls,
|
|
217
|
+
preload,
|
|
218
|
+
objectPosition,
|
|
219
|
+
videoRef: externalVideoRef,
|
|
220
|
+
onVideoError,
|
|
216
221
|
active = true,
|
|
217
222
|
sizes = "100vw",
|
|
218
223
|
priority = false,
|
|
@@ -225,7 +230,13 @@ function ProgressiveMedia({
|
|
|
225
230
|
const source = video ?? image;
|
|
226
231
|
const isVideo = !!video;
|
|
227
232
|
const [wrapperRef, inViewport] = useInViewport("300px", priority);
|
|
228
|
-
const
|
|
233
|
+
const [failedUrl, setFailedUrl] = useState2(void 0);
|
|
234
|
+
const videoFailed = isVideo && failedUrl !== void 0 && failedUrl === video?.url;
|
|
235
|
+
const [videoSize, setVideoSize] = useState2(
|
|
236
|
+
null
|
|
237
|
+
);
|
|
238
|
+
const videoActive = isVideo && active && !videoFailed && (priority || inViewport);
|
|
239
|
+
const showControls = controls ?? !autoplay;
|
|
229
240
|
const imageActive = !isVideo && active;
|
|
230
241
|
const hlsUrl = video?.hlsUrl;
|
|
231
242
|
const useHls = isVideo && !!hlsUrl;
|
|
@@ -282,6 +293,8 @@ function ProgressiveMedia({
|
|
|
282
293
|
cleanupRef.current = null;
|
|
283
294
|
}
|
|
284
295
|
videoRef.current = el;
|
|
296
|
+
if (typeof externalVideoRef === "function") externalVideoRef(el);
|
|
297
|
+
else if (externalVideoRef) externalVideoRef.current = el;
|
|
285
298
|
if (!el) return;
|
|
286
299
|
if (el.readyState >= 3) {
|
|
287
300
|
onFullReady();
|
|
@@ -299,7 +312,7 @@ function ProgressiveMedia({
|
|
|
299
312
|
el.removeEventListener("playing", handler);
|
|
300
313
|
};
|
|
301
314
|
},
|
|
302
|
-
[onFullReady]
|
|
315
|
+
[onFullReady, externalVideoRef]
|
|
303
316
|
);
|
|
304
317
|
if (!source) {
|
|
305
318
|
if (!placeholder) return null;
|
|
@@ -314,8 +327,9 @@ function ProgressiveMedia({
|
|
|
314
327
|
);
|
|
315
328
|
}
|
|
316
329
|
const effective = !isVideo ? effectiveImageDimensions(image) : null;
|
|
317
|
-
const w = effective?.width ?? source.width;
|
|
318
|
-
const h = effective?.height ?? source.height;
|
|
330
|
+
const w = effective?.width ?? source.width ?? videoSize?.w;
|
|
331
|
+
const h = effective?.height ?? source.height ?? videoSize?.h;
|
|
332
|
+
const positioned = objectPosition ? { objectPosition } : void 0;
|
|
319
333
|
const aspect = w && h ? w / h : 16 / 10;
|
|
320
334
|
return /* @__PURE__ */ jsxs(
|
|
321
335
|
"div",
|
|
@@ -342,13 +356,14 @@ function ProgressiveMedia({
|
|
|
342
356
|
"img",
|
|
343
357
|
{
|
|
344
358
|
src: thumbnailUrl,
|
|
345
|
-
alt: "",
|
|
346
|
-
"aria-hidden": true,
|
|
359
|
+
alt: videoFailed ? alt ?? "" : "",
|
|
360
|
+
"aria-hidden": videoFailed ? void 0 : true,
|
|
347
361
|
width: w,
|
|
348
362
|
height: h,
|
|
349
363
|
loading: priority ? "eager" : "lazy",
|
|
350
364
|
fetchPriority: priority ? "high" : void 0,
|
|
351
|
-
className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-400 ease-in-out ${stage === "thumbnail" || stage === "full" ? "opacity-100" : "opacity-0"}
|
|
365
|
+
className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-400 ease-in-out ${stage === "thumbnail" || stage === "full" ? "opacity-100" : "opacity-0"}`,
|
|
366
|
+
style: positioned
|
|
352
367
|
}
|
|
353
368
|
),
|
|
354
369
|
videoActive ? /* @__PURE__ */ jsx2(
|
|
@@ -363,8 +378,21 @@ function ProgressiveMedia({
|
|
|
363
378
|
muted,
|
|
364
379
|
loop,
|
|
365
380
|
playsInline: true,
|
|
366
|
-
controls:
|
|
367
|
-
|
|
381
|
+
controls: showControls,
|
|
382
|
+
preload,
|
|
383
|
+
"aria-label": alt || void 0,
|
|
384
|
+
onLoadedMetadata: (e) => {
|
|
385
|
+
const v = e.currentTarget;
|
|
386
|
+
if (!source.width && v.videoWidth && v.videoHeight) {
|
|
387
|
+
setVideoSize({ w: v.videoWidth, h: v.videoHeight });
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
onError: () => {
|
|
391
|
+
setFailedUrl(video?.url);
|
|
392
|
+
onVideoError?.();
|
|
393
|
+
},
|
|
394
|
+
className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-500 ease-in-out ${stage === "full" || showControls ? "opacity-100" : "opacity-0"}`,
|
|
395
|
+
style: positioned
|
|
368
396
|
}
|
|
369
397
|
) : imageActive ? (
|
|
370
398
|
// <picture>: browser picks AVIF when it can decode it, else WebP. The
|
|
@@ -383,7 +411,8 @@ function ProgressiveMedia({
|
|
|
383
411
|
height: h,
|
|
384
412
|
loading: priority ? "eager" : "lazy",
|
|
385
413
|
fetchPriority: priority ? "high" : void 0,
|
|
386
|
-
className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-500 ease-in-out ${stage === "full" ? "opacity-100" : "opacity-0"}
|
|
414
|
+
className: `absolute inset-0 h-full w-full ${fitClass} transition-opacity duration-500 ease-in-out ${stage === "full" ? "opacity-100" : "opacity-0"}`,
|
|
415
|
+
style: positioned
|
|
387
416
|
}
|
|
388
417
|
)
|
|
389
418
|
] })
|
package/dist/next/session.d.ts
CHANGED
|
@@ -27,8 +27,12 @@ interface PasswordArgs {
|
|
|
27
27
|
}
|
|
28
28
|
/** Password sign-in in a route handler; persists the session to cookies. */
|
|
29
29
|
export declare function foirSignInWithPassword({ env, store, email, password, cookieOptions, }: PasswordArgs): Promise<FoirUser>;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Start a sign-up in a route handler: the address is mailed a code, and
|
|
32
|
+
* {@link foirSignInWithOtp} with it finishes the account and persists the
|
|
33
|
+
* session to cookies.
|
|
34
|
+
*/
|
|
35
|
+
export declare function foirRegisterWithPassword({ env, email, password, }: Pick<PasswordArgs, 'env' | 'email' | 'password'>): Promise<void>;
|
|
32
36
|
/** Complete an OTP sign-in in a route handler; persists the session to cookies. */
|
|
33
37
|
export declare function foirSignInWithOtp({ env, store, email, otp, cookieOptions, }: {
|
|
34
38
|
env: FoirEnv;
|
package/dist/next.js
CHANGED
|
@@ -1127,8 +1127,8 @@ async function getSessionUser(adapter) {
|
|
|
1127
1127
|
async function signInWithPassword(adapter, env, vars) {
|
|
1128
1128
|
return saveSession(adapter, await login(env, vars));
|
|
1129
1129
|
}
|
|
1130
|
-
async function signUpWithPassword(
|
|
1131
|
-
|
|
1130
|
+
async function signUpWithPassword(env, vars) {
|
|
1131
|
+
await register(env, vars);
|
|
1132
1132
|
}
|
|
1133
1133
|
async function signInWithOTP(adapter, env, vars) {
|
|
1134
1134
|
return saveSession(adapter, await loginWithOTP(env, vars));
|
|
@@ -1158,15 +1158,10 @@ function foirSignInWithPassword({
|
|
|
1158
1158
|
}
|
|
1159
1159
|
function foirRegisterWithPassword({
|
|
1160
1160
|
env,
|
|
1161
|
-
store,
|
|
1162
1161
|
email,
|
|
1163
|
-
password
|
|
1164
|
-
cookieOptions
|
|
1162
|
+
password
|
|
1165
1163
|
}) {
|
|
1166
|
-
return signUpWithPassword(
|
|
1167
|
-
email,
|
|
1168
|
-
password
|
|
1169
|
-
});
|
|
1164
|
+
return signUpWithPassword(env, { email, password });
|
|
1170
1165
|
}
|
|
1171
1166
|
function foirSignInWithOtp({
|
|
1172
1167
|
env,
|
package/dist/placement.js
CHANGED
|
@@ -70,6 +70,7 @@ var GRANT_TABLE = [
|
|
|
70
70
|
["apikeys:write", "none", [], []],
|
|
71
71
|
["organizations:read", "none", [], []],
|
|
72
72
|
["organizations:write", "none", [], []],
|
|
73
|
+
["orgsettings:write", "none", [], []],
|
|
73
74
|
["organizations:create", "none", [], []],
|
|
74
75
|
["organizations:delete", "none", [], []],
|
|
75
76
|
["organizations:reach", "none", [], []],
|
|
@@ -80,7 +81,7 @@ var GRANT_TABLE = [
|
|
|
80
81
|
["projects:read", "none", [], []],
|
|
81
82
|
["projects:write", "none", [], []],
|
|
82
83
|
["project:stand", "none", [], []],
|
|
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", "
|
|
84
|
+
["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", "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", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
|
|
84
85
|
["projects:create", "none", [], []],
|
|
85
86
|
["projects:delete", "none", [], []],
|
|
86
87
|
["tenants:read", "none", [], []],
|
|
@@ -111,7 +112,6 @@ var GRANT_TABLE = [
|
|
|
111
112
|
["usersecrets:list", "none", [], []],
|
|
112
113
|
["usersecrets:put", "none", [], []],
|
|
113
114
|
["usersecrets:delete", "none", [], []],
|
|
114
|
-
["auth:login_with_foir", "none", [], []],
|
|
115
115
|
["signinproviders:read", "none", [], []],
|
|
116
116
|
["signinproviders:write", "none", [], []]
|
|
117
117
|
];
|
|
@@ -28,10 +28,14 @@ export interface FoirContextValue {
|
|
|
28
28
|
email: string;
|
|
29
29
|
password: string;
|
|
30
30
|
}): Promise<FoirUser>;
|
|
31
|
+
/**
|
|
32
|
+
* Start a sign-up: the address is mailed a code, and `signInWithOtp` with it
|
|
33
|
+
* finishes the account and signs them in.
|
|
34
|
+
*/
|
|
31
35
|
register(vars: {
|
|
32
36
|
email: string;
|
|
33
37
|
password: string;
|
|
34
|
-
}): Promise<
|
|
38
|
+
}): Promise<void>;
|
|
35
39
|
requestOtp(email: string): Promise<boolean>;
|
|
36
40
|
signInWithOtp(vars: {
|
|
37
41
|
email: string;
|
|
@@ -138,7 +142,7 @@ export declare function useFoirAuth(): {
|
|
|
138
142
|
register: (vars: {
|
|
139
143
|
email: string;
|
|
140
144
|
password: string;
|
|
141
|
-
}) => Promise<
|
|
145
|
+
}) => Promise<void>;
|
|
142
146
|
requestOtp: (email: string) => Promise<boolean>;
|
|
143
147
|
signInWithOtp: (vars: {
|
|
144
148
|
email: string;
|
package/dist/react.js
CHANGED
|
@@ -1133,8 +1133,8 @@ async function getSessionUser(adapter) {
|
|
|
1133
1133
|
async function signInWithPassword(adapter, env, vars) {
|
|
1134
1134
|
return saveSession(adapter, await login(env, vars));
|
|
1135
1135
|
}
|
|
1136
|
-
async function signUpWithPassword(
|
|
1137
|
-
|
|
1136
|
+
async function signUpWithPassword(env, vars) {
|
|
1137
|
+
await register(env, vars);
|
|
1138
1138
|
}
|
|
1139
1139
|
async function signInWithOTP(adapter, env, vars) {
|
|
1140
1140
|
return saveSession(adapter, await loginWithOTP(env, vars));
|
|
@@ -1280,6 +1280,7 @@ var GRANT_TABLE = [
|
|
|
1280
1280
|
["apikeys:write", "none", [], []],
|
|
1281
1281
|
["organizations:read", "none", [], []],
|
|
1282
1282
|
["organizations:write", "none", [], []],
|
|
1283
|
+
["orgsettings:write", "none", [], []],
|
|
1283
1284
|
["organizations:create", "none", [], []],
|
|
1284
1285
|
["organizations:delete", "none", [], []],
|
|
1285
1286
|
["organizations:reach", "none", [], []],
|
|
@@ -1290,7 +1291,7 @@ var GRANT_TABLE = [
|
|
|
1290
1291
|
["projects:read", "none", [], []],
|
|
1291
1292
|
["projects:write", "none", [], []],
|
|
1292
1293
|
["project:stand", "none", [], []],
|
|
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", "
|
|
1294
|
+
["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", "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", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
|
|
1294
1295
|
["projects:create", "none", [], []],
|
|
1295
1296
|
["projects:delete", "none", [], []],
|
|
1296
1297
|
["tenants:read", "none", [], []],
|
|
@@ -1321,7 +1322,6 @@ var GRANT_TABLE = [
|
|
|
1321
1322
|
["usersecrets:list", "none", [], []],
|
|
1322
1323
|
["usersecrets:put", "none", [], []],
|
|
1323
1324
|
["usersecrets:delete", "none", [], []],
|
|
1324
|
-
["auth:login_with_foir", "none", [], []],
|
|
1325
1325
|
["signinproviders:read", "none", [], []],
|
|
1326
1326
|
["signinproviders:write", "none", [], []]
|
|
1327
1327
|
];
|
|
@@ -1680,13 +1680,11 @@ function FoirProvider({
|
|
|
1680
1680
|
[apply, assertTokenMode]
|
|
1681
1681
|
);
|
|
1682
1682
|
const register2 = useCallback(
|
|
1683
|
-
|
|
1683
|
+
(vars) => {
|
|
1684
1684
|
assertTokenMode("register");
|
|
1685
|
-
|
|
1686
|
-
apply(c);
|
|
1687
|
-
return c;
|
|
1685
|
+
return signUpWithPassword(envRef.current, vars);
|
|
1688
1686
|
},
|
|
1689
|
-
[
|
|
1687
|
+
[assertTokenMode]
|
|
1690
1688
|
);
|
|
1691
1689
|
const requestOtp = useCallback(
|
|
1692
1690
|
(email) => {
|
package/dist/realtime/index.d.ts
CHANGED
|
@@ -71,6 +71,12 @@ export interface CreateCollabSessionOptions {
|
|
|
71
71
|
room: RoomAddress | string;
|
|
72
72
|
/** Async token source. Invoked before the first connect and on each refresh tick. */
|
|
73
73
|
getToken: () => string | Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* The organization the signed-in user stands at in the room, as a request
|
|
76
|
+
* names it. Omit for the project. The platform admits the join only where
|
|
77
|
+
* the user is a member.
|
|
78
|
+
*/
|
|
79
|
+
orgId?: string;
|
|
74
80
|
/** Caller-owned `Y.Doc` carrying the app-defined shared types. */
|
|
75
81
|
doc: Y.Doc;
|
|
76
82
|
/** Local presence. Sets awareness under the `user` field (contract shape). Omit for no presence. */
|
package/dist/realtime.js
CHANGED
|
@@ -39,12 +39,13 @@ function createCollabSession(options) {
|
|
|
39
39
|
refreshMs = 4 * 60 * 1e3,
|
|
40
40
|
connect = true,
|
|
41
41
|
params: extraParams,
|
|
42
|
+
orgId,
|
|
42
43
|
WebSocketPolyfill
|
|
43
44
|
} = options;
|
|
44
45
|
const room = roomPath(options.room);
|
|
45
46
|
const provider = new WebsocketProvider(url, room, doc, {
|
|
46
47
|
connect: false,
|
|
47
|
-
params: { ...extraParams },
|
|
48
|
+
params: { ...extraParams, ...orgId ? { org: orgId } : {} },
|
|
48
49
|
...WebSocketPolyfill ? { WebSocketPolyfill } : {}
|
|
49
50
|
});
|
|
50
51
|
const awareness = provider.awareness;
|
package/dist/server.js
CHANGED
|
@@ -144,6 +144,7 @@ var GRANT_TABLE = [
|
|
|
144
144
|
["apikeys:write", "none", [], []],
|
|
145
145
|
["organizations:read", "none", [], []],
|
|
146
146
|
["organizations:write", "none", [], []],
|
|
147
|
+
["orgsettings:write", "none", [], []],
|
|
147
148
|
["organizations:create", "none", [], []],
|
|
148
149
|
["organizations:delete", "none", [], []],
|
|
149
150
|
["organizations:reach", "none", [], []],
|
|
@@ -154,7 +155,7 @@ var GRANT_TABLE = [
|
|
|
154
155
|
["projects:read", "none", [], []],
|
|
155
156
|
["projects:write", "none", [], []],
|
|
156
157
|
["project:stand", "none", [], []],
|
|
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", "
|
|
158
|
+
["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", "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", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
|
|
158
159
|
["projects:create", "none", [], []],
|
|
159
160
|
["projects:delete", "none", [], []],
|
|
160
161
|
["tenants:read", "none", [], []],
|
|
@@ -185,7 +186,6 @@ var GRANT_TABLE = [
|
|
|
185
186
|
["usersecrets:list", "none", [], []],
|
|
186
187
|
["usersecrets:put", "none", [], []],
|
|
187
188
|
["usersecrets:delete", "none", [], []],
|
|
188
|
-
["auth:login_with_foir", "none", [], []],
|
|
189
189
|
["signinproviders:read", "none", [], []],
|
|
190
190
|
["signinproviders:write", "none", [], []]
|
|
191
191
|
];
|
|
@@ -1107,8 +1107,18 @@ async function resolveFoirAuth(config) {
|
|
|
1107
1107
|
key: { apiKey: config.apiKey, bearerToken }
|
|
1108
1108
|
};
|
|
1109
1109
|
}
|
|
1110
|
+
var renewing = /* @__PURE__ */ new WeakMap();
|
|
1111
|
+
function renewBearerOnce(refresh) {
|
|
1112
|
+
const running = renewing.get(refresh);
|
|
1113
|
+
if (running) return running;
|
|
1114
|
+
const next = refresh().finally(() => renewing.delete(refresh));
|
|
1115
|
+
renewing.set(refresh, next);
|
|
1116
|
+
return next;
|
|
1117
|
+
}
|
|
1110
1118
|
|
|
1111
1119
|
// src/data/ws-transport.ts
|
|
1120
|
+
var CLOSE_CREDENTIAL_ENDED = 4402;
|
|
1121
|
+
var CLOSE_FORBIDDEN = 4403;
|
|
1112
1122
|
var GQL = {
|
|
1113
1123
|
connectionInit: "connection_init",
|
|
1114
1124
|
connectionAck: "connection_ack",
|
|
@@ -1143,6 +1153,9 @@ var FoirSocketTransport = class {
|
|
|
1143
1153
|
disposed = false;
|
|
1144
1154
|
keepAlive = null;
|
|
1145
1155
|
retryTimer = null;
|
|
1156
|
+
// A bearer renewed after the socket's lapsed (4402), presented from the next
|
|
1157
|
+
// connect on in place of the client's own.
|
|
1158
|
+
renewedBearer = null;
|
|
1146
1159
|
constructor(config) {
|
|
1147
1160
|
if (config.dispatch !== void 0) {
|
|
1148
1161
|
throw new FoirRequestError(
|
|
@@ -1215,12 +1228,14 @@ var FoirSocketTransport = class {
|
|
|
1215
1228
|
socket.onmessage = (event) => this.receive(event.data);
|
|
1216
1229
|
socket.onerror = () => {
|
|
1217
1230
|
};
|
|
1218
|
-
socket.onclose = () => this.handleClose();
|
|
1231
|
+
socket.onclose = (event) => this.handleClose(event);
|
|
1219
1232
|
}
|
|
1220
1233
|
async sendConnectionInit(socket) {
|
|
1221
1234
|
this.connecting = false;
|
|
1222
1235
|
try {
|
|
1223
|
-
const { headers } = await resolveFoirAuth(
|
|
1236
|
+
const { headers } = await resolveFoirAuth(
|
|
1237
|
+
this.renewedBearer ? { ...this.config, bearerToken: this.renewedBearer } : this.config
|
|
1238
|
+
);
|
|
1224
1239
|
const { "Content-Type": _contentType, ...params } = headers;
|
|
1225
1240
|
const extra = await this.config.connectionParams?.() ?? {};
|
|
1226
1241
|
this.send(socket, {
|
|
@@ -1234,19 +1249,53 @@ var FoirSocketTransport = class {
|
|
|
1234
1249
|
this.teardownSocket(true);
|
|
1235
1250
|
}
|
|
1236
1251
|
}
|
|
1237
|
-
handleClose() {
|
|
1252
|
+
handleClose(event) {
|
|
1253
|
+
const code = event?.code;
|
|
1238
1254
|
const wasAcknowledged = this.acknowledged;
|
|
1239
1255
|
this.teardownSocket(false);
|
|
1240
1256
|
if (this.disposed || this.registrations.size === 0) return;
|
|
1241
1257
|
for (const registration of this.registrations.values()) {
|
|
1242
1258
|
registration.sent = false;
|
|
1243
1259
|
}
|
|
1260
|
+
if (code === CLOSE_FORBIDDEN) {
|
|
1261
|
+
this.failAll(
|
|
1262
|
+
new FoirRequestError(
|
|
1263
|
+
"[foir] the subscription socket was refused where it asked to stand: the signed-in user is no member of the organization this client names"
|
|
1264
|
+
)
|
|
1265
|
+
);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (code === CLOSE_CREDENTIAL_ENDED) {
|
|
1269
|
+
void this.reconnectWithARenewedBearer();
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1244
1272
|
this.scheduleReconnect(
|
|
1245
1273
|
new FoirRequestError(
|
|
1246
1274
|
wasAcknowledged ? "[foir] the subscription socket closed and could not be re-established" : "[foir] the subscription socket closed before the credential was accepted"
|
|
1247
1275
|
)
|
|
1248
1276
|
);
|
|
1249
1277
|
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Reconnect after the socket's credential lapsed: with the bearer renewed
|
|
1280
|
+
* through the client's `refreshBearer` where it has one, failing every
|
|
1281
|
+
* subscription with `FoirSessionExpiredError` when the session is over. A
|
|
1282
|
+
* client without one reconnects at once, presenting what its bearer
|
|
1283
|
+
* provider gives now.
|
|
1284
|
+
*/
|
|
1285
|
+
async reconnectWithARenewedBearer() {
|
|
1286
|
+
const refresh = this.config.refreshBearer;
|
|
1287
|
+
if (refresh) {
|
|
1288
|
+
const token = await renewBearerOnce(refresh).catch(() => null);
|
|
1289
|
+
if (this.disposed) return;
|
|
1290
|
+
if (!token) {
|
|
1291
|
+
this.failAll(new FoirSessionExpiredError());
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
this.renewedBearer = token;
|
|
1295
|
+
}
|
|
1296
|
+
this.attempt = 0;
|
|
1297
|
+
this.ensureConnected();
|
|
1298
|
+
}
|
|
1250
1299
|
scheduleReconnect(error) {
|
|
1251
1300
|
if (this.disposed || this.registrations.size === 0) return;
|
|
1252
1301
|
if (this.attempt >= this.policy.maxAttempts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foir/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Official Foir SDK — editor iframe client + server-side platform helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -173,12 +173,12 @@
|
|
|
173
173
|
"react-dom": "^19.2.0",
|
|
174
174
|
"tsup": "^8.5.1",
|
|
175
175
|
"typescript": "5.9.2",
|
|
176
|
-
"vitest": "^
|
|
176
|
+
"vitest": "^5.0.5",
|
|
177
177
|
"ws": "^8.18.0",
|
|
178
178
|
"y-websocket": "^3.0.0",
|
|
179
179
|
"yjs": "^13.6.30",
|
|
180
|
-
"@foir/
|
|
181
|
-
"@foir/
|
|
180
|
+
"@foir/access-vocab": "0.0.0",
|
|
181
|
+
"@foir/proto-ts": "0.109.0"
|
|
182
182
|
},
|
|
183
183
|
"engines": {
|
|
184
184
|
"node": ">=18.0.0"
|