@hraness/suite-accounts 0.9.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +496 -0
  3. package/compiler-foundation.css +1 -0
  4. package/dist/auth-client.js +16 -0
  5. package/dist/auth-proxy.js +757 -0
  6. package/dist/bearer-verifier.js +1119 -0
  7. package/dist/browser-session.js +208 -0
  8. package/dist/client-configuration.js +470 -0
  9. package/dist/convex-browser-auth-browser.js +923 -0
  10. package/dist/convex-browser-auth-server.js +846 -0
  11. package/dist/convex-browser-auth.js +597 -0
  12. package/dist/entitlements.js +502 -0
  13. package/dist/identity/functions.js +44 -0
  14. package/dist/identity/index.js +1716 -0
  15. package/dist/identity/return-targets.js +33 -0
  16. package/dist/index.js +1133 -0
  17. package/dist/oidc-device-code.js +253 -0
  18. package/dist/oidc-rp.js +2259 -0
  19. package/dist/oidc-session-policy.js +5 -0
  20. package/dist/oidc-surface-server.js +2332 -0
  21. package/dist/profile-form.js +1934 -0
  22. package/dist/profile.js +17 -0
  23. package/dist/public-config.js +462 -0
  24. package/dist/react.js +137 -0
  25. package/dist/receipt-verifier.js +619 -0
  26. package/dist/registry.js +327 -0
  27. package/dist/stylex-manifest.json +1 -0
  28. package/dist/stylex.css +435 -0
  29. package/dist/urls.js +391 -0
  30. package/package.json +263 -0
  31. package/portfolio-inventory.json +37 -0
  32. package/src/auth-client.ts +37 -0
  33. package/src/auth-proxy.ts +473 -0
  34. package/src/bearer-verifier.ts +478 -0
  35. package/src/browser-session.ts +260 -0
  36. package/src/client-configuration.ts +172 -0
  37. package/src/convex-browser-auth-browser.ts +238 -0
  38. package/src/convex-browser-auth-server.ts +492 -0
  39. package/src/convex-browser-auth.ts +208 -0
  40. package/src/convex-url.ts +74 -0
  41. package/src/entitlements.ts +317 -0
  42. package/src/identity/catalog.ts +227 -0
  43. package/src/identity/consumers.ts +53 -0
  44. package/src/identity/functions.ts +70 -0
  45. package/src/identity/identifiers.ts +82 -0
  46. package/src/identity/index.ts +10 -0
  47. package/src/identity/links.ts +280 -0
  48. package/src/identity/membership.ts +156 -0
  49. package/src/identity/principals.ts +301 -0
  50. package/src/identity/profiles-v2.ts +330 -0
  51. package/src/identity/profiles.ts +524 -0
  52. package/src/identity/return-targets.ts +20 -0
  53. package/src/identity/usernames.ts +101 -0
  54. package/src/identity/views.ts +289 -0
  55. package/src/immutable.ts +25 -0
  56. package/src/index.ts +7 -0
  57. package/src/oidc-continuation.ts +59 -0
  58. package/src/oidc-device-code.ts +401 -0
  59. package/src/oidc-rp.ts +2217 -0
  60. package/src/oidc-session-policy.ts +6 -0
  61. package/src/oidc-surface-server.ts +186 -0
  62. package/src/profile-form.css +1 -0
  63. package/src/profile-form.stylex.ts +226 -0
  64. package/src/profile-form.tsx +314 -0
  65. package/src/profile.ts +33 -0
  66. package/src/public-config.ts +294 -0
  67. package/src/public-profile-form-result.ts +82 -0
  68. package/src/public-profile-form.tsx +319 -0
  69. package/src/public-profile-links.ts +100 -0
  70. package/src/react.tsx +196 -0
  71. package/src/receipt-verifier.ts +273 -0
  72. package/src/registry.ts +600 -0
  73. package/src/urls.ts +234 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hraness contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,496 @@
1
+ # @hraness/suite-accounts
2
+
3
+ Add Hraness Accounts sign-in to a Hraness product. `@hraness/suite-accounts`
4
+ supplies the OAuth and OIDC settings Accounts registered for your product,
5
+ keeps OAuth tokens in encrypted HttpOnly cookies and server-to-server requests
6
+ so browser code never sees them, and validates the identity and entitlement
7
+ data Accounts returns before your code relies on it.
8
+
9
+ You start by passing the origin, callback, client ID, and authentication mode
10
+ Accounts assigned to your product, and you get back a frozen configuration. A
11
+ misspelled field, a retired client, an unregistered origin, or an attempt to
12
+ supply your own trust settings fails before the product starts an Accounts
13
+ flow.
14
+
15
+ Accounts alone manages account records, OAuth client registration, identity
16
+ links, and entitlements. Installing this package does not let a product
17
+ register itself or pick its own issuer, JWKS endpoint, resource, callback,
18
+ client ID, wire version, or trust algorithm.
19
+
20
+ ## Install
21
+
22
+ Pin the immutable release:
23
+
24
+ ```json
25
+ {
26
+ "dependencies": {
27
+ "@hraness/suite-accounts": "github:hraness/suite-accounts#v0.9.20"
28
+ }
29
+ }
30
+ ```
31
+
32
+ Then install with Bun 1.3.14:
33
+
34
+ ```sh
35
+ bun install
36
+ ```
37
+
38
+ React and React DOM 18.3.1 through 19.x are optional peers. Install them only
39
+ when using `@hraness/suite-accounts/react` or
40
+ `@hraness/suite-accounts/profile-form`.
41
+
42
+ Import `@hraness/suite-accounts/profile-form.css` once when rendering the profile
43
+ form. It loads the precompiled StyleX stylesheet; consumers do not need a Babel
44
+ plugin or a StyleX runtime compiler. The semantic `suite-profile-*` classes stay
45
+ available for inspection, and caller classes remain on the form.
46
+
47
+ The seven public variables are `--suite-profile-input-background`,
48
+ `--suite-profile-line`, `--suite-profile-focus`, `--suite-profile-muted`,
49
+ `--suite-profile-button-background`, `--suite-profile-button-foreground`, and
50
+ `--suite-profile-error`. Native readonly, disabled, and focus-visible behavior
51
+ is preserved. Width and padding keep their physical-axis behavior in vertical
52
+ writing modes.
53
+
54
+ Applications that already join StyleX package rules can consume
55
+ `@hraness/suite-accounts/stylex-manifest.json` and
56
+ `@hraness/suite-accounts/compiler-foundation.css` with the public
57
+ `@hraness/ui/stylex-build` compiler. In that mode, include the generated rule
58
+ union instead of the standalone `profile-form.css` or `stylex.css` export.
59
+ The profile form is the only styled runtime entry. Root authentication and
60
+ protocol imports do not import React or StyleX presentation.
61
+
62
+ ## Bind a registered client
63
+
64
+ Pass the exact public fields assigned to the product. The factory rejects
65
+ unknown fields, including attempts to supply authority-controlled trust data.
66
+
67
+ ```ts
68
+ import { createSuiteAccountsClientConfiguration } from
69
+ "@hraness/suite-accounts/client-configuration";
70
+
71
+ const configuration = createSuiteAccountsClientConfiguration({
72
+ authMode: "oidc-rp",
73
+ callbackUrl: "https://sound.fish/api/suite-auth/callback",
74
+ clientId: "hraness:soundfish:production:v1",
75
+ consumer: "soundfish",
76
+ environment: "production",
77
+ origin: "https://sound.fish",
78
+ });
79
+
80
+ if (!configuration.ok) {
81
+ throw new Error(`Invalid Accounts binding: ${configuration.error}`);
82
+ }
83
+
84
+ configuration.value.provider.issuer;
85
+ // "https://account.hraness.com"
86
+ ```
87
+
88
+ For the registered Soundfish production client, the checked result begins with:
89
+
90
+ ```json
91
+ {
92
+ "authBasePath": "/api/suite-auth",
93
+ "configurationVersion": "suite-accounts-client-configuration-v1",
94
+ "provider": {
95
+ "issuer": "https://account.hraness.com",
96
+ "resource": "https://hraness.com/suite"
97
+ },
98
+ "wireVersion": "v1"
99
+ }
100
+ ```
101
+
102
+ The example shows selected fields from the returned configuration. The package
103
+ also supplies the authorization, token, revocation, userinfo, JWKS, identity
104
+ link, and entitlement receipt endpoints from closed current-authority data.
105
+
106
+ The returned configuration is frozen. Its provider endpoints, resource,
107
+ configuration version, and wire version are derived from the package's checked
108
+ current authority data. Accounts enforces the same registration on its side,
109
+ so passing this check grants nothing Accounts has not registered. A valid
110
+ configuration does not show that live sign-in works. Setting up the client in
111
+ the Accounts service and turning sign-in on in the product are separate steps.
112
+
113
+ Local development still uses `parseSuiteAccountsPublicConfig`. The consumer
114
+ origin and both Accounts Convex origins must use one exact loopback hostname.
115
+ Remote configuration accepts only the checked production deployment.
116
+ Vercel Preview deployments can expose their generated hostname through
117
+ `NEXT_PUBLIC_VERCEL_SURFACE_ORIGIN`, but sign-in does not work on Preview
118
+ deployments.
119
+
120
+ ## Follow the trust path
121
+
122
+ ```text
123
+ browser
124
+ -> product-owned /api/suite-auth route and encrypted HttpOnly session
125
+ -> registered Accounts authorization, token, userinfo, and JWKS endpoints
126
+ -> product server verifies audience, client binding, receipt, and entitlement
127
+ -> browser receives bounded session JSON, never an OAuth bearer
128
+ ```
129
+
130
+ Three parties share the work:
131
+
132
+ | Authority | Owns |
133
+ | --- | --- |
134
+ | Accounts service | Account records, client registration, identity links, entitlements, and authoritative provider operations |
135
+ | `@hraness/suite-accounts` | Closed registrations, trust configuration, protocol parsers, server transports, receipt verification, and optional React adapters |
136
+ | Product | Its registered origin and callback, cookie encryption keys, local subject, authorization decision, interface copy, and provider deployment |
137
+
138
+ ## Use the identity protocol
139
+
140
+ Import provider-neutral identity parsers and signed-message builders from the
141
+ `identity` subpath:
142
+
143
+ ```ts
144
+ import {
145
+ parseSuiteAccountId,
146
+ parseSuiteJwtClaims,
147
+ parseSuiteUsername,
148
+ suiteEntitlementReceiptMessage,
149
+ } from "@hraness/suite-accounts/identity";
150
+ ```
151
+
152
+ The identity catalog contains only the finite revisions, plan IDs, feature IDs,
153
+ and plan-to-feature relationship required to verify signed evidence. Prices,
154
+ provider lookup keys, product copy, and provider event replay policy are not
155
+ part of this package.
156
+
157
+ `parseSuiteJwtClaims` performs structural parsing. It does not establish
158
+ signature, issuer, audience, or time trust. Use the registry-pinned verifier
159
+ for authorization.
160
+
161
+ ## Interface map
162
+
163
+ | Subpath | Intended runtime |
164
+ | --- | --- |
165
+ | `.` | Dependency-light configuration, registry, URL, and Convex browser-token contracts |
166
+ | `./auth-client` | Legacy product-neutral browser auth adapter |
167
+ | `./identity` | Provider-neutral identity types, parsers, views, and signed messages |
168
+ | `./identity/functions` | Opt-in Convex function references and billing transport types |
169
+ | `./identity/return-targets` | Opt-in Accounts-owned billing return-target identifiers |
170
+ | `./client-configuration` | Additive validated product-binding factory |
171
+ | `./browser-session` | Same-origin browser session reads and serialized refresh |
172
+ | `./oidc-rp` | Server-only OAuth 2.1 relying-party implementation |
173
+ | `./oidc-device-code` | OAuth 2.0 Device Authorization Grant client protocol |
174
+ | `./oidc-session-policy` | Shared encrypted-cookie and session-lifetime policy |
175
+ | `./oidc-surface-server` | Registered browser-RP server handlers |
176
+ | `./bearer-verifier` | Server-only registry-pinned ES256 bearer verification |
177
+ | `./receipt-verifier` | Server-only HMAC receipt and product-keyring verification |
178
+ | `./convex-browser-auth` | Product Convex token configuration and identity parsing |
179
+ | `./convex-browser-auth-browser` | Memory-only browser bearer loader |
180
+ | `./convex-browser-auth-server` | Server-only short-token signing and JWKS handlers |
181
+ | `./auth-proxy` | Same-origin compatibility proxy with fixed headers and cookies |
182
+ | `./entitlements` | Post-signature entitlement checks and receipt ordering |
183
+ | `./profile` | Provider-neutral profile contracts |
184
+ | `./profile-form` | Optional controlled React profile editor |
185
+ | `./profile-form.css` | Product-neutral profile-form styles |
186
+ | `./stylex.css` | Precompiled standalone profile rules |
187
+ | `./stylex-manifest.json` | Canonical rule manifest for an application-wide compiler join |
188
+ | `./compiler-foundation.css` | Empty structural foundation for compiler adopters |
189
+ | `./public-config` | Validated public development and production configuration |
190
+ | `./react` | Optional route-local Accounts Convex context |
191
+ | `./registry` | Deprecated v1 compatibility registry and distinct current authority |
192
+ | `./urls` | Closed Accounts and product URL helpers |
193
+
194
+ Import server-only modules only from server code.
195
+
196
+ ## Public profile v2 contracts
197
+
198
+ The `./profile` and `./identity` entries export additive v2 parsers. Existing
199
+ six-link profile contracts and `SuiteProfileForm` keep their v1 behavior.
200
+
201
+ | Parser | Exact fields |
202
+ | --- | --- |
203
+ | `parseSuitePublicProfileV2` | `schemaVersion`, `accountId`, `username`, `name`, `bio`, `links`, `avatarRef`, `revision` |
204
+ | `parseSuiteProfileEditorV2` | Public fields plus `publication`; `username` can be `null` |
205
+ | `parseSuiteProfileUpdateV2` | `schemaVersion`, `expectedRevision`, `name`, `bio`, `links`, `avatarRef`, `publication` |
206
+
207
+ Every v2 object has `schemaVersion: 2`. Public and editor projections require
208
+ canonical saved values. Edits normalize names, biographies, and the existing
209
+ six social links, and add a seventh `github` link. All seven link keys are
210
+ required; absent values are `null`. Results and nested links are owned and
211
+ frozen. Unknown fields, symbols, accessors, and malformed values return fixed
212
+ field/reason errors without echoing the input.
213
+
214
+ Public names must be explicitly saved and nonempty. These parsers do not infer
215
+ names from authentication data. Names allow 120 UTF-16 code units and biographies
216
+ allow 1,000 after normalization; each raw text input is limited to 32,768 code
217
+ units. URLs are limited to 2,048 code units before and after normalization.
218
+ HTTP adapters must separately bound the full request body.
219
+
220
+ Public revisions start at one. Editor revision zero is a private blank default:
221
+ empty name and biography, null links and avatar, and either a claimed canonical
222
+ username or `null`. Saved editor profiles require a nonempty name; published
223
+ ones also require a username. Revisions are nonnegative safe integers. The
224
+ authority must enforce optimistic revision matching and refuse overflow.
225
+ Editor publication is `private` or `published`; edits request `private` or
226
+ `publish`. Parsing a public projection does not authenticate its source or
227
+ grant permission to publish it.
228
+
229
+ `normalizeSuiteProfileLinkV2("github", value)` accepts an HTTPS URL on
230
+ `github.com` or `www.github.com` with one ASCII profile path of one to 100
231
+ letters, digits, underscores, or hyphens. It canonicalizes the host and path
232
+ to lowercase and removes an optional trailing slash. It rejects ports,
233
+ credentials, whitespace, escapes, queries, and fragments. This is a bounded
234
+ URL policy, not proof that a GitHub account exists or belongs to the user.
235
+
236
+ `parseSuiteAvatarRef` accepts `avref_` followed by 64 lowercase hexadecimal
237
+ characters with a nonzero suffix. `suiteProfileAvatarPublicUrl` and
238
+ `suiteProfileAvatarEditorUrl` derive distinct `.webp` paths from that reference
239
+ at the fixed Accounts origin. These helpers perform no requests and establish
240
+ neither ownership nor endpoint availability. The serving service must enforce
241
+ owner access on the editor route and current publication consent on the public
242
+ route, and serve only server-reencoded static WebP images. Withdrawing consent
243
+ can stop serving an image; it cannot erase copies already downloaded.
244
+
245
+ Profile and avatar contracts are separate from numeric usage data. Usage-upload
246
+ credentials do not authorize profile or avatar changes. Installing these
247
+ contracts does not activate profile publication, editing, or image hosting.
248
+
249
+ ### Optional public-profile form
250
+
251
+ The existing `./profile-form` entry also exports `SuitePublicProfileForm`,
252
+ `SuitePublicProfileFormProps`, and `SuitePublicProfileFormResult`. Import the
253
+ same stylesheet as the v1 form. No new React, authentication, or styling
254
+ dependency is required.
255
+
256
+ ```tsx
257
+ <SuitePublicProfileForm
258
+ initialProfile={editor}
259
+ onSave={saveThroughAuthenticatedAccountTransport}
260
+ onSaved={handleConfirmedProfile}
261
+ />
262
+ ```
263
+
264
+ `initialProfile` is a canonical `SuiteProfileEditorV2`. `onSave` receives only
265
+ the exact revision-bound `SuiteProfileUpdateV2` and returns an unknown result
266
+ for runtime validation. Successful and conflict results contain exactly
267
+ `{ status, profile }`. The form also recognizes the exact singleton statuses
268
+ `unauthorized`, `username_required`, and `invalid_avatar`. It rejects malformed,
269
+ cross-account, mismatched-content, or impossible-revision results without
270
+ echoing their contents. These checks do not authenticate the transport.
271
+
272
+ The form edits name, biography, and all seven social links. It preserves the
273
+ authority's avatar reference without rendering an upload control or fetching
274
+ an image. Public text never defaults to sign-in name or email. Missing username
275
+ disables publication but allows a private save. A saved profile needs a name.
276
+
277
+ Visibility is a native radio choice applied on save. Buttons distinguish
278
+ publishing, saving publicly, saving privately, and withdrawing publication.
279
+ A conflict preserves the rejected draft until the user chooses **Load latest
280
+ profile**. Loading replaces the draft and clears its visibility choice;
281
+ another save requires a fresh choice. The form never retries automatically.
282
+ An unconfirmed save preserves the draft and reports that uncertainty separately
283
+ from the last confirmed state. Consumer callback failure cannot change a
284
+ confirmed save into an unconfirmed one.
285
+
286
+ Treat the initial snapshot and save callbacks as mount-scoped. Changing an
287
+ account ID remounts the editor; same-account prop updates do not overwrite a
288
+ draft or retarget its transport. The consumer must unmount on logout or a
289
+ session change and explicitly remount when the user chooses to reload. Retired
290
+ instances discard late results and callbacks. React Activity reconnection
291
+ preserves the draft and marks an abandoned save unconfirmed without resending
292
+ it. Transport promises still belong to the caller; retiring the form does not
293
+ cancel an already-dispatched server operation.
294
+
295
+ Server-rendered controls stay disabled until hydration. Private fields have no
296
+ native submission names, and the form uses POST as a further URL-leak guard.
297
+ JavaScript is required to edit and save. Keep authentication, revision checks,
298
+ and consent policy on the server; these browser controls are an extra layer.
299
+
300
+ ## Trust boundary
301
+
302
+ The package preserves these checks across the public surface:
303
+
304
+ - OAuth authorization code uses S256 PKCE plus exact state and nonce checks.
305
+ - Issuer, audience, origin, callback, client ID, resource, and endpoints must
306
+ match the registered values.
307
+ - OAuth access and refresh tokens remain in encrypted HttpOnly cookies or
308
+ server-to-server requests. Browser session JSON never exposes bearer tokens.
309
+ - A verified callback ends the cross-site redirect chain before a nonce-locked
310
+ continuation starts the product navigation from its registered origin.
311
+ - Refresh-token rotation uses an origin-scoped exclusive lock and re-reads the
312
+ session after acquiring it.
313
+ - Bearer verification accepts only bounded public P-256 ES256 keys from the
314
+ pinned JWKS endpoint and enforces both client-binding claims.
315
+ - Product backends grant access only after receipt, replay, freshness,
316
+ entitlement, and exact feature checks succeed.
317
+ - Missing legacy entitlement claims grant no suite features. Malformed claims
318
+ invalidate the token.
319
+
320
+ Do not authorize from browser profile JSON, decoded JWT data, discovery
321
+ destinations, billing plan membership, or an unverified receipt.
322
+
323
+ ## Fresh authentication for server-owned actions
324
+
325
+ Use the explicit `startFreshAuthentication(request, input)` and
326
+ `completeFreshAuthentication(request)` methods from `./oidc-rp` when a product
327
+ action needs recently authenticated account evidence. Both methods are
328
+ server-only. The existing surface handler does not opt into this flow.
329
+
330
+ Pass `{ context, expiresAtMs }` as the start input, optionally with
331
+ `authenticationNotBeforeMs`. `context` is an opaque
332
+ 32–256 character value using only ASCII letters, digits, `_` and `-`.
333
+ Create it on the product server and bind it to a durable, short-lived action;
334
+ do not copy browser parameters, personal data or credentials into it.
335
+ `expiresAtMs` must be a safe integer after server time and no more than 10
336
+ minutes ahead. `authenticationNotBeforeMs`, when present, must be a
337
+ non-negative safe integer and maps to a positive OIDC `max_age`: the provider
338
+ may satisfy the login prompt with a live session authenticated at or after
339
+ that instant. Omit it to force a fresh interactive sign-in every time. The
340
+ request still uses the exact registered start URL, method
341
+ and same-origin rules. The context is encrypted in the transaction cookie and
342
+ is not added to the authorization URL, continuation HTML or browser session.
343
+
344
+ The fresh transaction uses a separate version. Ordinary `callback()` and
345
+ `handle()` reject it, including when rolling back to an older package.
346
+ `completeFreshAuthentication()` rejects ordinary login transactions. Route the
347
+ callback explicitly on the product server; do not retry it through ordinary
348
+ login after a rejection.
349
+
350
+ A successful completion returns `{ kind: "authenticated", authentication,
351
+ response }`. Its frozen `authentication` projection contains only `context`,
352
+ `suiteAccountId`, `authenticatedAtMs`, `startedAtMs` and `expiresAtMs`.
353
+ Authentication time comes from verified signed `auth_time`, not token issuance
354
+ or session refresh. It must fall between the start's whole-second boundary and
355
+ current server time, with no positive skew allowance. Same-second ordering
356
+ cannot be inferred from a seconds-precision claim. Validity ends at the earliest
357
+ transaction, ID-token or access-token expiry and is checked again after provider
358
+ work. Missing or invalid evidence returns `{ kind: "rejected", response }` with
359
+ a fixed error and cleared transaction cookie.
360
+
361
+ Consume this result on the server. Never serialize the result or its context
362
+ into browser JSON or an application URL. Recheck expiry and atomically bind the
363
+ context, current account and intended action in product-owned durable state
364
+ before returning `response`. The response carries the normal encrypted session
365
+ cookie and same-origin continuation. A browser session, this evidence, or a
366
+ recent OTP in another flow does not itself approve a device or authorize an
367
+ action. Keep explicit user approval and replay protection separate.
368
+
369
+ Completion has no exactly-once guarantee and invokes no product hook. A lost
370
+ response may leave a consumed OAuth code; reconcile durable product state and
371
+ start a new transaction instead of assuming the code can be replayed. Ordinary
372
+ login, session access and refresh cannot produce fresh completion evidence.
373
+
374
+ The SDK requests `prompt=login` with `max_age=0` by default, then independently
375
+ checks the signed result. An optional `authenticationNotBeforeMs` input maps to
376
+ a positive `max_age`, letting the provider satisfy the prompt with a recent
377
+ live session instead of another interactive sign-in; the same verified
378
+ `auth_time` evidence is still returned for the product's own freshness check.
379
+ Request parameters alone are insufficient evidence. See
380
+ [OIDC authentication-time validation](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation).
381
+ Before you enable privileged actions, verify how the live Accounts service
382
+ reports authentication time, and test your product's approval flow.
383
+
384
+ ## Frozen v1 protocol compatibility
385
+
386
+ `SUITE_CONSUMER_IDS` preserves released identity values for historical parsing.
387
+ `SUITE_ACCOUNTS_CONSUMERS`, `SUITE_ACCOUNTS_DEPLOYMENTS`, their policy arrays,
388
+ and their lookup helpers are deeply runtime-frozen and deprecated for new
389
+ consumers. Retired identities are absent from those registration and trust
390
+ surfaces even when a parser still accepts them at a bounded historical
391
+ boundary.
392
+
393
+ Current authority lives under the distinct
394
+ `SUITE_ACCOUNTS_CURRENT_CONSUMER_IDS` and
395
+ `SUITE_ACCOUNTS_CURRENT_CONSUMERS` exports and their current lookup helpers.
396
+ New registration remains an Accounts service change followed by a package
397
+ release and an exact factory binding. Retired client identifiers and routes are
398
+ rejected by current APIs.
399
+
400
+ The published v1 browser refresh-lock and session-notification channel strings
401
+ also remain unchanged in version 0.1. Existing tabs therefore coordinate
402
+ across a rolling package migration without a browser namespace cutover.
403
+
404
+ The compatibility registry is intentionally closed. It must not gain runtime
405
+ mutation, remote discovery, environment overrides, or caller-supplied trust
406
+ values.
407
+
408
+ ## Releases
409
+
410
+ Pin the immutable `v0.9.20` release for this package version. Each release is
411
+ an immutable Git tag, and [`CHANGELOG.md`](CHANGELOG.md) lists what each one
412
+ changed.
413
+
414
+ Deterministic tests exercise valid registrations and readable failures.
415
+ Property tests cover foreign-value parsers, ordering, and round trips. The
416
+ package smoke installs built entries into clean Bundler and NodeNext consumers
417
+ with React 18.3.1 and 19.2.3, then builds the client entries in a clean Next.js
418
+ 16.2 webpack consumer.
419
+
420
+ ## Service boundary
421
+
422
+ This repository does not contain billing prices or provider lookup keys,
423
+ authoritative registry writes, reconciliation jobs, email delivery, provider
424
+ credentials, webhook payloads, provider project identifiers, or product-owned
425
+ service policy. Those concerns belong to the Accounts service. The Convex
426
+ function references and billing transport types required by existing clients
427
+ are isolated behind explicit opt-in identity subpaths; the root and client
428
+ configuration entries do not load them.
429
+
430
+ ## Questions before integration
431
+
432
+ ### Can a product register itself with this package?
433
+
434
+ No. The product must already exist in current Accounts authority. The validated
435
+ factory can confirm that binding, but it cannot create or widen it.
436
+
437
+ ### Can browser code authorize from session JSON or a decoded JWT?
438
+
439
+ No. Browser JSON is a display and session-state surface. A product server must
440
+ verify signature, issuer, audience, client binding, time, receipt ordering, and
441
+ the exact required entitlement before it grants access.
442
+
443
+ ### Does this package own prices, billing, or provider reconciliation?
444
+
445
+ No. It contains finite identity and entitlement protocol values plus opt-in
446
+ transport types. Prices, provider keys, events, credentials, and product policy
447
+ stay with their authoritative services.
448
+
449
+ ### Where should a new integration start?
450
+
451
+ Start with `./client-configuration` and commit the exact assigned binding. Add
452
+ `./oidc-surface-server` for registered browser sign-in, then import only the
453
+ server verifiers and identity subpaths required by the product. Run the package
454
+ and product gates before the callback is enabled in production.
455
+
456
+ ## Development
457
+
458
+ ```sh
459
+ bun install --frozen-lockfile --ignore-scripts
460
+ bun run check
461
+ bun run test:browser
462
+ bun pm pack --dry-run --ignore-scripts
463
+ ```
464
+
465
+ `bun run check` validates the portable portfolio inventory, runs independent
466
+ ESLint and TypeScript configuration, executes deterministic and property
467
+ tests, builds the ESM entries, verifies canonical StyleX rules and deterministic
468
+ artifacts across isolated roots, scans every source and packed file, and installs the
469
+ package in clean Bundler and NodeNext consumers on React 18.3.1 and 19.2.3. It
470
+ also builds the packed React entries in a clean Next.js 16.2 webpack consumer,
471
+ which verifies that every client entry has one valid top-level directive.
472
+
473
+ `bun run test:browser` uses the built profile form and OIDC continuation under
474
+ strict Content Security Policies. It checks server-rendered light and dark
475
+ styles at compact and wide widths, native focus and readonly controls, vertical
476
+ writing, hydration, pending saves, conflict revisions, validation errors, and
477
+ save failures. It also verifies that a real cross-site callback ends before the
478
+ nonce-locked continuation starts a same-origin session request. Only the save
479
+ transport and callback provider are synthetic. The script uses an installed
480
+ browser without downloading one; set `CHROMIUM_EXECUTABLE_PATH` to select its
481
+ executable. It prints the retained temporary profile evidence directory and
482
+ closes its own browsers and loopback servers. Branch and release verification
483
+ both require this browser check.
484
+
485
+ The public-profile browser fixture uses the same built optional entry. It
486
+ checks pre-hydration closure, publication and withdrawal, conflicts, manual
487
+ retry, double submission, account replacement during commit, and Activity
488
+ hide/show recovery with synthetic save transport only.
489
+
490
+ Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull request. Report
491
+ suspected vulnerabilities privately as described in
492
+ [SECURITY.md](./SECURITY.md).
493
+
494
+ ## License
495
+
496
+ MIT
@@ -0,0 +1 @@
1
+ /* The native profile form requires no structural CSS outside its StyleX rules. */
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ // src/auth-client.ts
3
+ import { convexClient } from "@convex-dev/better-auth/client/plugins";
4
+ import { emailOTPClient } from "better-auth/client/plugins";
5
+ import { createAuthClient } from "better-auth/react";
6
+ function createSuiteAccountsAuthClient(options = {}) {
7
+ return createAuthClient({
8
+ basePath: options.basePath ?? "/api/auth",
9
+ plugins: [convexClient(), emailOTPClient()]
10
+ });
11
+ }
12
+ var suiteAccountsAuthClient = createSuiteAccountsAuthClient();
13
+ export {
14
+ suiteAccountsAuthClient,
15
+ createSuiteAccountsAuthClient
16
+ };