@cboxdk/id-js 0.7.0 → 0.9.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/dist/index.d.cts CHANGED
@@ -1,5 +1,242 @@
1
1
  export { A as AuthorizationRequest, C as CallbackParams, a as CboxIdClient, b as CboxIdConfig, c as CboxUser, D as DiscoveryDocument, R as RefreshedTokens, S as StoreSecretInput, d as StoredAuthState, T as TokenResponse, e as TokenTypeHint, V as VaultClient, f as VaultLease, g as VaultSecretRef, h as VerifyWebhookOptions, v as verifyWebhook } from './client-DkZSRmnO.cjs';
2
2
 
3
+ /**
4
+ * The handler on YOUR side of a legacy migration.
5
+ *
6
+ * Cbox ID offers it an email and a password it has never seen, and your code answers with
7
+ * the person or with nothing. On a yes they are created in Cbox ID as a result of the
8
+ * login that just succeeded, and from their next sign-in the old system is never asked
9
+ * again.
10
+ *
11
+ * WHY THIS IS A FACTORY AND NOT A DOCUMENT. The wire format is simple enough to describe
12
+ * in a paragraph — a signed POST, JSON in, JSON out — and describing it would mean every
13
+ * customer writes the signature check themselves. That check is exactly the part people
14
+ * get wrong: `===` instead of a constant-time compare, no freshness window, no thought
15
+ * about replay. So the factory owns all of it and you write the one function that knows
16
+ * your own database.
17
+ *
18
+ * It is deliberately framework-agnostic: it takes a `Request` and returns a `Response`,
19
+ * which is what Next.js route handlers, Remix, Hono, Bun and Deno all speak natively.
20
+ */
21
+ /** What your handler answers with. Returning `null` means "no", and is not an error. */
22
+ interface LegacyUser {
23
+ email: string;
24
+ name?: string | null;
25
+ emailVerified?: boolean;
26
+ /**
27
+ * Your stored hash, when you can expose it.
28
+ *
29
+ * Passing it lets the person keep their password in Cbox ID verbatim — it is stored as
30
+ * is and upgraded to the platform hasher on their next login, exactly like a
31
+ * bulk-imported user. Omit it and Cbox ID hashes the password they just proved they
32
+ * know, which is the honest option when your store cannot hand a hash over.
33
+ */
34
+ passwordHash?: string;
35
+ }
36
+ interface LegacyVerifierOptions {
37
+ /** The same secret you declared in your manifest, or configured in the console. */
38
+ secret: string;
39
+ /**
40
+ * Decide whether these credentials are good, and say who they belong to.
41
+ *
42
+ * Return `null` for no. THROWING IS NOT THE SAME as returning null: a throw means your
43
+ * store could not decide, and is answered with a 503 so Cbox ID refuses the sign-in
44
+ * rather than treating an outage as a wrong password.
45
+ */
46
+ verify: (email: string, password: string) => Promise<LegacyUser | null>;
47
+ /**
48
+ * How far out of date a request may be, in seconds. Defaults to 300.
49
+ *
50
+ * A window is what makes a captured request stop working. Too wide and a recording is
51
+ * useful for hours; too narrow and ordinary clock skew starts refusing real traffic.
52
+ */
53
+ toleranceSeconds?: number;
54
+ }
55
+ /**
56
+ * Build the request handler.
57
+ *
58
+ * ```ts
59
+ * // app/api/cbox-legacy/route.ts
60
+ * export const POST = createLegacyVerifier({
61
+ * secret: process.env.CBOX_LEGACY_SECRET!,
62
+ * async verify(email, password) {
63
+ * const row = await db.users.findByEmail(email)
64
+ * if (!row || !(await argon2.verify(row.password, password))) return null
65
+ * return { email: row.email, name: row.name, emailVerified: !!row.confirmedAt }
66
+ * },
67
+ * })
68
+ * ```
69
+ */
70
+ declare function createLegacyVerifier(options: LegacyVerifierOptions): (request: Request) => Promise<Response>;
71
+
72
+ /**
73
+ * The browser-safe half of the SDK.
74
+ *
75
+ * Everything else in this package assumes a server: it holds a client secret, it exchanges
76
+ * codes, it verifies webhooks. None of that may run in a page, which is why the UI
77
+ * components have always been redirect shells — a page that cannot authenticate itself to
78
+ * the identity provider cannot ask it anything, so it sends the person away and waits.
79
+ *
80
+ * A publishable key changes that. It is public on purpose: it ships in your bundle, it is
81
+ * visible in devtools, and it is safe there because it only works from the origins you
82
+ * registered. What it buys is that a page can read its OWN sign-in configuration — the
83
+ * endpoints, the social buttons, your theme — and render a form itself.
84
+ *
85
+ * This client deliberately has no secret, no token storage and no crypto. It reads two
86
+ * documents. Anything that mints or holds a credential still belongs on your server.
87
+ */
88
+ /** How a page identifies itself. Public — put it in your bundle. */
89
+ interface FrontendClientOptions {
90
+ /** Your identity provider's origin, e.g. `https://id.acme.com`. */
91
+ issuer: string;
92
+ /** `pk_live_…` or `pk_test_…`. Safe to publish. */
93
+ publishableKey: string;
94
+ /** Abort after this long. Defaults to 10s. */
95
+ timeoutMs?: number;
96
+ /**
97
+ * How many times to retry a transient failure. Defaults to 2 (so three attempts).
98
+ *
99
+ * Only the network and 5xx are retried. A refusal is a configuration problem and
100
+ * retrying it just delays the developer finding out; a 429 is honoured rather than
101
+ * hammered.
102
+ */
103
+ retries?: number;
104
+ /** Injectable for tests and for runtimes with a non-global fetch. */
105
+ fetch?: typeof globalThis.fetch;
106
+ }
107
+ /** A social button to draw. Name and provider only — never an internal id. */
108
+ interface SocialProvider {
109
+ provider: string;
110
+ name: string;
111
+ }
112
+ /** The customer's theme, when they have set one. Absent means "use your own defaults". */
113
+ interface Appearance {
114
+ preset: string;
115
+ radius: string;
116
+ font: string;
117
+ light: Record<string, string>;
118
+ dark: Record<string, string>;
119
+ }
120
+ /** Everything needed to draw a sign-in box, and nothing that identifies anybody. */
121
+ interface FrontendConfig {
122
+ mode: 'test' | 'live' | null;
123
+ issuer: string;
124
+ endpoints: {
125
+ authorization: string;
126
+ token: string;
127
+ userinfo: string;
128
+ end_session: string;
129
+ jwks: string;
130
+ };
131
+ social: SocialProvider[];
132
+ appearance?: Appearance;
133
+ }
134
+ /** What a sign-in attempt produced. */
135
+ type SignInResult = {
136
+ status: 'ok';
137
+ loginTicket: string;
138
+ expiresIn: number;
139
+ } | {
140
+ status: 'mfa_required';
141
+ } | {
142
+ status: 'otp_required';
143
+ } | {
144
+ status: 'sso_required';
145
+ } | {
146
+ status: 'invalid';
147
+ } | {
148
+ status: 'rate_limited';
149
+ retryAfter: number | undefined;
150
+ };
151
+ /** Who the browser is signed in as, or nobody. */
152
+ interface FrontendSession {
153
+ user: {
154
+ id: string;
155
+ email: string;
156
+ name: string | null;
157
+ } | null;
158
+ }
159
+ /**
160
+ * Reads the public configuration and the current session from a browser.
161
+ *
162
+ * ```ts
163
+ * const frontend = new CboxIdFrontend({
164
+ * issuer: 'https://id.acme.com',
165
+ * publishableKey: 'pk_live_…',
166
+ * })
167
+ *
168
+ * const config = await frontend.config() // cached for you; safe to call per component
169
+ * const { user } = await frontend.session(accessToken)
170
+ * ```
171
+ */
172
+ declare class CboxIdFrontend {
173
+ private readonly issuer;
174
+ private readonly publishableKey;
175
+ private readonly timeoutMs;
176
+ private readonly retries;
177
+ private readonly fetchImpl;
178
+ /**
179
+ * The config document, and the in-flight promise for it.
180
+ *
181
+ * Deduplicated rather than merely cached: a page with a sign-in form, a user button and
182
+ * an organization switcher mounts three components in the same tick, and three parallel
183
+ * fetches of the same immutable document is a waste the SDK should absorb rather than
184
+ * ask every consumer to.
185
+ */
186
+ private configPromise;
187
+ constructor(options: FrontendClientOptions);
188
+ /** Whether this key drives real sign-ins. Handy for a "test mode" badge in your own UI. */
189
+ get isLive(): boolean;
190
+ /**
191
+ * The public sign-in configuration.
192
+ *
193
+ * Fetched once per client instance. The document is small and changes when somebody
194
+ * edits it in the console, so a long-lived page picks changes up on its next full load
195
+ * rather than mid-session — which is the right trade for something that decides layout.
196
+ */
197
+ config(): Promise<FrontendConfig>;
198
+ /**
199
+ * Who is signed in, given an access token your app already holds.
200
+ *
201
+ * Returns `{ user: null }` rather than throwing when nobody is signed in. That is a
202
+ * state, not an error: a user button renders on pages nobody has signed in on, and
203
+ * making callers treat a rejection as a state is how flash-of-signed-out bugs happen.
204
+ *
205
+ * The publishable key grants nothing here — the token is the entire authority.
206
+ */
207
+ session(accessToken?: string): Promise<FrontendSession>;
208
+ /**
209
+ * Sign in with a password, from a form your page drew.
210
+ *
211
+ * WHAT COMES BACK IS A TICKET, NEVER A TOKEN. Handing tokens to a page that proved a
212
+ * password is the implicit grant, which OAuth 2.1 removes: tokens in a URL, in history,
213
+ * in `Referer`, with no client authentication and no PKCE binding. Spend the ticket by
214
+ * sending the browser to `/oauth/authorize` with your own PKCE challenge and
215
+ * `login_ticket` in the query — the ordinary flow, which then issues an ordinary code.
216
+ *
217
+ * It expires in sixty seconds. It is for one redirect, not for storing.
218
+ *
219
+ * ```ts
220
+ * const result = await frontend.signIn(email, password)
221
+ *
222
+ * if (result.status === 'ok') {
223
+ * window.location.href = `${config.endpoints.authorization}?${new URLSearchParams({
224
+ * client_id, redirect_uri, response_type: 'code', code_challenge, code_challenge_method: 'S256',
225
+ * login_ticket: result.loginTicket,
226
+ * })}`
227
+ * }
228
+ * ```
229
+ *
230
+ * EVERY REFUSAL IS THE SAME REFUSAL. `invalid` covers a wrong password, an unknown
231
+ * address and a locked account, because distinguishing them is the enumeration oracle
232
+ * the server refuses to build — do not present them differently in your UI either.
233
+ */
234
+ signIn(email: string, password: string): Promise<SignInResult>;
235
+ private post;
236
+ private get;
237
+ private attempt;
238
+ }
239
+
3
240
  /**
4
241
  * Base class for every error this SDK throws, so callers can `catch (e) { if (e
5
242
  * instanceof CboxIdError) ... }` in one branch.
@@ -22,6 +259,27 @@ declare class InvalidStateError extends CboxIdError {
22
259
  */
23
260
  declare class AuthenticationError extends CboxIdError {
24
261
  }
262
+ /**
263
+ * The Frontend API refused, or could not be reached.
264
+ *
265
+ * Carries a `code` because the four ways this goes wrong need four different responses
266
+ * from the caller, and telling them apart from a message string is the kind of thing
267
+ * that works until somebody rewords the message:
268
+ *
269
+ * - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a
270
+ * configuration problem the developer fixes once in the console.
271
+ * - `rate_limited` — back off; `retryAfter` says for how long, when the server said.
272
+ * - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.
273
+ * - `malformed` — a 200 whose body is not the document we expect, which means something
274
+ * between the page and us is rewriting responses (a proxy, an extension, a captive
275
+ * portal). Worth distinguishing, because retrying it never helps.
276
+ */
277
+ declare class FrontendApiError extends CboxIdError {
278
+ readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed';
279
+ readonly status?: number | undefined;
280
+ readonly retryAfter?: number | undefined;
281
+ constructor(message: string, code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed', status?: number | undefined, retryAfter?: number | undefined);
282
+ }
25
283
 
26
284
  /**
27
285
  * PKCE (RFC 7636) and the random values the login flow needs, built on Web Crypto
@@ -60,6 +318,27 @@ interface RoleDefinition {
60
318
  interface AuthzDeclaration {
61
319
  permissions?: PermissionDefinition[];
62
320
  roles?: RoleDefinition[];
321
+ /**
322
+ * Where this app's OLD login lives, while you are migrating off it.
323
+ *
324
+ * Declared here for the same reason roles are: it is a fact about the app, it belongs
325
+ * with the deploy, and the alternative is a URL pasted into a console and a secret
326
+ * pasted into an env file that quietly disagree six weeks later.
327
+ *
328
+ * IT DOES NOT TAKE EFFECT ON ITS OWN. Unlike a role — which affects only the app that
329
+ * declared it — this names a URL that every unknown email and the password typed with
330
+ * it will be offered to, on the environment's whole sign-in path. So it arrives as a
331
+ * proposal and an operator has to approve it in the console. Changing the url later
332
+ * drops that approval, on purpose.
333
+ */
334
+ legacyLogin?: LegacyLoginDeclaration;
335
+ }
336
+ /** Where an app's old login lives. See {@link AuthzDeclaration.legacyLogin}. */
337
+ interface LegacyLoginDeclaration {
338
+ /** Must be https — a password in the clear is readable by everything on the path. */
339
+ url: string;
340
+ /** At least 32 characters. It is the only thing proving a request came from Cbox ID. */
341
+ secret: string;
63
342
  }
64
343
  /**
65
344
  * The manifest as it is sent to Cbox ID: the declared permissions + roles plus a
@@ -69,6 +348,7 @@ interface AuthzManifest {
69
348
  version: string;
70
349
  permissions: PermissionDefinition[];
71
350
  roles: RoleDefinition[];
351
+ legacy_login?: LegacyLoginDeclaration;
72
352
  }
73
353
  /** What {@link publishManifest} needs to authenticate the push. */
74
354
  interface ManifestPublisherConfig {
@@ -101,7 +381,10 @@ interface ManifestSyncSummary {
101
381
  * @throws ConfigurationError on a missing key/name, a duplicate, or a role that
102
382
  * references a permission that was never declared.
103
383
  */
104
- declare function defineAuthz(declaration: AuthzDeclaration): Required<AuthzDeclaration>;
384
+ declare function defineAuthz(declaration: AuthzDeclaration): AuthzDeclaration & {
385
+ permissions: PermissionDefinition[];
386
+ roles: RoleDefinition[];
387
+ };
105
388
  /**
106
389
  * Build the manifest — the declared catalog plus a stable content hash `version`
107
390
  * (first 16 hex chars of the sha256 of the canonicalized catalog). The catalog is
@@ -123,4 +406,4 @@ declare function buildManifest(declaration: AuthzDeclaration): Promise<AuthzMani
123
406
  */
124
407
  declare function publishManifest(config: ManifestPublisherConfig, declaration: AuthzDeclaration): Promise<ManifestSyncSummary>;
125
408
 
126
- export { AuthenticationError, type AuthzDeclaration, type AuthzManifest, CboxIdError, ConfigurationError, InvalidStateError, type ManifestPublisherConfig, type ManifestSyncSummary, type PermissionDefinition, type RoleDefinition, buildManifest, challenge, createVerifier, defineAuthz, publishManifest, randomToken };
409
+ export { type Appearance, AuthenticationError, type AuthzDeclaration, type AuthzManifest, CboxIdError, CboxIdFrontend, ConfigurationError, FrontendApiError, type FrontendClientOptions, type FrontendConfig, type FrontendSession, InvalidStateError, type LegacyUser, type LegacyVerifierOptions, type ManifestPublisherConfig, type ManifestSyncSummary, type PermissionDefinition, type RoleDefinition, type SignInResult, type SocialProvider, buildManifest, challenge, createLegacyVerifier, createVerifier, defineAuthz, publishManifest, randomToken };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,242 @@
1
1
  export { A as AuthorizationRequest, C as CallbackParams, a as CboxIdClient, b as CboxIdConfig, c as CboxUser, D as DiscoveryDocument, R as RefreshedTokens, S as StoreSecretInput, d as StoredAuthState, T as TokenResponse, e as TokenTypeHint, V as VaultClient, f as VaultLease, g as VaultSecretRef, h as VerifyWebhookOptions, v as verifyWebhook } from './client-DkZSRmnO.js';
2
2
 
3
+ /**
4
+ * The handler on YOUR side of a legacy migration.
5
+ *
6
+ * Cbox ID offers it an email and a password it has never seen, and your code answers with
7
+ * the person or with nothing. On a yes they are created in Cbox ID as a result of the
8
+ * login that just succeeded, and from their next sign-in the old system is never asked
9
+ * again.
10
+ *
11
+ * WHY THIS IS A FACTORY AND NOT A DOCUMENT. The wire format is simple enough to describe
12
+ * in a paragraph — a signed POST, JSON in, JSON out — and describing it would mean every
13
+ * customer writes the signature check themselves. That check is exactly the part people
14
+ * get wrong: `===` instead of a constant-time compare, no freshness window, no thought
15
+ * about replay. So the factory owns all of it and you write the one function that knows
16
+ * your own database.
17
+ *
18
+ * It is deliberately framework-agnostic: it takes a `Request` and returns a `Response`,
19
+ * which is what Next.js route handlers, Remix, Hono, Bun and Deno all speak natively.
20
+ */
21
+ /** What your handler answers with. Returning `null` means "no", and is not an error. */
22
+ interface LegacyUser {
23
+ email: string;
24
+ name?: string | null;
25
+ emailVerified?: boolean;
26
+ /**
27
+ * Your stored hash, when you can expose it.
28
+ *
29
+ * Passing it lets the person keep their password in Cbox ID verbatim — it is stored as
30
+ * is and upgraded to the platform hasher on their next login, exactly like a
31
+ * bulk-imported user. Omit it and Cbox ID hashes the password they just proved they
32
+ * know, which is the honest option when your store cannot hand a hash over.
33
+ */
34
+ passwordHash?: string;
35
+ }
36
+ interface LegacyVerifierOptions {
37
+ /** The same secret you declared in your manifest, or configured in the console. */
38
+ secret: string;
39
+ /**
40
+ * Decide whether these credentials are good, and say who they belong to.
41
+ *
42
+ * Return `null` for no. THROWING IS NOT THE SAME as returning null: a throw means your
43
+ * store could not decide, and is answered with a 503 so Cbox ID refuses the sign-in
44
+ * rather than treating an outage as a wrong password.
45
+ */
46
+ verify: (email: string, password: string) => Promise<LegacyUser | null>;
47
+ /**
48
+ * How far out of date a request may be, in seconds. Defaults to 300.
49
+ *
50
+ * A window is what makes a captured request stop working. Too wide and a recording is
51
+ * useful for hours; too narrow and ordinary clock skew starts refusing real traffic.
52
+ */
53
+ toleranceSeconds?: number;
54
+ }
55
+ /**
56
+ * Build the request handler.
57
+ *
58
+ * ```ts
59
+ * // app/api/cbox-legacy/route.ts
60
+ * export const POST = createLegacyVerifier({
61
+ * secret: process.env.CBOX_LEGACY_SECRET!,
62
+ * async verify(email, password) {
63
+ * const row = await db.users.findByEmail(email)
64
+ * if (!row || !(await argon2.verify(row.password, password))) return null
65
+ * return { email: row.email, name: row.name, emailVerified: !!row.confirmedAt }
66
+ * },
67
+ * })
68
+ * ```
69
+ */
70
+ declare function createLegacyVerifier(options: LegacyVerifierOptions): (request: Request) => Promise<Response>;
71
+
72
+ /**
73
+ * The browser-safe half of the SDK.
74
+ *
75
+ * Everything else in this package assumes a server: it holds a client secret, it exchanges
76
+ * codes, it verifies webhooks. None of that may run in a page, which is why the UI
77
+ * components have always been redirect shells — a page that cannot authenticate itself to
78
+ * the identity provider cannot ask it anything, so it sends the person away and waits.
79
+ *
80
+ * A publishable key changes that. It is public on purpose: it ships in your bundle, it is
81
+ * visible in devtools, and it is safe there because it only works from the origins you
82
+ * registered. What it buys is that a page can read its OWN sign-in configuration — the
83
+ * endpoints, the social buttons, your theme — and render a form itself.
84
+ *
85
+ * This client deliberately has no secret, no token storage and no crypto. It reads two
86
+ * documents. Anything that mints or holds a credential still belongs on your server.
87
+ */
88
+ /** How a page identifies itself. Public — put it in your bundle. */
89
+ interface FrontendClientOptions {
90
+ /** Your identity provider's origin, e.g. `https://id.acme.com`. */
91
+ issuer: string;
92
+ /** `pk_live_…` or `pk_test_…`. Safe to publish. */
93
+ publishableKey: string;
94
+ /** Abort after this long. Defaults to 10s. */
95
+ timeoutMs?: number;
96
+ /**
97
+ * How many times to retry a transient failure. Defaults to 2 (so three attempts).
98
+ *
99
+ * Only the network and 5xx are retried. A refusal is a configuration problem and
100
+ * retrying it just delays the developer finding out; a 429 is honoured rather than
101
+ * hammered.
102
+ */
103
+ retries?: number;
104
+ /** Injectable for tests and for runtimes with a non-global fetch. */
105
+ fetch?: typeof globalThis.fetch;
106
+ }
107
+ /** A social button to draw. Name and provider only — never an internal id. */
108
+ interface SocialProvider {
109
+ provider: string;
110
+ name: string;
111
+ }
112
+ /** The customer's theme, when they have set one. Absent means "use your own defaults". */
113
+ interface Appearance {
114
+ preset: string;
115
+ radius: string;
116
+ font: string;
117
+ light: Record<string, string>;
118
+ dark: Record<string, string>;
119
+ }
120
+ /** Everything needed to draw a sign-in box, and nothing that identifies anybody. */
121
+ interface FrontendConfig {
122
+ mode: 'test' | 'live' | null;
123
+ issuer: string;
124
+ endpoints: {
125
+ authorization: string;
126
+ token: string;
127
+ userinfo: string;
128
+ end_session: string;
129
+ jwks: string;
130
+ };
131
+ social: SocialProvider[];
132
+ appearance?: Appearance;
133
+ }
134
+ /** What a sign-in attempt produced. */
135
+ type SignInResult = {
136
+ status: 'ok';
137
+ loginTicket: string;
138
+ expiresIn: number;
139
+ } | {
140
+ status: 'mfa_required';
141
+ } | {
142
+ status: 'otp_required';
143
+ } | {
144
+ status: 'sso_required';
145
+ } | {
146
+ status: 'invalid';
147
+ } | {
148
+ status: 'rate_limited';
149
+ retryAfter: number | undefined;
150
+ };
151
+ /** Who the browser is signed in as, or nobody. */
152
+ interface FrontendSession {
153
+ user: {
154
+ id: string;
155
+ email: string;
156
+ name: string | null;
157
+ } | null;
158
+ }
159
+ /**
160
+ * Reads the public configuration and the current session from a browser.
161
+ *
162
+ * ```ts
163
+ * const frontend = new CboxIdFrontend({
164
+ * issuer: 'https://id.acme.com',
165
+ * publishableKey: 'pk_live_…',
166
+ * })
167
+ *
168
+ * const config = await frontend.config() // cached for you; safe to call per component
169
+ * const { user } = await frontend.session(accessToken)
170
+ * ```
171
+ */
172
+ declare class CboxIdFrontend {
173
+ private readonly issuer;
174
+ private readonly publishableKey;
175
+ private readonly timeoutMs;
176
+ private readonly retries;
177
+ private readonly fetchImpl;
178
+ /**
179
+ * The config document, and the in-flight promise for it.
180
+ *
181
+ * Deduplicated rather than merely cached: a page with a sign-in form, a user button and
182
+ * an organization switcher mounts three components in the same tick, and three parallel
183
+ * fetches of the same immutable document is a waste the SDK should absorb rather than
184
+ * ask every consumer to.
185
+ */
186
+ private configPromise;
187
+ constructor(options: FrontendClientOptions);
188
+ /** Whether this key drives real sign-ins. Handy for a "test mode" badge in your own UI. */
189
+ get isLive(): boolean;
190
+ /**
191
+ * The public sign-in configuration.
192
+ *
193
+ * Fetched once per client instance. The document is small and changes when somebody
194
+ * edits it in the console, so a long-lived page picks changes up on its next full load
195
+ * rather than mid-session — which is the right trade for something that decides layout.
196
+ */
197
+ config(): Promise<FrontendConfig>;
198
+ /**
199
+ * Who is signed in, given an access token your app already holds.
200
+ *
201
+ * Returns `{ user: null }` rather than throwing when nobody is signed in. That is a
202
+ * state, not an error: a user button renders on pages nobody has signed in on, and
203
+ * making callers treat a rejection as a state is how flash-of-signed-out bugs happen.
204
+ *
205
+ * The publishable key grants nothing here — the token is the entire authority.
206
+ */
207
+ session(accessToken?: string): Promise<FrontendSession>;
208
+ /**
209
+ * Sign in with a password, from a form your page drew.
210
+ *
211
+ * WHAT COMES BACK IS A TICKET, NEVER A TOKEN. Handing tokens to a page that proved a
212
+ * password is the implicit grant, which OAuth 2.1 removes: tokens in a URL, in history,
213
+ * in `Referer`, with no client authentication and no PKCE binding. Spend the ticket by
214
+ * sending the browser to `/oauth/authorize` with your own PKCE challenge and
215
+ * `login_ticket` in the query — the ordinary flow, which then issues an ordinary code.
216
+ *
217
+ * It expires in sixty seconds. It is for one redirect, not for storing.
218
+ *
219
+ * ```ts
220
+ * const result = await frontend.signIn(email, password)
221
+ *
222
+ * if (result.status === 'ok') {
223
+ * window.location.href = `${config.endpoints.authorization}?${new URLSearchParams({
224
+ * client_id, redirect_uri, response_type: 'code', code_challenge, code_challenge_method: 'S256',
225
+ * login_ticket: result.loginTicket,
226
+ * })}`
227
+ * }
228
+ * ```
229
+ *
230
+ * EVERY REFUSAL IS THE SAME REFUSAL. `invalid` covers a wrong password, an unknown
231
+ * address and a locked account, because distinguishing them is the enumeration oracle
232
+ * the server refuses to build — do not present them differently in your UI either.
233
+ */
234
+ signIn(email: string, password: string): Promise<SignInResult>;
235
+ private post;
236
+ private get;
237
+ private attempt;
238
+ }
239
+
3
240
  /**
4
241
  * Base class for every error this SDK throws, so callers can `catch (e) { if (e
5
242
  * instanceof CboxIdError) ... }` in one branch.
@@ -22,6 +259,27 @@ declare class InvalidStateError extends CboxIdError {
22
259
  */
23
260
  declare class AuthenticationError extends CboxIdError {
24
261
  }
262
+ /**
263
+ * The Frontend API refused, or could not be reached.
264
+ *
265
+ * Carries a `code` because the four ways this goes wrong need four different responses
266
+ * from the caller, and telling them apart from a message string is the kind of thing
267
+ * that works until somebody rewords the message:
268
+ *
269
+ * - `origin_not_allowed` — the overwhelmingly likely cause of a refusal, and a
270
+ * configuration problem the developer fixes once in the console.
271
+ * - `rate_limited` — back off; `retryAfter` says for how long, when the server said.
272
+ * - `unavailable` — the network, or a 5xx. Transient by assumption; already retried.
273
+ * - `malformed` — a 200 whose body is not the document we expect, which means something
274
+ * between the page and us is rewriting responses (a proxy, an extension, a captive
275
+ * portal). Worth distinguishing, because retrying it never helps.
276
+ */
277
+ declare class FrontendApiError extends CboxIdError {
278
+ readonly code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed';
279
+ readonly status?: number | undefined;
280
+ readonly retryAfter?: number | undefined;
281
+ constructor(message: string, code: 'origin_not_allowed' | 'rate_limited' | 'unavailable' | 'malformed', status?: number | undefined, retryAfter?: number | undefined);
282
+ }
25
283
 
26
284
  /**
27
285
  * PKCE (RFC 7636) and the random values the login flow needs, built on Web Crypto
@@ -60,6 +318,27 @@ interface RoleDefinition {
60
318
  interface AuthzDeclaration {
61
319
  permissions?: PermissionDefinition[];
62
320
  roles?: RoleDefinition[];
321
+ /**
322
+ * Where this app's OLD login lives, while you are migrating off it.
323
+ *
324
+ * Declared here for the same reason roles are: it is a fact about the app, it belongs
325
+ * with the deploy, and the alternative is a URL pasted into a console and a secret
326
+ * pasted into an env file that quietly disagree six weeks later.
327
+ *
328
+ * IT DOES NOT TAKE EFFECT ON ITS OWN. Unlike a role — which affects only the app that
329
+ * declared it — this names a URL that every unknown email and the password typed with
330
+ * it will be offered to, on the environment's whole sign-in path. So it arrives as a
331
+ * proposal and an operator has to approve it in the console. Changing the url later
332
+ * drops that approval, on purpose.
333
+ */
334
+ legacyLogin?: LegacyLoginDeclaration;
335
+ }
336
+ /** Where an app's old login lives. See {@link AuthzDeclaration.legacyLogin}. */
337
+ interface LegacyLoginDeclaration {
338
+ /** Must be https — a password in the clear is readable by everything on the path. */
339
+ url: string;
340
+ /** At least 32 characters. It is the only thing proving a request came from Cbox ID. */
341
+ secret: string;
63
342
  }
64
343
  /**
65
344
  * The manifest as it is sent to Cbox ID: the declared permissions + roles plus a
@@ -69,6 +348,7 @@ interface AuthzManifest {
69
348
  version: string;
70
349
  permissions: PermissionDefinition[];
71
350
  roles: RoleDefinition[];
351
+ legacy_login?: LegacyLoginDeclaration;
72
352
  }
73
353
  /** What {@link publishManifest} needs to authenticate the push. */
74
354
  interface ManifestPublisherConfig {
@@ -101,7 +381,10 @@ interface ManifestSyncSummary {
101
381
  * @throws ConfigurationError on a missing key/name, a duplicate, or a role that
102
382
  * references a permission that was never declared.
103
383
  */
104
- declare function defineAuthz(declaration: AuthzDeclaration): Required<AuthzDeclaration>;
384
+ declare function defineAuthz(declaration: AuthzDeclaration): AuthzDeclaration & {
385
+ permissions: PermissionDefinition[];
386
+ roles: RoleDefinition[];
387
+ };
105
388
  /**
106
389
  * Build the manifest — the declared catalog plus a stable content hash `version`
107
390
  * (first 16 hex chars of the sha256 of the canonicalized catalog). The catalog is
@@ -123,4 +406,4 @@ declare function buildManifest(declaration: AuthzDeclaration): Promise<AuthzMani
123
406
  */
124
407
  declare function publishManifest(config: ManifestPublisherConfig, declaration: AuthzDeclaration): Promise<ManifestSyncSummary>;
125
408
 
126
- export { AuthenticationError, type AuthzDeclaration, type AuthzManifest, CboxIdError, ConfigurationError, InvalidStateError, type ManifestPublisherConfig, type ManifestSyncSummary, type PermissionDefinition, type RoleDefinition, buildManifest, challenge, createVerifier, defineAuthz, publishManifest, randomToken };
409
+ export { type Appearance, AuthenticationError, type AuthzDeclaration, type AuthzManifest, CboxIdError, CboxIdFrontend, ConfigurationError, FrontendApiError, type FrontendClientOptions, type FrontendConfig, type FrontendSession, InvalidStateError, type LegacyUser, type LegacyVerifierOptions, type ManifestPublisherConfig, type ManifestSyncSummary, type PermissionDefinition, type RoleDefinition, type SignInResult, type SocialProvider, buildManifest, challenge, createLegacyVerifier, createVerifier, defineAuthz, publishManifest, randomToken };