@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.
- package/LICENSE +21 -0
- package/README.md +496 -0
- package/compiler-foundation.css +1 -0
- package/dist/auth-client.js +16 -0
- package/dist/auth-proxy.js +757 -0
- package/dist/bearer-verifier.js +1119 -0
- package/dist/browser-session.js +208 -0
- package/dist/client-configuration.js +470 -0
- package/dist/convex-browser-auth-browser.js +923 -0
- package/dist/convex-browser-auth-server.js +846 -0
- package/dist/convex-browser-auth.js +597 -0
- package/dist/entitlements.js +502 -0
- package/dist/identity/functions.js +44 -0
- package/dist/identity/index.js +1716 -0
- package/dist/identity/return-targets.js +33 -0
- package/dist/index.js +1133 -0
- package/dist/oidc-device-code.js +253 -0
- package/dist/oidc-rp.js +2259 -0
- package/dist/oidc-session-policy.js +5 -0
- package/dist/oidc-surface-server.js +2332 -0
- package/dist/profile-form.js +1934 -0
- package/dist/profile.js +17 -0
- package/dist/public-config.js +462 -0
- package/dist/react.js +137 -0
- package/dist/receipt-verifier.js +619 -0
- package/dist/registry.js +327 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +435 -0
- package/dist/urls.js +391 -0
- package/package.json +263 -0
- package/portfolio-inventory.json +37 -0
- package/src/auth-client.ts +37 -0
- package/src/auth-proxy.ts +473 -0
- package/src/bearer-verifier.ts +478 -0
- package/src/browser-session.ts +260 -0
- package/src/client-configuration.ts +172 -0
- package/src/convex-browser-auth-browser.ts +238 -0
- package/src/convex-browser-auth-server.ts +492 -0
- package/src/convex-browser-auth.ts +208 -0
- package/src/convex-url.ts +74 -0
- package/src/entitlements.ts +317 -0
- package/src/identity/catalog.ts +227 -0
- package/src/identity/consumers.ts +53 -0
- package/src/identity/functions.ts +70 -0
- package/src/identity/identifiers.ts +82 -0
- package/src/identity/index.ts +10 -0
- package/src/identity/links.ts +280 -0
- package/src/identity/membership.ts +156 -0
- package/src/identity/principals.ts +301 -0
- package/src/identity/profiles-v2.ts +330 -0
- package/src/identity/profiles.ts +524 -0
- package/src/identity/return-targets.ts +20 -0
- package/src/identity/usernames.ts +101 -0
- package/src/identity/views.ts +289 -0
- package/src/immutable.ts +25 -0
- package/src/index.ts +7 -0
- package/src/oidc-continuation.ts +59 -0
- package/src/oidc-device-code.ts +401 -0
- package/src/oidc-rp.ts +2217 -0
- package/src/oidc-session-policy.ts +6 -0
- package/src/oidc-surface-server.ts +186 -0
- package/src/profile-form.css +1 -0
- package/src/profile-form.stylex.ts +226 -0
- package/src/profile-form.tsx +314 -0
- package/src/profile.ts +33 -0
- package/src/public-config.ts +294 -0
- package/src/public-profile-form-result.ts +82 -0
- package/src/public-profile-form.tsx +319 -0
- package/src/public-profile-links.ts +100 -0
- package/src/react.tsx +196 -0
- package/src/receipt-verifier.ts +273 -0
- package/src/registry.ts +600 -0
- 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
|
+
};
|