@molecule/app-bonds-default-react 2.0.0 → 3.0.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/README.md +41 -37
- package/dist/auth.d.ts +35 -31
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +58 -38
- package/dist/setup.d.ts +1 -1
- package/dist/setup.js +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-25T20:03:32.757Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/app-bonds-default-react
|
|
@@ -113,7 +113,9 @@ this same wiring. Pass `stripApiPrefix: true` when the app uses
|
|
|
113
113
|
when the API reads another scheme than `Bearer`: it is an option of THIS
|
|
114
114
|
constructor (`createDefaultHttpClientWithAuthBearer({ authScheme: 'Token', … })`,
|
|
115
115
|
or `createFetchClient({ baseURL, authScheme: 'Token' })` directly), never a
|
|
116
|
-
property of the client it returns.
|
|
116
|
+
property of the client it returns. The client is built with the same scheme, so
|
|
117
|
+
a token the client also holds — the one a default auth bond's token sync gives
|
|
118
|
+
it — is sent with that scheme too, not as `Bearer`.
|
|
117
119
|
|
|
118
120
|
```typescript
|
|
119
121
|
function createDefaultHttpClientWithAuthBearer(opts: {
|
|
@@ -128,19 +130,16 @@ function createDefaultHttpClientWithAuthBearer(opts: {
|
|
|
128
130
|
#### `createDefaultAuthClient(authConfig)`
|
|
129
131
|
|
|
130
132
|
Builds the default JWT auth client + wires it into `@molecule/app-auth`.
|
|
131
|
-
|
|
132
|
-
Replaces the 16-line per-app `bonds/auth-default.ts` that 93 fleet
|
|
133
|
-
apps shipped byte-identically. Apps pass their own `authConfig`
|
|
134
|
-
(which lives in `src/config.ts`).
|
|
133
|
+
Apps pass their own `authConfig` (which lives in `src/config.ts`).
|
|
135
134
|
|
|
136
135
|
`TUser` is the profile the app's pages read, and may be any JSON object type —
|
|
137
|
-
including a profile with NO `id` (`{ username, email, bio, image }`),
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
including a profile with NO `id` (`{ username, email, bio, image }`), as in the
|
|
137
|
+
factory it wraps. Never add a synthetic `id` to satisfy a type: nothing in the
|
|
138
|
+
client reads it, and the fabricated value then travels into storage, `getUser()`
|
|
139
|
+
and every page. Omit the type parameter and the default is `UserProfile`, whose
|
|
140
|
+
`id: string` stays required — the profile is never inferred from `authConfig`
|
|
141
|
+
(not even from an annotated config variable), so an untyped wrapper call keeps
|
|
142
|
+
the default contract.
|
|
144
143
|
|
|
145
144
|
Type the config with the SAME type (`const authConfig: AuthClientConfig<MyUser> = { … }`)
|
|
146
145
|
so its `mapAuthResponse` is checked against `MyUser`, and name that type at the
|
|
@@ -151,8 +150,8 @@ argument — that is a 1.8 caller shape this major changes (see
|
|
|
151
150
|
`AuthClientConfig`'s migration note in `@molecule/app-auth`).
|
|
152
151
|
|
|
153
152
|
`TRegister` is the app's own registration fields (`{ username: string }` for a
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
RealWorld-shaped API), passed on to `@molecule/app-auth`'s factory so
|
|
154
|
+
`authClient.register({ username, email, password })` type-checks with no cast.
|
|
156
155
|
|
|
157
156
|
```typescript
|
|
158
157
|
function createDefaultAuthClient(authConfig: NoInfer<AuthClientConfigOptions<TUser>>): {
|
|
@@ -164,26 +163,28 @@ function createDefaultAuthClient(authConfig: NoInfer<AuthClientConfigOptions<TUs
|
|
|
164
163
|
#### `createDefaultAuthClientWithHttpSync(authConfig)`
|
|
165
164
|
|
|
166
165
|
Variant of `createDefaultAuthClient` that also keeps the bonded
|
|
167
|
-
`@molecule/app-http` client's bearer token in sync with auth
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
`@molecule/app-http` client's bearer token in sync with the auth client's session.
|
|
167
|
+
Required by apps whose http client must carry the JWT on every request — without
|
|
168
|
+
this wiring, authed endpoints return 401 after page reloads or token refresh.
|
|
169
|
+
|
|
170
|
+
The sync is a LIVE read: `@molecule/app-http`'s `setAuthTokenSource` registers the auth
|
|
171
|
+
client's token, and every client the app bonds — one bonded after this setup too (the
|
|
172
|
+
scaffold wires auth BEFORE http, so a token copied into the client present at setup was
|
|
173
|
+
never sent) — reads it on each request. A session adopted with no
|
|
174
|
+
auth event (`useAuth().setSession()`, an OAuth callback's `setAccessToken()`, the same
|
|
175
|
+
profile under a new token) is therefore sent by the next request, not the previous
|
|
176
|
+
account's bearer. A transport without `setAuthTokenResolver` is followed through a request
|
|
177
|
+
interceptor (see `setAuthTokenSource` for which request first carries the change), and
|
|
178
|
+
auth events still push the token to it.
|
|
179
|
+
|
|
180
|
+
A cookie session (no body token) resolves to `null`, so the request carries NO
|
|
181
|
+
`Authorization` header — a kept bearer would answer as the PRIOR session.
|
|
182
|
+
Requests also wait for the session restore, so a page that fetches viewer data on mount
|
|
183
|
+
cannot race it (see {@link registerAuthGate}).
|
|
171
184
|
|
|
172
185
|
The profile type is `createDefaultAuthClient`'s: a JSON object type of named
|
|
173
186
|
fields (see `AuthProfileShape`), including an app profile with no `id`.
|
|
174
187
|
|
|
175
|
-
Hydrates the HTTP client's token from the persisted auth state
|
|
176
|
-
on setup, then listens for auth events to keep them aligned, and holds
|
|
177
|
-
requests until the session restore settles so a page effect that fetches
|
|
178
|
-
viewer-dependent data on mount cannot race it (see {@link registerAuthGate}).
|
|
179
|
-
|
|
180
|
-
The hydrated token is delivered through `@molecule/app-http`'s token source, not
|
|
181
|
-
through a client read at setup: the scaffold wires auth BEFORE http, so a client
|
|
182
|
-
read here is the unbonded fallback and the restored session's token would land on a
|
|
183
|
-
client nothing sends through (xr-molecule-flash-m156 — a reload sent every request
|
|
184
|
-
with no `Authorization` header). The source is applied to the client WHENEVER it is
|
|
185
|
-
bonded, so setup order no longer decides whether the first request is authorized.
|
|
186
|
-
|
|
187
188
|
```typescript
|
|
188
189
|
function createDefaultAuthClientWithHttpSync(authConfig: NoInfer<AuthClientConfigOptions<TUser>>): {
|
|
189
190
|
authClient: AuthClient<TUser, TRegister>
|
|
@@ -196,15 +197,18 @@ function createDefaultAuthClientWithHttpSync(authConfig: NoInfer<AuthClientConfi
|
|
|
196
197
|
Most aggressive variant — bonds a fetch-based HTTP client with the
|
|
197
198
|
given `baseURL` (so molecule pkg `useGet('/path')` calls hit
|
|
198
199
|
`${baseURL}/path` instead of the SPA's catch-all route), AND keeps
|
|
199
|
-
its bearer token in sync with auth
|
|
200
|
+
its bearer token in sync with the auth client's session, read on every
|
|
201
|
+
request: a session adopted with no auth event (`setSession()`,
|
|
202
|
+
`setAccessToken()`) is sent by the next request. The token uses the auth
|
|
203
|
+
config's `authScheme` (default `Bearer`); a client bonded after this setup
|
|
204
|
+
gets the same source and auth events.
|
|
200
205
|
|
|
201
206
|
The auth client restores a stored token AND user SYNCHRONOUSLY, so the first
|
|
202
207
|
render is the signed-in UI — do not gate a protected page (or a provider of
|
|
203
208
|
yours) on a current-user request. This bond registers `whenRestored()` as the
|
|
204
209
|
HTTP gate, so requests still wait for the background restore/verification to
|
|
205
210
|
settle; only `useAuth().status === 'loading'` means "no decision yet"
|
|
206
|
-
(
|
|
207
|
-
on every navigation).
|
|
211
|
+
(a page that waits on its own `/me` pays a round trip on every navigation).
|
|
208
212
|
|
|
209
213
|
Used by apps where molecule packages render pricing / billing /
|
|
210
214
|
other authed JSON-fetching screens that need both behaviors.
|
|
@@ -282,7 +286,7 @@ WITHOUT this, `createStore()` throws
|
|
|
282
286
|
`@molecule/app-state: No provider set. Call setProvider() first` — nothing
|
|
283
287
|
else in a scaffolded app wires the `state` bond, so the documented
|
|
284
288
|
`createStore()` + `useStore()` pattern failed at the first render and every
|
|
285
|
-
page came up blank while `tsc` passed
|
|
289
|
+
page came up blank while `tsc` passed.
|
|
286
290
|
|
|
287
291
|
```typescript
|
|
288
292
|
function setupAppStateSimple(): void
|
|
@@ -343,7 +347,7 @@ function setupAllDefaultBonds(): void
|
|
|
343
347
|
|
|
344
348
|
Peer dependencies:
|
|
345
349
|
|
|
346
|
-
- `@molecule/app-auth` ^
|
|
350
|
+
- `@molecule/app-auth` ^3.0.0
|
|
347
351
|
- `@molecule/app-charts` ^1.0.1
|
|
348
352
|
- `@molecule/app-charts-chartjs` ^1.0.1
|
|
349
353
|
- `@molecule/app-client-search` ^1.0.0
|
|
@@ -356,7 +360,7 @@ Peer dependencies:
|
|
|
356
360
|
- `@molecule/app-drag-drop-dndkit` ^1.0.1
|
|
357
361
|
- `@molecule/app-fonts` ^1.0.1
|
|
358
362
|
- `@molecule/app-fonts-arimo` ^1.0.1
|
|
359
|
-
- `@molecule/app-http` ^1.0
|
|
363
|
+
- `@molecule/app-http` ^1.5.0
|
|
360
364
|
- `@molecule/app-icons` ^1.0.1
|
|
361
365
|
- `@molecule/app-icons-molecule` ^1.0.1
|
|
362
366
|
- `@molecule/app-keyboard-shortcuts` ^1.0.1
|
package/dist/auth.d.ts
CHANGED
|
@@ -30,7 +30,9 @@ export declare function createDefaultHttpClient(baseURL: string): {
|
|
|
30
30
|
* when the API reads another scheme than `Bearer`: it is an option of THIS
|
|
31
31
|
* constructor (`createDefaultHttpClientWithAuthBearer({ authScheme: 'Token', … })`,
|
|
32
32
|
* or `createFetchClient({ baseURL, authScheme: 'Token' })` directly), never a
|
|
33
|
-
* property of the client it returns.
|
|
33
|
+
* property of the client it returns. The client is built with the same scheme, so
|
|
34
|
+
* a token the client also holds — the one a default auth bond's token sync gives
|
|
35
|
+
* it — is sent with that scheme too, not as `Bearer`.
|
|
34
36
|
*/
|
|
35
37
|
export declare function createDefaultHttpClientWithAuthBearer(opts: {
|
|
36
38
|
baseURL: string;
|
|
@@ -51,19 +53,16 @@ export declare function createDefaultHttpClientWithAuthBearer(opts: {
|
|
|
51
53
|
};
|
|
52
54
|
/**
|
|
53
55
|
* Builds the default JWT auth client + wires it into `@molecule/app-auth`.
|
|
54
|
-
*
|
|
55
|
-
* Replaces the 16-line per-app `bonds/auth-default.ts` that 93 fleet
|
|
56
|
-
* apps shipped byte-identically. Apps pass their own `authConfig`
|
|
57
|
-
* (which lives in `src/config.ts`).
|
|
56
|
+
* Apps pass their own `authConfig` (which lives in `src/config.ts`).
|
|
58
57
|
*
|
|
59
58
|
* `TUser` is the profile the app's pages read, and may be any JSON object type —
|
|
60
|
-
* including a profile with NO `id` (`{ username, email, bio, image }`),
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
66
|
-
*
|
|
59
|
+
* including a profile with NO `id` (`{ username, email, bio, image }`), as in the
|
|
60
|
+
* factory it wraps. Never add a synthetic `id` to satisfy a type: nothing in the
|
|
61
|
+
* client reads it, and the fabricated value then travels into storage, `getUser()`
|
|
62
|
+
* and every page. Omit the type parameter and the default is `UserProfile`, whose
|
|
63
|
+
* `id: string` stays required — the profile is never inferred from `authConfig`
|
|
64
|
+
* (not even from an annotated config variable), so an untyped wrapper call keeps
|
|
65
|
+
* the default contract.
|
|
67
66
|
*
|
|
68
67
|
* Type the config with the SAME type (`const authConfig: AuthClientConfig<MyUser> = { … }`)
|
|
69
68
|
* so its `mapAuthResponse` is checked against `MyUser`, and name that type at the
|
|
@@ -93,25 +92,27 @@ export declare function createDefaultAuthClient<TUser extends AuthProfileConstra
|
|
|
93
92
|
};
|
|
94
93
|
/**
|
|
95
94
|
* Variant of `createDefaultAuthClient` that also keeps the bonded
|
|
96
|
-
* `@molecule/app-http` client's bearer token in sync with auth
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
95
|
+
* `@molecule/app-http` client's bearer token in sync with the auth client's session.
|
|
96
|
+
* Required by apps whose http client must carry the JWT on every request — without
|
|
97
|
+
* this wiring, authed endpoints return 401 after page reloads or token refresh.
|
|
98
|
+
*
|
|
99
|
+
* The sync is a LIVE read: `@molecule/app-http`'s `setAuthTokenSource` registers the auth
|
|
100
|
+
* client's token, and every client the app bonds — one bonded after this setup too (the
|
|
101
|
+
* scaffold wires auth BEFORE http, so a token copied into the client present at setup was
|
|
102
|
+
* never sent) — reads it on each request. A session adopted with no
|
|
103
|
+
* auth event (`useAuth().setSession()`, an OAuth callback's `setAccessToken()`, the same
|
|
104
|
+
* profile under a new token) is therefore sent by the next request, not the previous
|
|
105
|
+
* account's bearer. A transport without `setAuthTokenResolver` is followed through a request
|
|
106
|
+
* interceptor (see `setAuthTokenSource` for which request first carries the change), and
|
|
107
|
+
* auth events still push the token to it.
|
|
108
|
+
*
|
|
109
|
+
* A cookie session (no body token) resolves to `null`, so the request carries NO
|
|
110
|
+
* `Authorization` header — a kept bearer would answer as the PRIOR session.
|
|
111
|
+
* Requests also wait for the session restore, so a page that fetches viewer data on mount
|
|
112
|
+
* cannot race it (see {@link registerAuthGate}).
|
|
100
113
|
*
|
|
101
114
|
* The profile type is `createDefaultAuthClient`'s: a JSON object type of named
|
|
102
115
|
* fields (see `AuthProfileShape`), including an app profile with no `id`.
|
|
103
|
-
*
|
|
104
|
-
* Hydrates the HTTP client's token from the persisted auth state
|
|
105
|
-
* on setup, then listens for auth events to keep them aligned, and holds
|
|
106
|
-
* requests until the session restore settles so a page effect that fetches
|
|
107
|
-
* viewer-dependent data on mount cannot race it (see {@link registerAuthGate}).
|
|
108
|
-
*
|
|
109
|
-
* The hydrated token is delivered through `@molecule/app-http`'s token source, not
|
|
110
|
-
* through a client read at setup: the scaffold wires auth BEFORE http, so a client
|
|
111
|
-
* read here is the unbonded fallback and the restored session's token would land on a
|
|
112
|
-
* client nothing sends through (xr-molecule-flash-m156 — a reload sent every request
|
|
113
|
-
* with no `Authorization` header). The source is applied to the client WHENEVER it is
|
|
114
|
-
* bonded, so setup order no longer decides whether the first request is authorized.
|
|
115
116
|
*/
|
|
116
117
|
export declare function createDefaultAuthClientWithHttpSync<TUser extends AuthProfileConstraint<TUser> = UserProfile, TRegister extends RegisterExtraShape<TRegister> = Record<never, never>>(authConfig: NoInfer<AuthClientConfigOptions<TUser>>): {
|
|
117
118
|
authClient: AuthClient<TUser, TRegister>;
|
|
@@ -121,15 +122,18 @@ export declare function createDefaultAuthClientWithHttpSync<TUser extends AuthPr
|
|
|
121
122
|
* Most aggressive variant — bonds a fetch-based HTTP client with the
|
|
122
123
|
* given `baseURL` (so molecule pkg `useGet('/path')` calls hit
|
|
123
124
|
* `${baseURL}/path` instead of the SPA's catch-all route), AND keeps
|
|
124
|
-
* its bearer token in sync with auth
|
|
125
|
+
* its bearer token in sync with the auth client's session, read on every
|
|
126
|
+
* request: a session adopted with no auth event (`setSession()`,
|
|
127
|
+
* `setAccessToken()`) is sent by the next request. The token uses the auth
|
|
128
|
+
* config's `authScheme` (default `Bearer`); a client bonded after this setup
|
|
129
|
+
* gets the same source and auth events.
|
|
125
130
|
*
|
|
126
131
|
* The auth client restores a stored token AND user SYNCHRONOUSLY, so the first
|
|
127
132
|
* render is the signed-in UI — do not gate a protected page (or a provider of
|
|
128
133
|
* yours) on a current-user request. This bond registers `whenRestored()` as the
|
|
129
134
|
* HTTP gate, so requests still wait for the background restore/verification to
|
|
130
135
|
* settle; only `useAuth().status === 'loading'` means "no decision yet"
|
|
131
|
-
* (
|
|
132
|
-
* on every navigation).
|
|
136
|
+
* (a page that waits on its own `/me` pays a round trip on every navigation).
|
|
133
137
|
*
|
|
134
138
|
* Used by apps where molecule packages render pricing / billing /
|
|
135
139
|
* other authed JSON-fetching screens that need both behaviors.
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,uBAAuB,EAEvB,qBAAqB,EACrB,kBAAkB,EAClB,WAAW,EACZ,MAAM,oBAAoB,CAAA;AAE3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,uBAAuB,EAEvB,qBAAqB,EACrB,kBAAkB,EAClB,WAAW,EACZ,MAAM,oBAAoB,CAAA;AAE3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAsCpD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG;IACxD,UAAU,EAAE,UAAU,CAAA;IACtB,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B,CAMA;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qCAAqC,CAAC,IAAI,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACzC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GAAG;IACF,UAAU,EAAE,UAAU,CAAA;IACtB,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B,CAuBA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,SAAS,qBAAqB,CAAC,KAAK,CAAC,GAAG,WAAW,EACxD,SAAS,SAAS,kBAAkB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAEtE,UAAU,EAAE,OAAO,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,GAClD;IACD,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IACxC,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B,CAMA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mCAAmC,CACjD,KAAK,SAAS,qBAAqB,CAAC,KAAK,CAAC,GAAG,WAAW,EACxD,SAAS,SAAS,kBAAkB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAEtE,UAAU,EAAE,OAAO,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,GAClD;IACD,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IACxC,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B,CAkCA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,sCAAsC,CACpD,KAAK,SAAS,qBAAqB,CAAC,KAAK,CAAC,GAAG,WAAW,EACxD,SAAS,SAAS,kBAAkB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAEtE,UAAU,EAAE,OAAO,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,EACnD,kBAAkB,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACjE;IACD,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IACxC,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B,CAmCA"}
|
package/dist/auth.js
CHANGED
|
@@ -7,9 +7,8 @@ import { createFetchClient, getClient as getHttpClient, setAuthGate, setAuthToke
|
|
|
7
7
|
* flag, a feed, a `favorited` state) sends its request before `initialize()` has
|
|
8
8
|
* restored the session, so it leaves with no `Authorization` header and the server
|
|
9
9
|
* answers for an anonymous viewer. Localhost usually hides it — the restore wins
|
|
10
|
-
* the race against the first effect there — and the deployed URL exposes it,
|
|
11
|
-
*
|
|
12
|
-
* followed user as database leftovers.
|
|
10
|
+
* the race against the first effect there — and the deployed URL exposes it, where a
|
|
11
|
+
* "Follow" button on a followed user reads as stale database state.
|
|
13
12
|
*
|
|
14
13
|
* `whenRestored()` resolves immediately when no restore is in flight, so a request
|
|
15
14
|
* made after boot never waits at all; it holds only while the restore is actually
|
|
@@ -62,13 +61,18 @@ export function createDefaultHttpClient(baseURL) {
|
|
|
62
61
|
* when the API reads another scheme than `Bearer`: it is an option of THIS
|
|
63
62
|
* constructor (`createDefaultHttpClientWithAuthBearer({ authScheme: 'Token', … })`,
|
|
64
63
|
* or `createFetchClient({ baseURL, authScheme: 'Token' })` directly), never a
|
|
65
|
-
* property of the client it returns.
|
|
64
|
+
* property of the client it returns. The client is built with the same scheme, so
|
|
65
|
+
* a token the client also holds — the one a default auth bond's token sync gives
|
|
66
|
+
* it — is sent with that scheme too, not as `Bearer`.
|
|
66
67
|
*/
|
|
67
68
|
export function createDefaultHttpClientWithAuthBearer(opts) {
|
|
68
69
|
const authScheme = opts.authScheme ?? 'Bearer';
|
|
69
70
|
const httpClient = createFetchClient({
|
|
70
71
|
baseURL: opts.baseURL,
|
|
71
72
|
withCredentials: opts.withCredentials,
|
|
73
|
+
// The client's own token (a default auth bond's token sync) overrides the interceptor's
|
|
74
|
+
// header, so it must carry the same scheme.
|
|
75
|
+
authScheme,
|
|
72
76
|
});
|
|
73
77
|
httpClient.addRequestInterceptor((config) => {
|
|
74
78
|
if (opts.stripApiPrefix && typeof config.url === 'string' && config.url.startsWith('/api/')) {
|
|
@@ -87,19 +91,16 @@ export function createDefaultHttpClientWithAuthBearer(opts) {
|
|
|
87
91
|
}
|
|
88
92
|
/**
|
|
89
93
|
* Builds the default JWT auth client + wires it into `@molecule/app-auth`.
|
|
90
|
-
*
|
|
91
|
-
* Replaces the 16-line per-app `bonds/auth-default.ts` that 93 fleet
|
|
92
|
-
* apps shipped byte-identically. Apps pass their own `authConfig`
|
|
93
|
-
* (which lives in `src/config.ts`).
|
|
94
|
+
* Apps pass their own `authConfig` (which lives in `src/config.ts`).
|
|
94
95
|
*
|
|
95
96
|
* `TUser` is the profile the app's pages read, and may be any JSON object type —
|
|
96
|
-
* including a profile with NO `id` (`{ username, email, bio, image }`),
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* `
|
|
101
|
-
*
|
|
102
|
-
*
|
|
97
|
+
* including a profile with NO `id` (`{ username, email, bio, image }`), as in the
|
|
98
|
+
* factory it wraps. Never add a synthetic `id` to satisfy a type: nothing in the
|
|
99
|
+
* client reads it, and the fabricated value then travels into storage, `getUser()`
|
|
100
|
+
* and every page. Omit the type parameter and the default is `UserProfile`, whose
|
|
101
|
+
* `id: string` stays required — the profile is never inferred from `authConfig`
|
|
102
|
+
* (not even from an annotated config variable), so an untyped wrapper call keeps
|
|
103
|
+
* the default contract.
|
|
103
104
|
*
|
|
104
105
|
* Type the config with the SAME type (`const authConfig: AuthClientConfig<MyUser> = { … }`)
|
|
105
106
|
* so its `mapAuthResponse` is checked against `MyUser`, and name that type at the
|
|
@@ -132,25 +133,27 @@ export function createDefaultAuthClient(authConfig) {
|
|
|
132
133
|
}
|
|
133
134
|
/**
|
|
134
135
|
* Variant of `createDefaultAuthClient` that also keeps the bonded
|
|
135
|
-
* `@molecule/app-http` client's bearer token in sync with auth
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
136
|
+
* `@molecule/app-http` client's bearer token in sync with the auth client's session.
|
|
137
|
+
* Required by apps whose http client must carry the JWT on every request — without
|
|
138
|
+
* this wiring, authed endpoints return 401 after page reloads or token refresh.
|
|
139
|
+
*
|
|
140
|
+
* The sync is a LIVE read: `@molecule/app-http`'s `setAuthTokenSource` registers the auth
|
|
141
|
+
* client's token, and every client the app bonds — one bonded after this setup too (the
|
|
142
|
+
* scaffold wires auth BEFORE http, so a token copied into the client present at setup was
|
|
143
|
+
* never sent) — reads it on each request. A session adopted with no
|
|
144
|
+
* auth event (`useAuth().setSession()`, an OAuth callback's `setAccessToken()`, the same
|
|
145
|
+
* profile under a new token) is therefore sent by the next request, not the previous
|
|
146
|
+
* account's bearer. A transport without `setAuthTokenResolver` is followed through a request
|
|
147
|
+
* interceptor (see `setAuthTokenSource` for which request first carries the change), and
|
|
148
|
+
* auth events still push the token to it.
|
|
149
|
+
*
|
|
150
|
+
* A cookie session (no body token) resolves to `null`, so the request carries NO
|
|
151
|
+
* `Authorization` header — a kept bearer would answer as the PRIOR session.
|
|
152
|
+
* Requests also wait for the session restore, so a page that fetches viewer data on mount
|
|
153
|
+
* cannot race it (see {@link registerAuthGate}).
|
|
139
154
|
*
|
|
140
155
|
* The profile type is `createDefaultAuthClient`'s: a JSON object type of named
|
|
141
156
|
* fields (see `AuthProfileShape`), including an app profile with no `id`.
|
|
142
|
-
*
|
|
143
|
-
* Hydrates the HTTP client's token from the persisted auth state
|
|
144
|
-
* on setup, then listens for auth events to keep them aligned, and holds
|
|
145
|
-
* requests until the session restore settles so a page effect that fetches
|
|
146
|
-
* viewer-dependent data on mount cannot race it (see {@link registerAuthGate}).
|
|
147
|
-
*
|
|
148
|
-
* The hydrated token is delivered through `@molecule/app-http`'s token source, not
|
|
149
|
-
* through a client read at setup: the scaffold wires auth BEFORE http, so a client
|
|
150
|
-
* read here is the unbonded fallback and the restored session's token would land on a
|
|
151
|
-
* client nothing sends through (xr-molecule-flash-m156 — a reload sent every request
|
|
152
|
-
* with no `Authorization` header). The source is applied to the client WHENEVER it is
|
|
153
|
-
* bonded, so setup order no longer decides whether the first request is authorized.
|
|
154
157
|
*/
|
|
155
158
|
export function createDefaultAuthClientWithHttpSync(authConfig) {
|
|
156
159
|
const authClient = createJWTAuthClient(authConfig);
|
|
@@ -164,7 +167,9 @@ export function createDefaultAuthClientWithHttpSync(authConfig) {
|
|
|
164
167
|
// Resolve the HTTP client per sync, never once here. `setClient` runs this
|
|
165
168
|
// source, so the token reaches the client the scaffold bonds later as well as
|
|
166
169
|
// one already bonded; a client kept from this moment would be the throwaway
|
|
167
|
-
// fallback the later `setHttpDefault()` replaces.
|
|
170
|
+
// fallback the later `setHttpDefault()` replaces. The client reads the source
|
|
171
|
+
// itself on every request, so a session adopted by `setSession()` (no auth
|
|
172
|
+
// event) or re-adopted for the same profile (no state change) is sent at once.
|
|
168
173
|
setHttpAuthTokenSource(() => authClient.getAccessToken());
|
|
169
174
|
authClient.addEventListener((event) => {
|
|
170
175
|
if (event.type === 'login' || event.type === 'register' || event.type === 'refresh') {
|
|
@@ -173,6 +178,10 @@ export function createDefaultAuthClientWithHttpSync(authConfig) {
|
|
|
173
178
|
// keeping a previously-set bearer here would pin the http client to the
|
|
174
179
|
// PRIOR session (e.g. a guest's) — the server prefers the Authorization
|
|
175
180
|
// header over the fresh cookie, so every request 401s as the old user.
|
|
181
|
+
//
|
|
182
|
+
// A client that implements `setAuthTokenResolver` already resolves the source
|
|
183
|
+
// per request; this push is what carries the same change to a transport that
|
|
184
|
+
// does not (and keeps `getAuthToken()` current there).
|
|
176
185
|
getHttpClient().setAuthToken(authClient.getAccessToken());
|
|
177
186
|
}
|
|
178
187
|
else if (event.type === 'logout') {
|
|
@@ -186,15 +195,18 @@ export function createDefaultAuthClientWithHttpSync(authConfig) {
|
|
|
186
195
|
* Most aggressive variant — bonds a fetch-based HTTP client with the
|
|
187
196
|
* given `baseURL` (so molecule pkg `useGet('/path')` calls hit
|
|
188
197
|
* `${baseURL}/path` instead of the SPA's catch-all route), AND keeps
|
|
189
|
-
* its bearer token in sync with auth
|
|
198
|
+
* its bearer token in sync with the auth client's session, read on every
|
|
199
|
+
* request: a session adopted with no auth event (`setSession()`,
|
|
200
|
+
* `setAccessToken()`) is sent by the next request. The token uses the auth
|
|
201
|
+
* config's `authScheme` (default `Bearer`); a client bonded after this setup
|
|
202
|
+
* gets the same source and auth events.
|
|
190
203
|
*
|
|
191
204
|
* The auth client restores a stored token AND user SYNCHRONOUSLY, so the first
|
|
192
205
|
* render is the signed-in UI — do not gate a protected page (or a provider of
|
|
193
206
|
* yours) on a current-user request. This bond registers `whenRestored()` as the
|
|
194
207
|
* HTTP gate, so requests still wait for the background restore/verification to
|
|
195
208
|
* settle; only `useAuth().status === 'loading'` means "no decision yet"
|
|
196
|
-
* (
|
|
197
|
-
* on every navigation).
|
|
209
|
+
* (a page that waits on its own `/me` pays a round trip on every navigation).
|
|
198
210
|
*
|
|
199
211
|
* Used by apps where molecule packages render pricing / billing /
|
|
200
212
|
* other authed JSON-fetching screens that need both behaviors.
|
|
@@ -214,22 +226,30 @@ export function createDefaultAuthClientWithFetchClient(authConfig, fetchClientOp
|
|
|
214
226
|
const httpClient = createFetchClient({
|
|
215
227
|
baseURL: fetchClientOptions.baseURL,
|
|
216
228
|
withCredentials: fetchClientOptions.withCredentials ?? true,
|
|
229
|
+
// The scheme the auth config names for this token (`Authorization: Token <jwt>`).
|
|
230
|
+
authScheme: authConfig.authScheme,
|
|
217
231
|
});
|
|
218
232
|
// Registered BEFORE the bond: `setHttpClient` applies the restored session's
|
|
219
233
|
// token, and the same source reaches any client an app bonds later instead of
|
|
220
|
-
// this one (xr-molecule-flash-m156 — a client captured at setup cannot).
|
|
234
|
+
// this one (xr-molecule-flash-m156 — a client captured at setup cannot). The
|
|
235
|
+
// client resolves the source on every request, so a session adopted by
|
|
236
|
+
// `setSession()` or `setAccessToken()` after this point is sent immediately.
|
|
221
237
|
setHttpAuthTokenSource(() => authClient.getAccessToken());
|
|
222
238
|
setHttpClient(httpClient);
|
|
223
239
|
setClient(authClient);
|
|
224
240
|
registerAuthGate(authClient);
|
|
225
241
|
authClient.addEventListener((event) => {
|
|
242
|
+
// The client bonded NOW, resolved per event: an app that bonds its own client after
|
|
243
|
+
// this setup sends through that one, never through the client created above.
|
|
226
244
|
if (event.type === 'login' || event.type === 'register' || event.type === 'refresh') {
|
|
227
245
|
// Mirror the token exactly, including null — see the identical note in
|
|
228
246
|
// createDefaultAuthClientWithHttpSync (stale bearer = stuck prior session).
|
|
229
|
-
|
|
247
|
+
// The per-request read of the source above is the guarantee; this push keeps
|
|
248
|
+
// `getAuthToken()` current on a transport without `setAuthTokenResolver`.
|
|
249
|
+
getHttpClient().setAuthToken(authClient.getAccessToken());
|
|
230
250
|
}
|
|
231
251
|
else if (event.type === 'logout') {
|
|
232
|
-
|
|
252
|
+
getHttpClient().setAuthToken(null);
|
|
233
253
|
}
|
|
234
254
|
});
|
|
235
255
|
},
|
package/dist/setup.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare function setupAppRoutingReactRouter(): void;
|
|
|
29
29
|
* `@molecule/app-state: No provider set. Call setProvider() first` — nothing
|
|
30
30
|
* else in a scaffolded app wires the `state` bond, so the documented
|
|
31
31
|
* `createStore()` + `useStore()` pattern failed at the first render and every
|
|
32
|
-
* page came up blank while `tsc` passed
|
|
32
|
+
* page came up blank while `tsc` passed.
|
|
33
33
|
*/
|
|
34
34
|
export declare function setupAppStateSimple(): void;
|
|
35
35
|
/** Wires `@molecule/app-storage-localstorage` to `@molecule/app-storage`. */
|
package/dist/setup.js
CHANGED
|
@@ -68,7 +68,7 @@ export function setupAppRoutingReactRouter() {
|
|
|
68
68
|
* `@molecule/app-state: No provider set. Call setProvider() first` — nothing
|
|
69
69
|
* else in a scaffolded app wires the `state` bond, so the documented
|
|
70
70
|
* `createStore()` + `useStore()` pattern failed at the first render and every
|
|
71
|
-
* page came up blank while `tsc` passed
|
|
71
|
+
* page came up blank while `tsc` passed.
|
|
72
72
|
*/
|
|
73
73
|
export function setupAppStateSimple() {
|
|
74
74
|
setState(defaultStateProvider);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/app-bonds-default-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Default app-side bond wirings for React+Tailwind+react-router+localStorage+Arimo font+molecule icons. 5 setup functions consolidating 665 byte-identical per-app bond wiring files across the fleet.",
|
|
5
5
|
"homepage": "https://www.molecule.dev/packages/app-bonds-default-react",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"test": "vitest run",
|
|
12
|
-
"test:watch": "vitest"
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"typecheck:tests": "tsc -p tsconfig.tests.json"
|
|
13
14
|
},
|
|
14
15
|
"exports": {
|
|
15
16
|
".": {
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"directory": "packages/app/infrastructure/bonds-default-react"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@molecule/app-auth": "
|
|
42
|
+
"@molecule/app-auth": "3.0.0",
|
|
42
43
|
"@molecule/app-charts-chartjs": "1.0.3",
|
|
43
44
|
"@molecule/app-client-search": "1.0.3",
|
|
44
45
|
"@molecule/app-client-search-minisearch": "1.0.4",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"@molecule/app-drag-drop-dndkit": "1.0.3",
|
|
51
52
|
"@molecule/app-fonts": "1.2.2",
|
|
52
53
|
"@molecule/app-fonts-arimo": "1.0.5",
|
|
53
|
-
"@molecule/app-http": "1.
|
|
54
|
+
"@molecule/app-http": "1.5.0",
|
|
54
55
|
"@molecule/app-icons": "1.0.3",
|
|
55
56
|
"@molecule/app-icons-molecule": "1.0.3",
|
|
56
57
|
"@molecule/app-keyboard-shortcuts": "1.0.3",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"@molecule/app-realtime": "1.0.4",
|
|
60
61
|
"@molecule/app-realtime-socketio": "1.1.1",
|
|
61
62
|
"@molecule/app-routing": "1.0.4",
|
|
62
|
-
"@molecule/app-routing-react-router": "1.1.
|
|
63
|
+
"@molecule/app-routing-react-router": "1.1.2",
|
|
63
64
|
"@molecule/app-state": "1.0.4",
|
|
64
65
|
"@molecule/app-storage": "1.0.4",
|
|
65
66
|
"@molecule/app-storage-localstorage": "1.0.3",
|
|
@@ -80,7 +81,7 @@
|
|
|
80
81
|
"vitest": "4.1.12"
|
|
81
82
|
},
|
|
82
83
|
"peerDependencies": {
|
|
83
|
-
"@molecule/app-auth": "^
|
|
84
|
+
"@molecule/app-auth": "^3.0.0",
|
|
84
85
|
"@molecule/app-charts": "^1.0.1",
|
|
85
86
|
"@molecule/app-charts-chartjs": "^1.0.1",
|
|
86
87
|
"@molecule/app-client-search": "^1.0.0",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
"@molecule/app-drag-drop-dndkit": "^1.0.1",
|
|
94
95
|
"@molecule/app-fonts": "^1.0.1",
|
|
95
96
|
"@molecule/app-fonts-arimo": "^1.0.1",
|
|
96
|
-
"@molecule/app-http": "^1.0
|
|
97
|
+
"@molecule/app-http": "^1.5.0",
|
|
97
98
|
"@molecule/app-icons": "^1.0.1",
|
|
98
99
|
"@molecule/app-icons-molecule": "^1.0.1",
|
|
99
100
|
"@molecule/app-keyboard-shortcuts": "^1.0.1",
|