@cboxdk/id-js 0.18.0 → 0.19.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 +97 -1
- package/dist/{chunk-CXGKOJRJ.js → chunk-YGLMLIKL.js} +144 -9
- package/dist/chunk-YGLMLIKL.js.map +1 -0
- package/dist/{client-BxUHm5hX.d.cts → client-BT-jOm0p.d.cts} +126 -2
- package/dist/{client-BSZASrMo.d.ts → client-xlEJalu8.d.ts} +126 -2
- package/dist/index.cjs +211 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -6
- package/dist/index.d.ts +83 -6
- package/dist/index.js +63 -11
- package/dist/index.js.map +1 -1
- package/dist/management.cjs +726 -6
- package/dist/management.cjs.map +1 -1
- package/dist/management.d.cts +2557 -26
- package/dist/management.d.ts +2557 -26
- package/dist/management.js +726 -7
- package/dist/management.js.map +1 -1
- package/dist/nextjs.cjs +136 -5
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +3 -2
- package/dist/nextjs.d.ts +3 -2
- package/dist/nextjs.js +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/{types-BYZldock.d.cts → types-CC1bz68x.d.cts} +30 -1
- package/dist/{types-BYZldock.d.ts → types-CC1bz68x.d.ts} +30 -1
- package/package.json +1 -1
- package/dist/chunk-CXGKOJRJ.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as CboxIdError } from './errors-D61xljZE.cjs';
|
|
2
|
+
import { a as CboxIdConfig, A as AuthorizationRequestOptions, b as AuthorizationRequest, C as CboxUser, D as DeviceAuthorization, R as RefreshedTokens, T as TokenTypeHint } from './types-CC1bz68x.cjs';
|
|
2
3
|
|
|
3
4
|
/** A stored vault secret's metadata (never its plaintext). */
|
|
4
5
|
interface VaultSecretRef {
|
|
@@ -17,6 +18,98 @@ interface VaultLease {
|
|
|
17
18
|
secret: string;
|
|
18
19
|
expiresAt: string;
|
|
19
20
|
}
|
|
21
|
+
/** A provider a pipe can connect to. */
|
|
22
|
+
type PipeProvider = 'github' | 'google' | 'microsoft' | 'slack' | 'salesforce' | 'hubspot' | 'linear' | 'notion';
|
|
23
|
+
/**
|
|
24
|
+
* A fresh access token for one person's connected account at a provider — what
|
|
25
|
+
* {@link VaultClient.leasePipeToken} returns. Use it, drop it, lease again next time.
|
|
26
|
+
*/
|
|
27
|
+
interface PipeToken {
|
|
28
|
+
/** The provider's access token. Send it as a bearer token to the provider's API. */
|
|
29
|
+
accessToken: string;
|
|
30
|
+
tokenType: 'Bearer';
|
|
31
|
+
provider: string;
|
|
32
|
+
userId: string;
|
|
33
|
+
connectionId: string;
|
|
34
|
+
/** What the person consented to. */
|
|
35
|
+
scopes: string[];
|
|
36
|
+
/** When the PROVIDER stops accepting it; null for tokens that do not expire. */
|
|
37
|
+
expiresAt: string | null;
|
|
38
|
+
/** When to drop it and lease again. */
|
|
39
|
+
leaseExpiresAt: string;
|
|
40
|
+
/** What some providers need to be called at all — Salesforce's `instance_url`, Slack's `team.id`. */
|
|
41
|
+
metadata: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/** Where a person connects a provider, and where they come back to afterwards. */
|
|
44
|
+
interface PipeConnectOptions {
|
|
45
|
+
/** Your app's client id — the connect page shows its name and allows its origins. */
|
|
46
|
+
clientId?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Where to land afterwards, with `?provider=…&status=connected|cancelled|failed`. Must be
|
|
49
|
+
* on one of the app's registered redirect origins, or it is ignored.
|
|
50
|
+
*/
|
|
51
|
+
returnTo?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Add `client_id` and `return_to` to a connect URL — the one a lease error carries, or
|
|
55
|
+
* one built with {@link pipeConnectUrl}.
|
|
56
|
+
*/
|
|
57
|
+
declare function withConnectReturn(connectUrl: string, options?: PipeConnectOptions): string;
|
|
58
|
+
/**
|
|
59
|
+
* The hosted page where the signed-in person connects their account at `provider`:
|
|
60
|
+
* `{issuer}/account/connected-services/{provider}/connect?client_id=…&return_to=…`.
|
|
61
|
+
*/
|
|
62
|
+
declare function pipeConnectUrl(issuer: string, provider: PipeProvider | string, options?: PipeConnectOptions): string;
|
|
63
|
+
/**
|
|
64
|
+
* A pipe token lease was refused. Catch the subclasses for the cases you can act on:
|
|
65
|
+
*
|
|
66
|
+
* - {@link PipeNotConnectedError} (404) and {@link PipeReauthorizationRequiredError}
|
|
67
|
+
* (409) — send the person to `connectUrl`.
|
|
68
|
+
* - {@link PipeTemporarilyUnavailableError} (503) — retry after `retryAfter` seconds.
|
|
69
|
+
* - {@link PipeLeaseDeniedError} (403) — a configuration problem: the app is not granted
|
|
70
|
+
* the pipe, it is disabled, or the person is outside the app's organization.
|
|
71
|
+
*
|
|
72
|
+
* Anything else (401, 422, 429) arrives as this base class with `status` and `error`.
|
|
73
|
+
*/
|
|
74
|
+
declare class PipeLeaseError extends CboxIdError {
|
|
75
|
+
/** The server's error code, e.g. `not_connected`. */
|
|
76
|
+
readonly error: string | null;
|
|
77
|
+
readonly status: number;
|
|
78
|
+
/** Where to send the person to (re)connect — set on 404 and 409. */
|
|
79
|
+
readonly connectUrl: string | null;
|
|
80
|
+
/** Seconds to wait off `Retry-After`, when the server sent one. */
|
|
81
|
+
readonly retryAfter: number | null;
|
|
82
|
+
constructor(message: string,
|
|
83
|
+
/** The server's error code, e.g. `not_connected`. */
|
|
84
|
+
error: string | null, status: number,
|
|
85
|
+
/** Where to send the person to (re)connect — set on 404 and 409. */
|
|
86
|
+
connectUrl?: string | null,
|
|
87
|
+
/** Seconds to wait off `Retry-After`, when the server sent one. */
|
|
88
|
+
retryAfter?: number | null);
|
|
89
|
+
/** `connectUrl` with your `client_id` and `return_to` on it, or null when there is none. */
|
|
90
|
+
connectUrlWith(options: PipeConnectOptions): string | null;
|
|
91
|
+
}
|
|
92
|
+
/** The person has not connected this provider (404 `not_connected`). Send them to `connectUrl`. */
|
|
93
|
+
declare class PipeNotConnectedError extends PipeLeaseError {
|
|
94
|
+
readonly connectUrl: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The provider stopped accepting the connection — revoked, or the refresh token expired
|
|
98
|
+
* (409 `reauthorization_required`). Send the person to `connectUrl` to connect again.
|
|
99
|
+
*/
|
|
100
|
+
declare class PipeReauthorizationRequiredError extends PipeLeaseError {
|
|
101
|
+
readonly connectUrl: string;
|
|
102
|
+
}
|
|
103
|
+
/** The provider could not refresh the token just now (503). Retry after `retryAfter`. */
|
|
104
|
+
declare class PipeTemporarilyUnavailableError extends PipeLeaseError {
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The lease was denied (403 `lease_denied`): the app is not granted the pipe, the pipe is
|
|
108
|
+
* disabled or missing, the person is not in the app's organization, or `userId` names
|
|
109
|
+
* someone other than the token's person. One answer for every reason, by design.
|
|
110
|
+
*/
|
|
111
|
+
declare class PipeLeaseDeniedError extends PipeLeaseError {
|
|
112
|
+
}
|
|
20
113
|
interface StoreSecretInput {
|
|
21
114
|
name: string;
|
|
22
115
|
provider: string;
|
|
@@ -54,8 +147,32 @@ declare class VaultClient {
|
|
|
54
147
|
* is identified by the token's client; a lease with no live grant is refused.
|
|
55
148
|
*/
|
|
56
149
|
lease(secretId: string, purpose: string): Promise<VaultLease>;
|
|
150
|
+
/**
|
|
151
|
+
* Lease a fresh access token for a person's connected account at `provider` (Pipes,
|
|
152
|
+
* scope `vault.lease`). The token is refreshed first when it is about to expire, so
|
|
153
|
+
* you never handle a refresh token.
|
|
154
|
+
*
|
|
155
|
+
* With a client-credentials token, name the person in `userId`. With a token issued to
|
|
156
|
+
* your app FOR a person, leave it out — the lease is theirs.
|
|
157
|
+
*
|
|
158
|
+
* ```ts
|
|
159
|
+
* try {
|
|
160
|
+
* const { accessToken } = await vault.leasePipeToken('github', { userId, purpose: 'list-repos' })
|
|
161
|
+
* } catch (e) {
|
|
162
|
+
* if (e instanceof PipeNotConnectedError || e instanceof PipeReauthorizationRequiredError) {
|
|
163
|
+
* return redirect(e.connectUrlWith({ clientId, returnTo }))
|
|
164
|
+
* }
|
|
165
|
+
* throw e
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
leasePipeToken(provider: PipeProvider | string, options: {
|
|
170
|
+
purpose: string;
|
|
171
|
+
userId?: string;
|
|
172
|
+
}): Promise<PipeToken>;
|
|
57
173
|
private json;
|
|
58
174
|
private request;
|
|
175
|
+
private send;
|
|
59
176
|
}
|
|
60
177
|
|
|
61
178
|
/**
|
|
@@ -259,6 +376,13 @@ declare class CboxIdClient {
|
|
|
259
376
|
* registered. `organization` picks which of the person's organizations the keys act in;
|
|
260
377
|
* omitted, the page uses the one they are in.
|
|
261
378
|
*/
|
|
379
|
+
/**
|
|
380
|
+
* The hosted page where the signed-in person connects their account at `provider`
|
|
381
|
+
* (Pipes) — `/account/connected-services/{provider}/connect`, preselected to this app.
|
|
382
|
+
* They come back to `returnTo` with `?provider=…&status=connected|cancelled|failed`,
|
|
383
|
+
* honoured only on an origin the app registered.
|
|
384
|
+
*/
|
|
385
|
+
pipeConnectUrl(provider: PipeProvider | string, returnTo?: string): string;
|
|
262
386
|
apiKeysUrl(options?: {
|
|
263
387
|
clientId?: string;
|
|
264
388
|
returnTo?: string;
|
|
@@ -338,4 +462,4 @@ declare class CboxIdClient {
|
|
|
338
462
|
private fetchWithTimeout;
|
|
339
463
|
}
|
|
340
464
|
|
|
341
|
-
export { CboxIdClient as C, type StoreSecretInput as S, VaultClient as V, type WebhookHeaders as W, type CallbackParams as a, type StoredAuthState as
|
|
465
|
+
export { CboxIdClient as C, type PipeConnectOptions as P, type StoreSecretInput as S, VaultClient as V, type WebhookHeaders as W, type CallbackParams as a, PipeLeaseDeniedError as b, PipeLeaseError as c, PipeNotConnectedError as d, type PipeProvider as e, PipeReauthorizationRequiredError as f, PipeTemporarilyUnavailableError as g, type PipeToken as h, type StoredAuthState as i, type VaultLease as j, type VaultSecretRef as k, type VerifyStandardWebhookOptions as l, type VerifyWebhookOptions as m, verifyWebhook as n, pipeConnectUrl as p, verifyStandardWebhook as v, withConnectReturn as w };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as CboxIdError } from './errors-D61xljZE.js';
|
|
2
|
+
import { a as CboxIdConfig, A as AuthorizationRequestOptions, b as AuthorizationRequest, C as CboxUser, D as DeviceAuthorization, R as RefreshedTokens, T as TokenTypeHint } from './types-CC1bz68x.js';
|
|
2
3
|
|
|
3
4
|
/** A stored vault secret's metadata (never its plaintext). */
|
|
4
5
|
interface VaultSecretRef {
|
|
@@ -17,6 +18,98 @@ interface VaultLease {
|
|
|
17
18
|
secret: string;
|
|
18
19
|
expiresAt: string;
|
|
19
20
|
}
|
|
21
|
+
/** A provider a pipe can connect to. */
|
|
22
|
+
type PipeProvider = 'github' | 'google' | 'microsoft' | 'slack' | 'salesforce' | 'hubspot' | 'linear' | 'notion';
|
|
23
|
+
/**
|
|
24
|
+
* A fresh access token for one person's connected account at a provider — what
|
|
25
|
+
* {@link VaultClient.leasePipeToken} returns. Use it, drop it, lease again next time.
|
|
26
|
+
*/
|
|
27
|
+
interface PipeToken {
|
|
28
|
+
/** The provider's access token. Send it as a bearer token to the provider's API. */
|
|
29
|
+
accessToken: string;
|
|
30
|
+
tokenType: 'Bearer';
|
|
31
|
+
provider: string;
|
|
32
|
+
userId: string;
|
|
33
|
+
connectionId: string;
|
|
34
|
+
/** What the person consented to. */
|
|
35
|
+
scopes: string[];
|
|
36
|
+
/** When the PROVIDER stops accepting it; null for tokens that do not expire. */
|
|
37
|
+
expiresAt: string | null;
|
|
38
|
+
/** When to drop it and lease again. */
|
|
39
|
+
leaseExpiresAt: string;
|
|
40
|
+
/** What some providers need to be called at all — Salesforce's `instance_url`, Slack's `team.id`. */
|
|
41
|
+
metadata: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
/** Where a person connects a provider, and where they come back to afterwards. */
|
|
44
|
+
interface PipeConnectOptions {
|
|
45
|
+
/** Your app's client id — the connect page shows its name and allows its origins. */
|
|
46
|
+
clientId?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Where to land afterwards, with `?provider=…&status=connected|cancelled|failed`. Must be
|
|
49
|
+
* on one of the app's registered redirect origins, or it is ignored.
|
|
50
|
+
*/
|
|
51
|
+
returnTo?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Add `client_id` and `return_to` to a connect URL — the one a lease error carries, or
|
|
55
|
+
* one built with {@link pipeConnectUrl}.
|
|
56
|
+
*/
|
|
57
|
+
declare function withConnectReturn(connectUrl: string, options?: PipeConnectOptions): string;
|
|
58
|
+
/**
|
|
59
|
+
* The hosted page where the signed-in person connects their account at `provider`:
|
|
60
|
+
* `{issuer}/account/connected-services/{provider}/connect?client_id=…&return_to=…`.
|
|
61
|
+
*/
|
|
62
|
+
declare function pipeConnectUrl(issuer: string, provider: PipeProvider | string, options?: PipeConnectOptions): string;
|
|
63
|
+
/**
|
|
64
|
+
* A pipe token lease was refused. Catch the subclasses for the cases you can act on:
|
|
65
|
+
*
|
|
66
|
+
* - {@link PipeNotConnectedError} (404) and {@link PipeReauthorizationRequiredError}
|
|
67
|
+
* (409) — send the person to `connectUrl`.
|
|
68
|
+
* - {@link PipeTemporarilyUnavailableError} (503) — retry after `retryAfter` seconds.
|
|
69
|
+
* - {@link PipeLeaseDeniedError} (403) — a configuration problem: the app is not granted
|
|
70
|
+
* the pipe, it is disabled, or the person is outside the app's organization.
|
|
71
|
+
*
|
|
72
|
+
* Anything else (401, 422, 429) arrives as this base class with `status` and `error`.
|
|
73
|
+
*/
|
|
74
|
+
declare class PipeLeaseError extends CboxIdError {
|
|
75
|
+
/** The server's error code, e.g. `not_connected`. */
|
|
76
|
+
readonly error: string | null;
|
|
77
|
+
readonly status: number;
|
|
78
|
+
/** Where to send the person to (re)connect — set on 404 and 409. */
|
|
79
|
+
readonly connectUrl: string | null;
|
|
80
|
+
/** Seconds to wait off `Retry-After`, when the server sent one. */
|
|
81
|
+
readonly retryAfter: number | null;
|
|
82
|
+
constructor(message: string,
|
|
83
|
+
/** The server's error code, e.g. `not_connected`. */
|
|
84
|
+
error: string | null, status: number,
|
|
85
|
+
/** Where to send the person to (re)connect — set on 404 and 409. */
|
|
86
|
+
connectUrl?: string | null,
|
|
87
|
+
/** Seconds to wait off `Retry-After`, when the server sent one. */
|
|
88
|
+
retryAfter?: number | null);
|
|
89
|
+
/** `connectUrl` with your `client_id` and `return_to` on it, or null when there is none. */
|
|
90
|
+
connectUrlWith(options: PipeConnectOptions): string | null;
|
|
91
|
+
}
|
|
92
|
+
/** The person has not connected this provider (404 `not_connected`). Send them to `connectUrl`. */
|
|
93
|
+
declare class PipeNotConnectedError extends PipeLeaseError {
|
|
94
|
+
readonly connectUrl: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The provider stopped accepting the connection — revoked, or the refresh token expired
|
|
98
|
+
* (409 `reauthorization_required`). Send the person to `connectUrl` to connect again.
|
|
99
|
+
*/
|
|
100
|
+
declare class PipeReauthorizationRequiredError extends PipeLeaseError {
|
|
101
|
+
readonly connectUrl: string;
|
|
102
|
+
}
|
|
103
|
+
/** The provider could not refresh the token just now (503). Retry after `retryAfter`. */
|
|
104
|
+
declare class PipeTemporarilyUnavailableError extends PipeLeaseError {
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The lease was denied (403 `lease_denied`): the app is not granted the pipe, the pipe is
|
|
108
|
+
* disabled or missing, the person is not in the app's organization, or `userId` names
|
|
109
|
+
* someone other than the token's person. One answer for every reason, by design.
|
|
110
|
+
*/
|
|
111
|
+
declare class PipeLeaseDeniedError extends PipeLeaseError {
|
|
112
|
+
}
|
|
20
113
|
interface StoreSecretInput {
|
|
21
114
|
name: string;
|
|
22
115
|
provider: string;
|
|
@@ -54,8 +147,32 @@ declare class VaultClient {
|
|
|
54
147
|
* is identified by the token's client; a lease with no live grant is refused.
|
|
55
148
|
*/
|
|
56
149
|
lease(secretId: string, purpose: string): Promise<VaultLease>;
|
|
150
|
+
/**
|
|
151
|
+
* Lease a fresh access token for a person's connected account at `provider` (Pipes,
|
|
152
|
+
* scope `vault.lease`). The token is refreshed first when it is about to expire, so
|
|
153
|
+
* you never handle a refresh token.
|
|
154
|
+
*
|
|
155
|
+
* With a client-credentials token, name the person in `userId`. With a token issued to
|
|
156
|
+
* your app FOR a person, leave it out — the lease is theirs.
|
|
157
|
+
*
|
|
158
|
+
* ```ts
|
|
159
|
+
* try {
|
|
160
|
+
* const { accessToken } = await vault.leasePipeToken('github', { userId, purpose: 'list-repos' })
|
|
161
|
+
* } catch (e) {
|
|
162
|
+
* if (e instanceof PipeNotConnectedError || e instanceof PipeReauthorizationRequiredError) {
|
|
163
|
+
* return redirect(e.connectUrlWith({ clientId, returnTo }))
|
|
164
|
+
* }
|
|
165
|
+
* throw e
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
leasePipeToken(provider: PipeProvider | string, options: {
|
|
170
|
+
purpose: string;
|
|
171
|
+
userId?: string;
|
|
172
|
+
}): Promise<PipeToken>;
|
|
57
173
|
private json;
|
|
58
174
|
private request;
|
|
175
|
+
private send;
|
|
59
176
|
}
|
|
60
177
|
|
|
61
178
|
/**
|
|
@@ -259,6 +376,13 @@ declare class CboxIdClient {
|
|
|
259
376
|
* registered. `organization` picks which of the person's organizations the keys act in;
|
|
260
377
|
* omitted, the page uses the one they are in.
|
|
261
378
|
*/
|
|
379
|
+
/**
|
|
380
|
+
* The hosted page where the signed-in person connects their account at `provider`
|
|
381
|
+
* (Pipes) — `/account/connected-services/{provider}/connect`, preselected to this app.
|
|
382
|
+
* They come back to `returnTo` with `?provider=…&status=connected|cancelled|failed`,
|
|
383
|
+
* honoured only on an origin the app registered.
|
|
384
|
+
*/
|
|
385
|
+
pipeConnectUrl(provider: PipeProvider | string, returnTo?: string): string;
|
|
262
386
|
apiKeysUrl(options?: {
|
|
263
387
|
clientId?: string;
|
|
264
388
|
returnTo?: string;
|
|
@@ -338,4 +462,4 @@ declare class CboxIdClient {
|
|
|
338
462
|
private fetchWithTimeout;
|
|
339
463
|
}
|
|
340
464
|
|
|
341
|
-
export { CboxIdClient as C, type StoreSecretInput as S, VaultClient as V, type WebhookHeaders as W, type CallbackParams as a, type StoredAuthState as
|
|
465
|
+
export { CboxIdClient as C, type PipeConnectOptions as P, type StoreSecretInput as S, VaultClient as V, type WebhookHeaders as W, type CallbackParams as a, PipeLeaseDeniedError as b, PipeLeaseError as c, PipeNotConnectedError as d, type PipeProvider as e, PipeReauthorizationRequiredError as f, PipeTemporarilyUnavailableError as g, type PipeToken as h, type StoredAuthState as i, type VaultLease as j, type VaultSecretRef as k, type VerifyStandardWebhookOptions as l, type VerifyWebhookOptions as m, verifyWebhook as n, pipeConnectUrl as p, verifyStandardWebhook as v, withConnectReturn as w };
|
package/dist/index.cjs
CHANGED
|
@@ -174,6 +174,7 @@ function assertSecureIssuer(issuer) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
// src/frontend.ts
|
|
177
|
+
var SECOND_FACTOR_KINDS = /* @__PURE__ */ new Set(["totp", "sms", "recovery_code"]);
|
|
177
178
|
var CboxIdFrontend = class {
|
|
178
179
|
issuer;
|
|
179
180
|
publishableKey;
|
|
@@ -188,7 +189,7 @@ var CboxIdFrontend = class {
|
|
|
188
189
|
* fetches of the same immutable document is a waste the SDK should absorb rather than
|
|
189
190
|
* ask every consumer to.
|
|
190
191
|
*/
|
|
191
|
-
|
|
192
|
+
configPromises = /* @__PURE__ */ new Map();
|
|
192
193
|
constructor(options) {
|
|
193
194
|
if (!options.issuer) {
|
|
194
195
|
throw new ConfigurationError("issuer is required.");
|
|
@@ -215,13 +216,20 @@ var CboxIdFrontend = class {
|
|
|
215
216
|
* Fetched once per client instance. The document is small and changes when somebody
|
|
216
217
|
* edits it in the console, so a long-lived page picks changes up on its next full load
|
|
217
218
|
* rather than mid-session — which is the right trade for something that decides layout.
|
|
219
|
+
* Each `organization` is fetched and cached on its own.
|
|
218
220
|
*/
|
|
219
|
-
async config() {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
async config(options = {}) {
|
|
222
|
+
const organization2 = options.organization ?? "";
|
|
223
|
+
let pending = this.configPromises.get(organization2);
|
|
224
|
+
if (pending === void 0) {
|
|
225
|
+
const path = organization2 === "" ? "/frontend/v1/config" : `/frontend/v1/config?${new URLSearchParams({ organization: organization2 }).toString()}`;
|
|
226
|
+
pending = this.get(path, {}, isFrontendConfig).catch((error) => {
|
|
227
|
+
this.configPromises.delete(organization2);
|
|
228
|
+
throw error;
|
|
229
|
+
});
|
|
230
|
+
this.configPromises.set(organization2, pending);
|
|
231
|
+
}
|
|
232
|
+
return pending;
|
|
225
233
|
}
|
|
226
234
|
/**
|
|
227
235
|
* Who is signed in, given an access token your app already holds.
|
|
@@ -283,8 +291,12 @@ var CboxIdFrontend = class {
|
|
|
283
291
|
if (body.status === "ok" && typeof body.login_ticket === "string") {
|
|
284
292
|
return { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 };
|
|
285
293
|
}
|
|
286
|
-
if (
|
|
287
|
-
|
|
294
|
+
if (body.status === "mfa_required" && typeof body.mfa_token === "string") {
|
|
295
|
+
const factors = Array.isArray(body.factors) ? body.factors.filter((f) => typeof f === "string" && SECOND_FACTOR_KINDS.has(f)) : void 0;
|
|
296
|
+
return { status: "mfa_required", mfaToken: body.mfa_token, ...factors ? { factors } : {} };
|
|
297
|
+
}
|
|
298
|
+
if (body.status === "otp_required" && typeof body.mfa_token === "string") {
|
|
299
|
+
return { status: "otp_required", mfaToken: body.mfa_token };
|
|
288
300
|
}
|
|
289
301
|
if (body.status === "sso_required") {
|
|
290
302
|
return { status: "sso_required" };
|
|
@@ -301,6 +313,11 @@ var CboxIdFrontend = class {
|
|
|
301
313
|
*
|
|
302
314
|
* A wrong code costs an attempt, not the sign-in: five are allowed before the token
|
|
303
315
|
* dies and the person starts from the password again.
|
|
316
|
+
*
|
|
317
|
+
* `method` says which challenge the code answers: `'mfa'` for an authenticator or
|
|
318
|
+
* recovery code, `'otp'` for an emailed code (`otp_required`), `'sms'` for a code sent
|
|
319
|
+
* with {@link sendSmsCode}. A texted code is six digits like an authenticator's, so the
|
|
320
|
+
* server cannot tell them apart by shape — answering one as the other is refused.
|
|
304
321
|
*/
|
|
305
322
|
async submitSecondFactor(mfaToken, code, method = "mfa") {
|
|
306
323
|
const response = await this.post("/frontend/v1/sign-in/factor", { mfa_token: mfaToken, code, method });
|
|
@@ -312,6 +329,41 @@ var CboxIdFrontend = class {
|
|
|
312
329
|
}
|
|
313
330
|
return body.status === "ok" && typeof body.login_ticket === "string" ? { status: "ok", loginTicket: body.login_ticket, expiresIn: body.expires_in ?? 60 } : { status: "invalid" };
|
|
314
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Text the person a sign-in code, when `mfa_required` listed `'sms'` in `factors`.
|
|
334
|
+
* Then finish with `submitSecondFactor(mfaToken, code, 'sms')`.
|
|
335
|
+
*
|
|
336
|
+
* Only call it when the person asks ("Text me a code"), never on page load: each send
|
|
337
|
+
* spends one of the token's five attempts — that is what bounds how many texts one
|
|
338
|
+
* proved password can cause — and every text costs the customer money.
|
|
339
|
+
*
|
|
340
|
+
* ```ts
|
|
341
|
+
* if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
|
|
342
|
+
* const sent = await frontend.sendSmsCode(result.mfaToken)
|
|
343
|
+
* if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`)
|
|
344
|
+
* }
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
async sendSmsCode(mfaToken) {
|
|
348
|
+
const response = await this.post("/frontend/v1/sign-in/factor/sms", { mfa_token: mfaToken });
|
|
349
|
+
let body;
|
|
350
|
+
try {
|
|
351
|
+
body = await response.json();
|
|
352
|
+
} catch {
|
|
353
|
+
throw new FrontendApiError("Cbox ID returned a body that is not JSON.", "malformed", response.status);
|
|
354
|
+
}
|
|
355
|
+
if (body.status === "sent" && typeof body.to === "string") {
|
|
356
|
+
return { status: "sent", to: body.to, expiresIn: typeof body.expires_in === "number" ? body.expires_in : 300 };
|
|
357
|
+
}
|
|
358
|
+
if (body.status === "rate_limited" || response.status === 429) {
|
|
359
|
+
const retryAfter = typeof body.retry_after === "number" ? body.retry_after : Number(response.headers.get("Retry-After") ?? "");
|
|
360
|
+
return { status: "rate_limited", retryAfter: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : void 0 };
|
|
361
|
+
}
|
|
362
|
+
if (body.status === "unavailable") {
|
|
363
|
+
return { status: "unavailable" };
|
|
364
|
+
}
|
|
365
|
+
return { status: "invalid" };
|
|
366
|
+
}
|
|
315
367
|
/**
|
|
316
368
|
* The WebAuthn options for a passkey sign-in, and the handle that carries the challenge.
|
|
317
369
|
*
|
|
@@ -607,6 +659,14 @@ function hasRole(source, role) {
|
|
|
607
659
|
function hasPermission(source, permission) {
|
|
608
660
|
return permissions(source).includes(permission);
|
|
609
661
|
}
|
|
662
|
+
var FEATURE_FLAGS_SCOPE = "feature_flags";
|
|
663
|
+
function featureFlags(source) {
|
|
664
|
+
const claim = claimsOf(source)["feature_flags"];
|
|
665
|
+
return Array.isArray(claim) ? stringList(claim) : null;
|
|
666
|
+
}
|
|
667
|
+
function hasFeature(source, key) {
|
|
668
|
+
return featureFlags(source)?.includes(key) ?? false;
|
|
669
|
+
}
|
|
610
670
|
var MAX_ACTOR_DEPTH = 8;
|
|
611
671
|
function parseActor(claim, depth) {
|
|
612
672
|
if (claim === void 0 || claim === null) {
|
|
@@ -633,6 +693,43 @@ function isUser(source) {
|
|
|
633
693
|
}
|
|
634
694
|
|
|
635
695
|
// src/vault.ts
|
|
696
|
+
function withConnectReturn(connectUrl, options = {}) {
|
|
697
|
+
const url = new URL(connectUrl);
|
|
698
|
+
if (options.clientId) url.searchParams.set("client_id", options.clientId);
|
|
699
|
+
if (options.returnTo) url.searchParams.set("return_to", options.returnTo);
|
|
700
|
+
return url.toString();
|
|
701
|
+
}
|
|
702
|
+
function pipeConnectUrl(issuer, provider, options = {}) {
|
|
703
|
+
return withConnectReturn(
|
|
704
|
+
`${issuer.replace(/\/$/, "")}/account/connected-services/${encodeURIComponent(provider)}/connect`,
|
|
705
|
+
options
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
var PipeLeaseError = class extends CboxIdError {
|
|
709
|
+
constructor(message, error, status, connectUrl = null, retryAfter = null) {
|
|
710
|
+
super(message);
|
|
711
|
+
this.error = error;
|
|
712
|
+
this.status = status;
|
|
713
|
+
this.connectUrl = connectUrl;
|
|
714
|
+
this.retryAfter = retryAfter;
|
|
715
|
+
}
|
|
716
|
+
error;
|
|
717
|
+
status;
|
|
718
|
+
connectUrl;
|
|
719
|
+
retryAfter;
|
|
720
|
+
/** `connectUrl` with your `client_id` and `return_to` on it, or null when there is none. */
|
|
721
|
+
connectUrlWith(options) {
|
|
722
|
+
return this.connectUrl === null ? null : withConnectReturn(this.connectUrl, options);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
var PipeNotConnectedError = class extends PipeLeaseError {
|
|
726
|
+
};
|
|
727
|
+
var PipeReauthorizationRequiredError = class extends PipeLeaseError {
|
|
728
|
+
};
|
|
729
|
+
var PipeTemporarilyUnavailableError = class extends PipeLeaseError {
|
|
730
|
+
};
|
|
731
|
+
var PipeLeaseDeniedError = class extends PipeLeaseError {
|
|
732
|
+
};
|
|
636
733
|
var VaultClient = class {
|
|
637
734
|
constructor(issuer, token, timeoutMs = 1e4) {
|
|
638
735
|
this.token = token;
|
|
@@ -688,11 +785,69 @@ var VaultClient = class {
|
|
|
688
785
|
return { secretId: r.secret_id, provider: r.provider, secret: r.secret, expiresAt: r.expires_at };
|
|
689
786
|
});
|
|
690
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* Lease a fresh access token for a person's connected account at `provider` (Pipes,
|
|
790
|
+
* scope `vault.lease`). The token is refreshed first when it is about to expire, so
|
|
791
|
+
* you never handle a refresh token.
|
|
792
|
+
*
|
|
793
|
+
* With a client-credentials token, name the person in `userId`. With a token issued to
|
|
794
|
+
* your app FOR a person, leave it out — the lease is theirs.
|
|
795
|
+
*
|
|
796
|
+
* ```ts
|
|
797
|
+
* try {
|
|
798
|
+
* const { accessToken } = await vault.leasePipeToken('github', { userId, purpose: 'list-repos' })
|
|
799
|
+
* } catch (e) {
|
|
800
|
+
* if (e instanceof PipeNotConnectedError || e instanceof PipeReauthorizationRequiredError) {
|
|
801
|
+
* return redirect(e.connectUrlWith({ clientId, returnTo }))
|
|
802
|
+
* }
|
|
803
|
+
* throw e
|
|
804
|
+
* }
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
async leasePipeToken(provider, options) {
|
|
808
|
+
const path = `/pipes/${encodeURIComponent(provider)}/token`;
|
|
809
|
+
const response = await this.send("POST", path, {
|
|
810
|
+
purpose: options.purpose,
|
|
811
|
+
...options.userId !== void 0 ? { user_id: options.userId } : {}
|
|
812
|
+
});
|
|
813
|
+
let body = {};
|
|
814
|
+
try {
|
|
815
|
+
const parsed = await response.json();
|
|
816
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
817
|
+
body = parsed;
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
if (!response.ok) {
|
|
822
|
+
throw pipeLeaseError(response, body);
|
|
823
|
+
}
|
|
824
|
+
if (typeof body.access_token !== "string") {
|
|
825
|
+
throw new PipeLeaseError("The pipe lease answered without an access token.", null, response.status);
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
accessToken: body.access_token,
|
|
829
|
+
tokenType: "Bearer",
|
|
830
|
+
provider: String(body.provider ?? provider),
|
|
831
|
+
userId: String(body.user_id ?? ""),
|
|
832
|
+
connectionId: String(body.connection_id ?? ""),
|
|
833
|
+
scopes: Array.isArray(body.scopes) ? body.scopes.filter((s) => typeof s === "string") : [],
|
|
834
|
+
expiresAt: typeof body.expires_at === "string" ? body.expires_at : null,
|
|
835
|
+
leaseExpiresAt: String(body.lease_expires_at ?? ""),
|
|
836
|
+
metadata: isStringRecord(body.metadata) ? body.metadata : {}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
691
839
|
async json(method, path, body) {
|
|
692
840
|
const response = await this.request(method, path, body);
|
|
693
841
|
return await response.json();
|
|
694
842
|
}
|
|
695
843
|
async request(method, path, body) {
|
|
844
|
+
const response = await this.send(method, path, body);
|
|
845
|
+
if (!response.ok) {
|
|
846
|
+
throw new AuthenticationError(`Vault ${method} ${path} failed with status ${response.status}.`);
|
|
847
|
+
}
|
|
848
|
+
return response;
|
|
849
|
+
}
|
|
850
|
+
async send(method, path, body) {
|
|
696
851
|
const controller = new AbortController();
|
|
697
852
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
698
853
|
try {
|
|
@@ -707,16 +862,35 @@ var VaultClient = class {
|
|
|
707
862
|
if (body) {
|
|
708
863
|
init.body = JSON.stringify(body);
|
|
709
864
|
}
|
|
710
|
-
|
|
711
|
-
if (!response.ok) {
|
|
712
|
-
throw new AuthenticationError(`Vault ${method} ${path} failed with status ${response.status}.`);
|
|
713
|
-
}
|
|
714
|
-
return response;
|
|
865
|
+
return await fetch(`${this.base}${path}`, init);
|
|
715
866
|
} finally {
|
|
716
867
|
clearTimeout(timer);
|
|
717
868
|
}
|
|
718
869
|
}
|
|
719
870
|
};
|
|
871
|
+
function pipeLeaseError(response, body) {
|
|
872
|
+
const error = typeof body.error === "string" ? body.error : null;
|
|
873
|
+
const message = typeof body.message === "string" && body.message !== "" ? body.message : `The pipe lease failed with status ${response.status}.`;
|
|
874
|
+
const connectUrl = typeof body.connect_url === "string" && body.connect_url !== "" ? body.connect_url : null;
|
|
875
|
+
const header2 = response.headers.get("retry-after")?.trim() ?? "";
|
|
876
|
+
const retryAfter = /^\d+$/.test(header2) ? Number(header2) : null;
|
|
877
|
+
if (response.status === 404 && connectUrl !== null) {
|
|
878
|
+
return new PipeNotConnectedError(message, error, 404, connectUrl, retryAfter);
|
|
879
|
+
}
|
|
880
|
+
if (response.status === 409 && connectUrl !== null) {
|
|
881
|
+
return new PipeReauthorizationRequiredError(message, error, 409, connectUrl, retryAfter);
|
|
882
|
+
}
|
|
883
|
+
if (response.status === 503) {
|
|
884
|
+
return new PipeTemporarilyUnavailableError(message, error, 503, connectUrl, retryAfter);
|
|
885
|
+
}
|
|
886
|
+
if (response.status === 403 && error !== "insufficient_scope") {
|
|
887
|
+
return new PipeLeaseDeniedError(message, error, 403, connectUrl, retryAfter);
|
|
888
|
+
}
|
|
889
|
+
return new PipeLeaseError(message, error, response.status, connectUrl, retryAfter);
|
|
890
|
+
}
|
|
891
|
+
function isStringRecord(value) {
|
|
892
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((v) => typeof v === "string");
|
|
893
|
+
}
|
|
720
894
|
|
|
721
895
|
// src/webhook.ts
|
|
722
896
|
function timingSafeEqual2(a, b) {
|
|
@@ -986,6 +1160,7 @@ var CboxIdClient = class {
|
|
|
986
1160
|
organization: organization(claims),
|
|
987
1161
|
roles: roles(claims),
|
|
988
1162
|
permissions: permissions(claims),
|
|
1163
|
+
featureFlags: featureFlags(claims),
|
|
989
1164
|
actor: actor(claims),
|
|
990
1165
|
sessionId: sessionId(claims),
|
|
991
1166
|
// Only present when the instance emitted the claim (exactOptionalPropertyTypes).
|
|
@@ -1165,6 +1340,18 @@ var CboxIdClient = class {
|
|
|
1165
1340
|
* registered. `organization` picks which of the person's organizations the keys act in;
|
|
1166
1341
|
* omitted, the page uses the one they are in.
|
|
1167
1342
|
*/
|
|
1343
|
+
/**
|
|
1344
|
+
* The hosted page where the signed-in person connects their account at `provider`
|
|
1345
|
+
* (Pipes) — `/account/connected-services/{provider}/connect`, preselected to this app.
|
|
1346
|
+
* They come back to `returnTo` with `?provider=…&status=connected|cancelled|failed`,
|
|
1347
|
+
* honoured only on an origin the app registered.
|
|
1348
|
+
*/
|
|
1349
|
+
pipeConnectUrl(provider, returnTo) {
|
|
1350
|
+
return pipeConnectUrl(this.config.issuer, provider, {
|
|
1351
|
+
clientId: this.config.clientId,
|
|
1352
|
+
...returnTo !== void 0 ? { returnTo } : {}
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1168
1355
|
apiKeysUrl(options = {}) {
|
|
1169
1356
|
const params = new URLSearchParams({ client_id: options.clientId ?? this.config.clientId });
|
|
1170
1357
|
if (options.returnTo) {
|
|
@@ -1693,8 +1880,14 @@ exports.CboxIdClient = CboxIdClient;
|
|
|
1693
1880
|
exports.CboxIdError = CboxIdError;
|
|
1694
1881
|
exports.CboxIdFrontend = CboxIdFrontend;
|
|
1695
1882
|
exports.ConfigurationError = ConfigurationError;
|
|
1883
|
+
exports.FEATURE_FLAGS_SCOPE = FEATURE_FLAGS_SCOPE;
|
|
1696
1884
|
exports.FrontendApiError = FrontendApiError;
|
|
1697
1885
|
exports.InvalidStateError = InvalidStateError;
|
|
1886
|
+
exports.PipeLeaseDeniedError = PipeLeaseDeniedError;
|
|
1887
|
+
exports.PipeLeaseError = PipeLeaseError;
|
|
1888
|
+
exports.PipeNotConnectedError = PipeNotConnectedError;
|
|
1889
|
+
exports.PipeReauthorizationRequiredError = PipeReauthorizationRequiredError;
|
|
1890
|
+
exports.PipeTemporarilyUnavailableError = PipeTemporarilyUnavailableError;
|
|
1698
1891
|
exports.VaultClient = VaultClient;
|
|
1699
1892
|
exports.actor = actor;
|
|
1700
1893
|
exports.buildManifest = buildManifest;
|
|
@@ -1702,16 +1895,20 @@ exports.challenge = challenge;
|
|
|
1702
1895
|
exports.createLegacyVerifier = createLegacyVerifier;
|
|
1703
1896
|
exports.createVerifier = createVerifier;
|
|
1704
1897
|
exports.defineAuthz = defineAuthz;
|
|
1898
|
+
exports.featureFlags = featureFlags;
|
|
1899
|
+
exports.hasFeature = hasFeature;
|
|
1705
1900
|
exports.hasPermission = hasPermission;
|
|
1706
1901
|
exports.hasRole = hasRole;
|
|
1707
1902
|
exports.isSupportSession = isSupportSession;
|
|
1708
1903
|
exports.organization = organization;
|
|
1709
1904
|
exports.permissions = permissions;
|
|
1905
|
+
exports.pipeConnectUrl = pipeConnectUrl;
|
|
1710
1906
|
exports.publishManifest = publishManifest;
|
|
1711
1907
|
exports.randomToken = randomToken;
|
|
1712
1908
|
exports.roles = roles;
|
|
1713
1909
|
exports.sessionId = sessionId;
|
|
1714
1910
|
exports.verifyStandardWebhook = verifyStandardWebhook;
|
|
1715
1911
|
exports.verifyWebhook = verifyWebhook;
|
|
1912
|
+
exports.withConnectReturn = withConnectReturn;
|
|
1716
1913
|
//# sourceMappingURL=index.cjs.map
|
|
1717
1914
|
//# sourceMappingURL=index.cjs.map
|