@cboxdk/id-js 0.15.1 → 0.17.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 +237 -2
- package/dist/chunk-ASOQN47M.js +81 -0
- package/dist/chunk-ASOQN47M.js.map +1 -0
- package/dist/{chunk-3V6OKRWF.js → chunk-G3CD7X4U.js} +321 -90
- package/dist/chunk-G3CD7X4U.js.map +1 -0
- package/dist/chunk-WYTGLCMF.js +154 -0
- package/dist/chunk-WYTGLCMF.js.map +1 -0
- package/dist/{client-CecDIu_N.d.cts → client-CEXddiCR.d.cts} +81 -136
- package/dist/{client-CecDIu_N.d.ts → client-CdwdigzG.d.ts} +81 -136
- package/dist/index.cjs +384 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -4
- package/dist/index.d.ts +39 -4
- package/dist/index.js +63 -25
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +517 -42
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +29 -8
- package/dist/nextjs.d.ts +29 -8
- package/dist/nextjs.js +41 -12
- package/dist/nextjs.js.map +1 -1
- package/dist/server.cjs +220 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +105 -0
- package/dist/server.d.ts +105 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -0
- package/dist/types-BYZldock.d.cts +316 -0
- package/dist/types-BYZldock.d.ts +316 -0
- package/package.json +6 -1
- package/dist/chunk-3V6OKRWF.js.map +0 -1
package/README.md
CHANGED
|
@@ -90,6 +90,167 @@ const user = await client.authenticate({
|
|
|
90
90
|
});
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
## Organizations
|
|
94
|
+
|
|
95
|
+
A sign-in can be bound to one organization. The tokens then carry `org`, `org_name`, the
|
|
96
|
+
person's membership tier in it (`org_role`), and the app `roles` / `permissions` they hold
|
|
97
|
+
**there** — so switching organization means a new authorization, not a flag on the old
|
|
98
|
+
session.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// Bind to an organization you already know (the person must be an active member):
|
|
102
|
+
await client.createAuthorizationRequest({ organization: 'org_2x…' });
|
|
103
|
+
|
|
104
|
+
// Always show the hosted organization picker, with your guess preselected:
|
|
105
|
+
await client.createAuthorizationRequest({
|
|
106
|
+
prompt: 'select_organization',
|
|
107
|
+
organizationHint: 'org_2x…',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Hosted "create a team" step; the person becomes its owner and the sign-in
|
|
111
|
+
// continues bound to the new organization:
|
|
112
|
+
await client.createAuthorizationRequest({ prompt: 'create_organization' });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Option | Sent as | Meaning |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `organization` | `organization` | Bind the sign-in to this organization. |
|
|
118
|
+
| `organizationHint` | `organization_hint` | Preselect it in the picker; the person may choose another. |
|
|
119
|
+
| `prompt: 'select_organization'` | `prompt=select_organization` | Always show the picker. |
|
|
120
|
+
| `prompt: 'create_organization'` | `prompt=create_organization` | Create an organization first. |
|
|
121
|
+
|
|
122
|
+
`organization` cannot be combined with either organization prompt — it has already made
|
|
123
|
+
the choice they ask the person to make — and the SDK refuses the combination rather than
|
|
124
|
+
sending it. Use `organizationHint` with the picker instead.
|
|
125
|
+
|
|
126
|
+
### Switching
|
|
127
|
+
|
|
128
|
+
`switchOrganization(id)` is `createAuthorizationRequest({ organization: id })` under a name
|
|
129
|
+
that says what it is for. Persist and redirect exactly as for a sign-in; Cbox ID already
|
|
130
|
+
has the person's session, so they normally come straight back without seeing a form.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// app/auth/switch-organization/route.ts (Next.js)
|
|
134
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
135
|
+
import { cboxId } from '@/lib/cbox';
|
|
136
|
+
|
|
137
|
+
export async function GET(request: NextRequest) {
|
|
138
|
+
const org = request.nextUrl.searchParams.get('org');
|
|
139
|
+
if (!org) return NextResponse.redirect(new URL('/', request.url));
|
|
140
|
+
return cboxId.switchOrganization(org);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**The binding is checked, not trusted.** The request echoes `organization`; persist it with
|
|
145
|
+
`state`, `codeVerifier` and `nonce` and pass it back as `stored.organization`, and
|
|
146
|
+
`authenticate()` refuses tokens for any other organization. (The Next.js adapter does this
|
|
147
|
+
in a cookie for you.) An instance that predates organization selection ignores the
|
|
148
|
+
parameter and answers for whichever organization the session already had — without the
|
|
149
|
+
check, your app would show the new organization's name over the old one's data.
|
|
150
|
+
|
|
151
|
+
Replace your session with the user the callback returns rather than patching the old one:
|
|
152
|
+
`org_role`, `roles` and `permissions` can all differ between organizations. A person who
|
|
153
|
+
is not (or no longer) an active member comes back with `error=access_denied`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { AuthenticationError } from '@cboxdk/id-js';
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
user = await cboxId.callback(request);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
if (e instanceof AuthenticationError && e.error === 'access_denied') {
|
|
162
|
+
// Not a member of that organization — send them back to the one they were in.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Listing a person's organizations
|
|
168
|
+
|
|
169
|
+
Request the `organizations` scope and `user.organizations` lists every organization the
|
|
170
|
+
person is an active member of — `{ id, name, role }` — for an organization switcher. It is
|
|
171
|
+
a separate scope because it discloses memberships across unrelated customers; a plain
|
|
172
|
+
`profile` sign-in does not get it.
|
|
173
|
+
|
|
174
|
+
### Reading the claims
|
|
175
|
+
|
|
176
|
+
The signed-in user carries them typed:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
user.organization; // { id, name, role } | null — role is 'owner' | 'admin' | 'developer' | 'member' | 'viewer' | null
|
|
180
|
+
user.roles; // string[]
|
|
181
|
+
user.permissions; // string[]
|
|
182
|
+
user.actor; // { sub, actor } | null — see support sessions below
|
|
183
|
+
user.sessionId; // the id_token's `sid` | null — keep it to match a back-channel logout
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The same helpers work on the user and on a claim set you verified yourself, such as an
|
|
187
|
+
access token's payload on a resource server:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { organization, hasPermission, hasRole, isSupportSession } from '@cboxdk/id-js';
|
|
191
|
+
|
|
192
|
+
if (!hasPermission(payload, 'invoices:create')) return forbidden();
|
|
193
|
+
if (organization(user)?.role === 'owner') showBilling();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Matching is exact — `invoices:*` does not grant `invoices:delete`. An `org_role` this SDK
|
|
197
|
+
version does not recognise reads as `null`, never as a tier it would have to guess.
|
|
198
|
+
|
|
199
|
+
### Support sessions
|
|
200
|
+
|
|
201
|
+
A staff member can act as one of your users for a limited time (at most an hour, no refresh
|
|
202
|
+
token, with a recorded reason). Those tokens carry the RFC 8693 `act` claim naming the
|
|
203
|
+
staff member, and `isSupportSession()` reports it:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
if (isSupportSession(user)) {
|
|
207
|
+
// Show a banner, and refuse what a helper should never do on someone's behalf:
|
|
208
|
+
// changing their password, their email, their payout details.
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
It is **fail-closed**: any `act` claim counts, including one whose shape the SDK cannot
|
|
213
|
+
read (`user.actor.sub` is then `null`). A claim it cannot parse is not evidence that nobody
|
|
214
|
+
else is at the keyboard.
|
|
215
|
+
|
|
216
|
+
## From a CLI (device flow)
|
|
217
|
+
|
|
218
|
+
A command-line tool has no browser to redirect, and neither does a CI job, a container or
|
|
219
|
+
a TV app. The device authorization grant (RFC 8628) is for all of them: your program
|
|
220
|
+
prints a short code, the person approves it on whatever device is already in their hand,
|
|
221
|
+
and your program collects the tokens.
|
|
222
|
+
|
|
223
|
+
Register the app as **"CLI or device"** in the console. It is issued **no secret** — a
|
|
224
|
+
binary on somebody's laptop cannot keep one — and has **no redirect URI**, so neither
|
|
225
|
+
appears here:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { CboxIdClient } from '@cboxdk/id-js';
|
|
229
|
+
|
|
230
|
+
const cbox = new CboxIdClient({
|
|
231
|
+
issuer: process.env.CBOX_ID_ISSUER!,
|
|
232
|
+
clientId: process.env.CBOX_ID_CLIENT_ID!,
|
|
233
|
+
scopes: ['openid', 'profile', 'email', 'offline_access'],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const auth = await cbox.requestDeviceAuthorization();
|
|
237
|
+
console.log(`Open ${auth.verificationUri} and enter ${auth.userCode}`);
|
|
238
|
+
|
|
239
|
+
// Blocks until they approve. Honours the server's interval, backs off on `slow_down`,
|
|
240
|
+
// and stops on a decline or an expired code. Pass an AbortSignal so Ctrl-C works.
|
|
241
|
+
const user = await cbox.pollDeviceToken(auth);
|
|
242
|
+
console.log(`Signed in as ${user.email}`);
|
|
243
|
+
// Persist user.refreshToken (mode 0600) so the next run does not ask again.
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The scopes are bounded by what the app is **registered** for: a device request naming one
|
|
247
|
+
outside that ceiling is refused with `invalid_scope` rather than quietly reduced, because
|
|
248
|
+
no browser is in front of it to notice a smaller grant. Keep `offline_access` unless you
|
|
249
|
+
want the person re-approving every hour.
|
|
250
|
+
|
|
251
|
+
See [Sign in from a CLI](https://github.com/cboxdk/cbox-id/blob/main/docs/getting-started/sign-in-from-a-cli.md)
|
|
252
|
+
for the protocol itself and where to store the tokens.
|
|
253
|
+
|
|
93
254
|
## In the browser (publishable keys)
|
|
94
255
|
|
|
95
256
|
Everything above needs a server: it holds your client secret. A **publishable key** is the
|
|
@@ -239,6 +400,47 @@ everywhere" needs. `machineToken`, `introspect` and `revoke` authenticate as the
|
|
|
239
400
|
client, so they require a `clientSecret`; `userinfo` authenticates with the user's
|
|
240
401
|
own access token and does not.
|
|
241
402
|
|
|
403
|
+
## API keys for your API
|
|
404
|
+
|
|
405
|
+
Your customers can create API keys for **your** API on Cbox ID's hosted page, and your API
|
|
406
|
+
asks Cbox ID whether a key it was handed is good. Link people to the page:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
client.apiKeysUrl({ returnTo: 'https://app.acme.com/settings' });
|
|
410
|
+
// → {issuer}/account/api-keys?client_id=<your client>&return_to=…
|
|
411
|
+
// options: clientId (another of your apps), returnTo, organization (which of theirs)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Verify a key on your server — never in a browser, since it uses your client secret. It
|
|
415
|
+
lives in its own entry, `@cboxdk/id-js/server`, so a browser bundle never pulls it in:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { ApiKeyVerifier } from '@cboxdk/id-js/server';
|
|
419
|
+
import { hasPermission } from '@cboxdk/id-js';
|
|
420
|
+
|
|
421
|
+
const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });
|
|
422
|
+
|
|
423
|
+
const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');
|
|
424
|
+
if (!answer.active) return new Response(null, { status: 401 });
|
|
425
|
+
if (!hasPermission(answer, 'invoices:create')) return new Response(null, { status: 403 });
|
|
426
|
+
// answer: { active, key_id, sub, org, org_role, permissions, client_id, expires_at }
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
On Next.js the adapter does the same with its own configuration:
|
|
430
|
+
`await cboxId.verifyApiKey(key)` and `cboxId.apiKeysUrl()`.
|
|
431
|
+
|
|
432
|
+
- Every bad key — unknown, revoked, expired, another app's, a holder who left the
|
|
433
|
+
organization — is `{ active: false }`, with no reason. That is Cbox ID's design, so the
|
|
434
|
+
endpoint cannot be used to probe keys.
|
|
435
|
+
- `permissions` is already re-capped to what the holder holds for your app **now**; a
|
|
436
|
+
demotion takes effect on the next verification.
|
|
437
|
+
- An answer naming another app's `client_id` is refused with an `AuthenticationError`, as
|
|
438
|
+
is a failed call (wrong client credentials, instance unreachable). Treat a throw as "do
|
|
439
|
+
not let this request in".
|
|
440
|
+
- `cacheTtlMs` (off by default, at most 60 seconds) caches **active** answers per key, never
|
|
441
|
+
past the key's `expires_at`. While an answer is cached, a revoked key keeps working —
|
|
442
|
+
keep it short. Refusals are never cached.
|
|
443
|
+
|
|
242
444
|
## Migrating off an old login
|
|
243
445
|
|
|
244
446
|
Bulk-importing users with their existing hashes is the first answer, and the better one.
|
|
@@ -334,8 +536,8 @@ claims for you to enforce. Requires the app's client to hold the `apps.manifest`
|
|
|
334
536
|
```ts
|
|
335
537
|
import { defineAuthz, publishManifest } from '@cboxdk/id-js';
|
|
336
538
|
|
|
337
|
-
// Declare the catalog (validated: keys are `feature:action
|
|
338
|
-
// declared permissions). Keep this next to the code that enforces it.
|
|
539
|
+
// Declare the catalog (validated: keys are lowercase `feature:action` slugs, and roles
|
|
540
|
+
// must reference declared permissions). Keep this next to the code that enforces it.
|
|
339
541
|
export const authz = defineAuthz({
|
|
340
542
|
permissions: [
|
|
341
543
|
{ key: 'invoices:create', description: 'Create invoices' },
|
|
@@ -360,6 +562,39 @@ const summary = await publishManifest(
|
|
|
360
562
|
// → { unchanged, roles_declared, permissions_declared, ... }
|
|
361
563
|
```
|
|
362
564
|
|
|
565
|
+
### Staff roles and self-serve permissions
|
|
566
|
+
|
|
567
|
+
Two flags decide who may hand something out:
|
|
568
|
+
|
|
569
|
+
- **`tenantAssignable: false` on a role** makes it a **staff role** — for your own support
|
|
570
|
+
or operations people, held environment-wide across every customer. Cbox ID never lists
|
|
571
|
+
or accepts it on an organization's own admin pages; only an environment administrator
|
|
572
|
+
can grant it. Roles default to `true`.
|
|
573
|
+
- **`tenantAssignable: true` on a permission** lets an organization's administrators put it
|
|
574
|
+
in custom roles they build themselves. Permissions default to `false`: internal unless
|
|
575
|
+
you opt in.
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
export const authz = defineAuthz({
|
|
579
|
+
permissions: [
|
|
580
|
+
{ key: 'parcels:read', description: 'View parcels', tenantAssignable: true },
|
|
581
|
+
// Lets a staff member start a support session in this app (see "Support sessions").
|
|
582
|
+
{ key: 'support:impersonate', description: 'Act as a customer' },
|
|
583
|
+
],
|
|
584
|
+
roles: [
|
|
585
|
+
{ key: 'viewer', name: 'Viewer', permissions: ['parcels:read'] },
|
|
586
|
+
{ key: 'support', name: 'Support', description: 'Our support team',
|
|
587
|
+
permissions: ['support:impersonate', 'parcels:read'], tenantAssignable: false },
|
|
588
|
+
],
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
On the wire the flags are `tenant_assignable`, and only their non-default state is sent
|
|
593
|
+
(`false` on a role, `true` on a permission). Both are part of the manifest's `version`, so
|
|
594
|
+
marking a role staff-only in a new deploy re-syncs it rather than being skipped as
|
|
595
|
+
unchanged. A value that is not a real boolean — the string `"false"` from a YAML file, say
|
|
596
|
+
— is refused, because it would otherwise read as the opposite of what it says.
|
|
597
|
+
|
|
363
598
|
`publishManifest` mints a client-credentials token (`scope=apps.manifest`) and POSTs
|
|
364
599
|
the manifest to `{issuer}/api/v1/apps/manifest`. It is a server-side operation — keep
|
|
365
600
|
your `clientSecret` off the browser. The wire format matches the PHP SDK
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var CboxIdError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var ConfigurationError = class extends CboxIdError {
|
|
9
|
+
};
|
|
10
|
+
var InvalidStateError = class extends CboxIdError {
|
|
11
|
+
};
|
|
12
|
+
var AuthenticationError = class extends CboxIdError {
|
|
13
|
+
constructor(message, error, errorDescription, status, retryAfter) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.error = error;
|
|
16
|
+
this.errorDescription = errorDescription;
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.retryAfter = retryAfter;
|
|
19
|
+
}
|
|
20
|
+
error;
|
|
21
|
+
errorDescription;
|
|
22
|
+
status;
|
|
23
|
+
retryAfter;
|
|
24
|
+
/** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */
|
|
25
|
+
get isRateLimited() {
|
|
26
|
+
return this.status === 429;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
async function oauthError(response, fallback) {
|
|
30
|
+
const body = await response.text().catch(() => "");
|
|
31
|
+
let code;
|
|
32
|
+
let description;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(body);
|
|
35
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
36
|
+
const { error, error_description: errorDescription } = parsed;
|
|
37
|
+
code = typeof error === "string" ? error : void 0;
|
|
38
|
+
description = typeof errorDescription === "string" ? errorDescription : void 0;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
const detail = code ?? `HTTP ${response.status}`;
|
|
43
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
44
|
+
const retryAfter = retryAfterHeader !== null && /^\d+$/.test(retryAfterHeader.trim()) ? Number(retryAfterHeader.trim()) : void 0;
|
|
45
|
+
return new AuthenticationError(`${fallback}: ${detail}`, code, description, response.status, retryAfter);
|
|
46
|
+
}
|
|
47
|
+
var FrontendApiError = class extends CboxIdError {
|
|
48
|
+
constructor(message, code, status, retryAfter) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.status = status;
|
|
52
|
+
this.retryAfter = retryAfter;
|
|
53
|
+
}
|
|
54
|
+
code;
|
|
55
|
+
status;
|
|
56
|
+
retryAfter;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/issuer.ts
|
|
60
|
+
function assertSecureIssuer(issuer) {
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(issuer);
|
|
64
|
+
} catch {
|
|
65
|
+
throw new ConfigurationError("Cbox ID config `issuer` is not a valid URL.");
|
|
66
|
+
}
|
|
67
|
+
if (url.protocol === "https:") {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
71
|
+
if (url.protocol === "http:" && loopback) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
throw new ConfigurationError(
|
|
75
|
+
`Cbox ID config \`issuer\` must be https (got ${url.protocol}//${url.hostname}).`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { AuthenticationError, CboxIdError, ConfigurationError, FrontendApiError, InvalidStateError, assertSecureIssuer, oauthError };
|
|
80
|
+
//# sourceMappingURL=chunk-ASOQN47M.js.map
|
|
81
|
+
//# sourceMappingURL=chunk-ASOQN47M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/issuer.ts"],"names":[],"mappings":"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","file":"chunk-ASOQN47M.js","sourcesContent":["/**\n * Base class for every error this SDK throws, so callers can `catch (e) { if (e\n * instanceof CboxIdError) ... }` in one branch.\n */\nexport class CboxIdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\n/** The client is misconfigured (a required option is missing or empty). */\nexport class ConfigurationError extends CboxIdError {}\n\n/**\n * The login `state` did not match what was stored — the callback is forged or\n * stale. Treat as a fresh start, not an error to surface to the user.\n */\nexport class InvalidStateError extends CboxIdError {}\n\n/**\n * Login could not be completed: the provider returned an error, the callback was\n * missing a code, or a token / id_token failed verification.\n *\n * `error` carries the RFC 6749 §5.2 code the authorization server sent, when it sent one.\n * It used to be discarded at every back-channel boundary, leaving a single message string\n * for outcomes that need opposite responses: `invalid_grant` on a refresh means the\n * session is over and the person has to sign in again, while `temporarily_unavailable`\n * means try the same token in a moment. A caller reduced to matching on prose either\n * retries what can never succeed, or signs out somebody who did not need to be.\n */\nexport class AuthenticationError extends CboxIdError {\n constructor(\n message: string,\n /** RFC 6749 §5.2 error code, when the server sent a parseable one. */\n readonly error?: string,\n /** The server's `error_description`, verbatim. Not end-user copy. */\n readonly errorDescription?: string,\n /** HTTP status, for the cases where the body says nothing useful. */\n readonly status?: number,\n /**\n * Seconds to wait, off the `Retry-After` header — set only on a 429.\n *\n * The distinction it carries is the one a caller most needs and could least infer:\n * a 429 is the ONLY back-channel failure where the same request will succeed\n * unchanged if you simply wait. Every other one needs a different request, or a new\n * sign-in. Without this the SDK reported \"token refresh failed\" and a caller with a\n * retry loop hammered a limiter that was already telling it exactly how long to stop.\n */\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n\n /** Whether waiting `retryAfter` seconds and repeating the same request is worth it. */\n get isRateLimited(): boolean {\n return this.status === 429;\n }\n}\n\n/**\n * Read an OAuth error out of a failed token-endpoint response.\n *\n * Best-effort by design: a 502 from a proxy is HTML, a captive portal is worse, and the\n * caller still needs an error rather than a parse exception. What it must never do is\n * invent a code — an absent or unparseable `error` stays undefined, so\n * `e.error === 'invalid_grant'` is true only because the server said so.\n */\nexport async function oauthError(response: Response, fallback: string): Promise<AuthenticationError> {\n const body = await response.text().catch(() => '');\n\n let code: string | undefined;\n let description: string | undefined;\n\n try {\n const parsed: unknown = JSON.parse(body);\n\n if (typeof parsed === 'object' && parsed !== null) {\n const { error, error_description: errorDescription } = parsed as Record<string, unknown>;\n code = typeof error === 'string' ? error : undefined;\n description = typeof errorDescription === 'string' ? errorDescription : undefined;\n }\n } catch {\n // Not JSON. The status and the truncated body below are all there is to report.\n }\n\n // THE BODY IS NEVER ECHOED. These calls carry `client_secret`, `code_verifier` and\n // refresh tokens in their REQUEST, and a debug proxy, a captive portal or an upstream\n // error page that reflects what it received puts those straight into a thrown message\n // — which production apps log. Only the server's own OAuth error code travels, and\n // failing that, the status alone.\n const detail = code ?? `HTTP ${response.status}`;\n\n // `Retry-After` is seconds here — Laravel's throttler writes the integer form. The\n // HTTP-date form is legal too and is deliberately NOT parsed: guessing at a clock\n // skew is worse than saying nothing, and `status === 429` still tells the caller to\n // back off.\n const retryAfterHeader = response.headers.get('retry-after');\n const retryAfter = retryAfterHeader !== null && /^\\d+$/.test(retryAfterHeader.trim())\n ? Number(retryAfterHeader.trim())\n : undefined;\n\n return new AuthenticationError(`${fallback}: ${detail}`, code, description, response.status, retryAfter);\n}\n\n/**\n * The Frontend API refused, or could not be reached.\n *\n * Carries a `code` because the four ways this goes wrong need four different responses\n * from the caller, and telling them apart from a message string is the kind of thing\n * that works until somebody rewords the message:\n *\n * - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a\n * configuration problem the developer fixes once in the console.\n * - `rate_limited` — back off; `retryAfter` says for how long, when the server said.\n * - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.\n * - `malformed` — a 200 whose body is not the document we expect, which means something\n * between the page and us is rewriting responses (a proxy, an extension, a captive\n * portal). Worth distinguishing, because retrying it never helps.\n */\nexport class FrontendApiError extends CboxIdError {\n constructor(\n message: string,\n readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed',\n readonly status?: number,\n readonly retryAfter?: number,\n ) {\n super(message);\n }\n}\n","import { ConfigurationError } from './errors.js';\n\n/**\n * An issuer must be HTTPS.\n *\n * Everything this SDK does with it carries a credential: the authorization code, the PKCE\n * verifier, the client secret, the refresh token. Over `http://` a network attacker reads\n * all of them — and, worse, replaces the discovery document and JWKS, at which point a\n * forged id_token verifies cleanly and the whole verification chain proves nothing.\n *\n * Loopback is allowed because a native app's own callback listener is loopback by\n * definition (RFC 8252) and a dev instance runs there.\n */\nexport function assertSecureIssuer(issuer: string): void {\n let url: URL;\n\n try {\n url = new URL(issuer);\n } catch {\n throw new ConfigurationError('Cbox ID config `issuer` is not a valid URL.');\n }\n\n if (url.protocol === 'https:') {\n return;\n }\n\n const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]' || url.hostname === '::1';\n\n if (url.protocol === 'http:' && loopback) {\n return;\n }\n\n throw new ConfigurationError(\n `Cbox ID config \\`issuer\\` must be https (got ${url.protocol}//${url.hostname}).`,\n );\n}\n"]}
|