@atlasauth/js 0.1.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/attempt.d.ts +71 -0
- package/dist/attempt.js +89 -0
- package/dist/attempt.js.map +1 -0
- package/dist/check-session.d.ts +56 -0
- package/dist/check-session.js +106 -0
- package/dist/check-session.js.map +1 -0
- package/dist/connect.d.ts +50 -0
- package/dist/connect.js +72 -0
- package/dist/connect.js.map +1 -0
- package/dist/fapi.d.ts +123 -0
- package/dist/fapi.js +314 -0
- package/dist/fapi.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +112 -0
- package/dist/native.js +201 -0
- package/dist/native.js.map +1 -0
- package/dist/passkey.d.ts +70 -0
- package/dist/passkey.js +126 -0
- package/dist/passkey.js.map +1 -0
- package/dist/password-reset.d.ts +41 -0
- package/dist/password-reset.js +80 -0
- package/dist/password-reset.js.map +1 -0
- package/dist/reauth.d.ts +29 -0
- package/dist/reauth.js +45 -0
- package/dist/reauth.js.map +1 -0
- package/dist/redirect.d.ts +38 -0
- package/dist/redirect.js +56 -0
- package/dist/redirect.js.map +1 -0
- package/dist/siwe.d.ts +23 -0
- package/dist/siwe.js +28 -0
- package/dist/siwe.js.map +1 -0
- package/dist/tab-election.d.ts +68 -0
- package/dist/tab-election.js +111 -0
- package/dist/tab-election.js.map +1 -0
- package/dist/telegram.d.ts +25 -0
- package/dist/telegram.js +18 -0
- package/dist/telegram.js.map +1 -0
- package/dist/telemetry.d.ts +125 -0
- package/dist/telemetry.js +357 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/token-cache.d.ts +74 -0
- package/dist/token-cache.js +104 -0
- package/dist/token-cache.js.map +1 -0
- package/package.json +26 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §5: "the client reads `status` and renders whatever the server demands next;
|
|
3
|
+
* it never picks the step itself."
|
|
4
|
+
*
|
|
5
|
+
* This is the client half of that contract, and it is written as an exhaustive
|
|
6
|
+
* mapping rather than a chain of ifs on purpose. An `if (status === ...)` ladder
|
|
7
|
+
* silently falls through when the server introduces a status the client has not
|
|
8
|
+
* seen — and "falls through" in a sign-in flow means a blank screen with no
|
|
9
|
+
* error, which is the single worst failure mode a login box has.
|
|
10
|
+
*
|
|
11
|
+
* Here an unknown status maps to an explicit `unknown` step that a UI can render
|
|
12
|
+
* as "this needs an update", which is honest and actionable.
|
|
13
|
+
*/
|
|
14
|
+
export type AttemptStatus = 'needs_identifier' | 'needs_first_factor' | 'needs_second_factor'
|
|
15
|
+
/** §11.1 MFA policy `required`, for a user who has no second factor yet. */
|
|
16
|
+
| 'needs_mfa_enrollment' | 'needs_email_verification' | 'needs_oauth_callback' | 'needs_new_password'
|
|
17
|
+
/** §5 the server is asking the identifier step to clear a captcha challenge. */
|
|
18
|
+
| 'needs_captcha' | 'complete' | 'abandoned';
|
|
19
|
+
export type Step = {
|
|
20
|
+
kind: 'collect_identifier';
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'collect_first_factor';
|
|
23
|
+
strategies: string[];
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'collect_second_factor';
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'enroll_second_factor';
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'collect_email_code';
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'collect_captcha';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'await_oauth';
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'collect_new_password';
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'done';
|
|
38
|
+
sessionId: string | null;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'restart';
|
|
41
|
+
reason: 'abandoned';
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'unknown';
|
|
44
|
+
status: string;
|
|
45
|
+
};
|
|
46
|
+
export interface AttemptView {
|
|
47
|
+
status: string;
|
|
48
|
+
supported_first_factors?: string[];
|
|
49
|
+
created_session_id?: string | null;
|
|
50
|
+
}
|
|
51
|
+
export declare function nextStep(attempt: AttemptView): Step;
|
|
52
|
+
/** Whether the flow can still progress, for deciding whether to keep polling. */
|
|
53
|
+
export declare function isTerminal(attempt: AttemptView): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* §9.1 error envelope, reduced to what a form needs.
|
|
56
|
+
*
|
|
57
|
+
* The API writes messages for humans, so they are surfaced verbatim. The `param`
|
|
58
|
+
* is what lets a form attach the message to the right field instead of dumping
|
|
59
|
+
* everything in a banner at the top, which is how "password too short" ends up
|
|
60
|
+
* appearing next to the email input.
|
|
61
|
+
*/
|
|
62
|
+
export interface FieldError {
|
|
63
|
+
code: string;
|
|
64
|
+
message: string;
|
|
65
|
+
param?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function fieldErrors(body: unknown): FieldError[];
|
|
68
|
+
/** Errors for one field, so a form can render them inline. */
|
|
69
|
+
export declare function errorsFor(errors: readonly FieldError[], param: string): FieldError[];
|
|
70
|
+
/** Errors with no field, which belong in a form-level banner. */
|
|
71
|
+
export declare function formErrors(errors: readonly FieldError[]): FieldError[];
|
package/dist/attempt.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* §5: "the client reads `status` and renders whatever the server demands next;
|
|
4
|
+
* it never picks the step itself."
|
|
5
|
+
*
|
|
6
|
+
* This is the client half of that contract, and it is written as an exhaustive
|
|
7
|
+
* mapping rather than a chain of ifs on purpose. An `if (status === ...)` ladder
|
|
8
|
+
* silently falls through when the server introduces a status the client has not
|
|
9
|
+
* seen — and "falls through" in a sign-in flow means a blank screen with no
|
|
10
|
+
* error, which is the single worst failure mode a login box has.
|
|
11
|
+
*
|
|
12
|
+
* Here an unknown status maps to an explicit `unknown` step that a UI can render
|
|
13
|
+
* as "this needs an update", which is honest and actionable.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.nextStep = nextStep;
|
|
17
|
+
exports.isTerminal = isTerminal;
|
|
18
|
+
exports.fieldErrors = fieldErrors;
|
|
19
|
+
exports.errorsFor = errorsFor;
|
|
20
|
+
exports.formErrors = formErrors;
|
|
21
|
+
function nextStep(attempt) {
|
|
22
|
+
switch (attempt.status) {
|
|
23
|
+
case 'needs_identifier':
|
|
24
|
+
return { kind: 'collect_identifier' };
|
|
25
|
+
case 'needs_first_factor':
|
|
26
|
+
return {
|
|
27
|
+
kind: 'collect_first_factor',
|
|
28
|
+
// The server's list, never a client-side guess. §13.2 makes this list
|
|
29
|
+
// identical for unknown identifiers, so a client that filtered it by
|
|
30
|
+
// "what this user probably has" would reintroduce the enumeration leak
|
|
31
|
+
// the uniform response exists to close.
|
|
32
|
+
strategies: attempt.supported_first_factors ?? [],
|
|
33
|
+
};
|
|
34
|
+
case 'needs_second_factor':
|
|
35
|
+
return { kind: 'collect_second_factor' };
|
|
36
|
+
/**
|
|
37
|
+
* Distinct from `collect_second_factor`, and the difference matters: this
|
|
38
|
+
* user has NO factor to be asked for. Rendering a "enter your code" box
|
|
39
|
+
* here would ask for something that does not exist, and the person would
|
|
40
|
+
* have no way past it.
|
|
41
|
+
*/
|
|
42
|
+
case 'needs_mfa_enrollment':
|
|
43
|
+
return { kind: 'enroll_second_factor' };
|
|
44
|
+
case 'needs_email_verification':
|
|
45
|
+
return { kind: 'collect_email_code' };
|
|
46
|
+
case 'needs_captcha':
|
|
47
|
+
return { kind: 'collect_captcha' };
|
|
48
|
+
case 'needs_oauth_callback':
|
|
49
|
+
return { kind: 'await_oauth' };
|
|
50
|
+
case 'needs_new_password':
|
|
51
|
+
return { kind: 'collect_new_password' };
|
|
52
|
+
case 'complete':
|
|
53
|
+
return { kind: 'done', sessionId: attempt.created_session_id ?? null };
|
|
54
|
+
case 'abandoned':
|
|
55
|
+
return { kind: 'restart', reason: 'abandoned' };
|
|
56
|
+
default:
|
|
57
|
+
/**
|
|
58
|
+
* A status this SDK version does not know. Rendering nothing would be a
|
|
59
|
+
* blank login box with no error; saying so lets the app show something a
|
|
60
|
+
* user can act on.
|
|
61
|
+
*/
|
|
62
|
+
return { kind: 'unknown', status: attempt.status };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Whether the flow can still progress, for deciding whether to keep polling. */
|
|
66
|
+
function isTerminal(attempt) {
|
|
67
|
+
return attempt.status === 'complete' || attempt.status === 'abandoned';
|
|
68
|
+
}
|
|
69
|
+
function fieldErrors(body) {
|
|
70
|
+
if (!body || typeof body !== 'object')
|
|
71
|
+
return [];
|
|
72
|
+
const errors = body.errors;
|
|
73
|
+
if (!Array.isArray(errors))
|
|
74
|
+
return [];
|
|
75
|
+
return errors
|
|
76
|
+
.filter((error) => {
|
|
77
|
+
return Boolean(error) && typeof error.message === 'string';
|
|
78
|
+
})
|
|
79
|
+
.map((error) => ({ code: error.code, message: error.message, param: error.param }));
|
|
80
|
+
}
|
|
81
|
+
/** Errors for one field, so a form can render them inline. */
|
|
82
|
+
function errorsFor(errors, param) {
|
|
83
|
+
return errors.filter((error) => error.param === param);
|
|
84
|
+
}
|
|
85
|
+
/** Errors with no field, which belong in a form-level banner. */
|
|
86
|
+
function formErrors(errors) {
|
|
87
|
+
return errors.filter((error) => !error.param);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=attempt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attempt.js","sourceRoot":"","sources":["../src/attempt.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAmCH,4BAqDC;AAGD,gCAEC;AAgBD,kCAUC;AAGD,8BAEC;AAGD,gCAEC;AA9FD,SAAgB,QAAQ,CAAC,OAAoB;IAC3C,QAAQ,OAAO,CAAC,MAAuB,EAAE,CAAC;QACxC,KAAK,kBAAkB;YACrB,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC;QAExC,KAAK,oBAAoB;YACvB,OAAO;gBACL,IAAI,EAAE,sBAAsB;gBAC5B,sEAAsE;gBACtE,qEAAqE;gBACrE,uEAAuE;gBACvE,wCAAwC;gBACxC,UAAU,EAAE,OAAO,CAAC,uBAAuB,IAAI,EAAE;aAClD,CAAC;QAEJ,KAAK,qBAAqB;YACxB,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;QAE3C;;;;;WAKG;QACH,KAAK,sBAAsB;YACzB,OAAO,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC;QAE1C,KAAK,0BAA0B;YAC7B,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC;QAExC,KAAK,eAAe;YAClB,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;QAErC,KAAK,sBAAsB;YACzB,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QAEjC,KAAK,oBAAoB;YACvB,OAAO,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC;QAE1C,KAAK,UAAU;YACb,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,kBAAkB,IAAI,IAAI,EAAE,CAAC;QAEzE,KAAK,WAAW;YACd,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAElD;YACE;;;;eAIG;YACH,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;IACvD,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,SAAgB,UAAU,CAAC,OAAoB;IAC7C,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,IAAI,OAAO,CAAC,MAAM,KAAK,WAAW,CAAC;AACzE,CAAC;AAgBD,SAAgB,WAAW,CAAC,IAAa;IACvC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACjD,MAAM,MAAM,GAAI,IAA6B,CAAC,MAAM,CAAC;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,KAAK,EAAuB,EAAE;QACrC,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,OAAQ,KAAoB,CAAC,OAAO,KAAK,QAAQ,CAAC;IAC7E,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACxF,CAAC;AAED,8DAA8D;AAC9D,SAAgB,SAAS,CAAC,MAA6B,EAAE,KAAa;IACpE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;AACzD,CAAC;AAED,iEAAiE;AACjE,SAAgB,UAAU,CAAC,MAA6B;IACtD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silent "is this user already signed in with Atlas?" check for cross-property
|
|
3
|
+
* SSO — the browser-side companion to the server handshake.
|
|
4
|
+
*
|
|
5
|
+
* It runs an OIDC `prompt=none` request in a hidden iframe against the Atlas
|
|
6
|
+
* instance's `/oauth2/authorize`. If a session exists, Atlas redirects the
|
|
7
|
+
* iframe to the registered `redirectUri` with a `code` (the user is signed in);
|
|
8
|
+
* if not, it returns `error=login_required`. The tiny page you host at
|
|
9
|
+
* `redirectUri` calls {@link handleSilentCallback} to postMessage the result
|
|
10
|
+
* back to this opener — the standard OIDC silent-renew pattern.
|
|
11
|
+
*
|
|
12
|
+
* There is no visible navigation. This is a client-side convenience for UI
|
|
13
|
+
* decisions ("show Sign in" vs "show account"); the server (`authenticateRequest`
|
|
14
|
+
* / the handshake middleware) remains the security boundary.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: not yet exercised in an integration test — verify against a live
|
|
17
|
+
* instance before relying on it. See docs/atlas-cross-property-sso-plan.md (P3).
|
|
18
|
+
*/
|
|
19
|
+
export interface CheckSessionOptions {
|
|
20
|
+
/** The Atlas instance origin / OIDC issuer, e.g. `https://id.atlasauth.net`. */
|
|
21
|
+
issuer: string;
|
|
22
|
+
/** A public OIDC client registered for silent checks. */
|
|
23
|
+
clientId: string;
|
|
24
|
+
/** A redirect URI registered on that client whose page calls {@link handleSilentCallback}. */
|
|
25
|
+
redirectUri: string;
|
|
26
|
+
/** Extra scopes beyond `openid`. Default: `['openid']`. */
|
|
27
|
+
scopes?: readonly string[];
|
|
28
|
+
/** Give up and report signed-out after this many ms. Default 8000. */
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
/** A prior id_token to assert a specific user (`id_token_hint`). */
|
|
31
|
+
idTokenHint?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CheckSessionResult {
|
|
34
|
+
signedIn: boolean;
|
|
35
|
+
/** The OIDC error when signed out (`login_required`, `consent_required`, …). */
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run the silent check. Resolves `{ signedIn: true }` when the authorize
|
|
40
|
+
* endpoint returns a code, `{ signedIn: false, error }` on `login_required`
|
|
41
|
+
* (or any error / timeout). Never rejects — a timeout is a signed-out signal.
|
|
42
|
+
*/
|
|
43
|
+
export declare function checkSession(options: CheckSessionOptions): Promise<CheckSessionResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Call this from the page you host at `redirectUri`. It reads the OIDC result
|
|
46
|
+
* from the current URL and postMessages ONLY a signed-in/out verdict to the
|
|
47
|
+
* opener (the {@link checkSession} iframe parent), then the iframe is discarded.
|
|
48
|
+
* Safe to call unconditionally on that page; it no-ops outside an iframe.
|
|
49
|
+
*
|
|
50
|
+
* The single-use authorization `code` is deliberately NOT forwarded: silent auth
|
|
51
|
+
* only needs to know *whether* the user is signed in, and putting a live OAuth
|
|
52
|
+
* code on a postMessage channel is a needless secret exposure (a misconfigured
|
|
53
|
+
* `targetOrigin`, or any other listener, could capture and redeem it). We send a
|
|
54
|
+
* boolean instead; the code stays in this callback frame and is never redeemed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function handleSilentCallback(targetOrigin?: string): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Silent "is this user already signed in with Atlas?" check for cross-property
|
|
4
|
+
* SSO — the browser-side companion to the server handshake.
|
|
5
|
+
*
|
|
6
|
+
* It runs an OIDC `prompt=none` request in a hidden iframe against the Atlas
|
|
7
|
+
* instance's `/oauth2/authorize`. If a session exists, Atlas redirects the
|
|
8
|
+
* iframe to the registered `redirectUri` with a `code` (the user is signed in);
|
|
9
|
+
* if not, it returns `error=login_required`. The tiny page you host at
|
|
10
|
+
* `redirectUri` calls {@link handleSilentCallback} to postMessage the result
|
|
11
|
+
* back to this opener — the standard OIDC silent-renew pattern.
|
|
12
|
+
*
|
|
13
|
+
* There is no visible navigation. This is a client-side convenience for UI
|
|
14
|
+
* decisions ("show Sign in" vs "show account"); the server (`authenticateRequest`
|
|
15
|
+
* / the handshake middleware) remains the security boundary.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: not yet exercised in an integration test — verify against a live
|
|
18
|
+
* instance before relying on it. See docs/atlas-cross-property-sso-plan.md (P3).
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.checkSession = checkSession;
|
|
22
|
+
exports.handleSilentCallback = handleSilentCallback;
|
|
23
|
+
const DEFAULT_TIMEOUT = 8000;
|
|
24
|
+
function randomState() {
|
|
25
|
+
const b = new Uint8Array(16);
|
|
26
|
+
crypto.getRandomValues(b);
|
|
27
|
+
return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run the silent check. Resolves `{ signedIn: true }` when the authorize
|
|
31
|
+
* endpoint returns a code, `{ signedIn: false, error }` on `login_required`
|
|
32
|
+
* (or any error / timeout). Never rejects — a timeout is a signed-out signal.
|
|
33
|
+
*/
|
|
34
|
+
function checkSession(options) {
|
|
35
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
36
|
+
return Promise.resolve({ signedIn: false, error: 'not_in_browser' });
|
|
37
|
+
}
|
|
38
|
+
const state = randomState();
|
|
39
|
+
const authUrl = new URL('/oauth2/authorize', options.issuer);
|
|
40
|
+
authUrl.searchParams.set('client_id', options.clientId);
|
|
41
|
+
authUrl.searchParams.set('redirect_uri', options.redirectUri);
|
|
42
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
43
|
+
authUrl.searchParams.set('scope', (options.scopes ?? ['openid']).join(' '));
|
|
44
|
+
authUrl.searchParams.set('prompt', 'none');
|
|
45
|
+
authUrl.searchParams.set('state', state);
|
|
46
|
+
if (options.idTokenHint)
|
|
47
|
+
authUrl.searchParams.set('id_token_hint', options.idTokenHint);
|
|
48
|
+
const expectedOrigin = new URL(options.redirectUri).origin;
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const iframe = document.createElement('iframe');
|
|
51
|
+
iframe.style.display = 'none';
|
|
52
|
+
let done = false;
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
window.removeEventListener('message', onMessage);
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
iframe.remove();
|
|
57
|
+
};
|
|
58
|
+
const finish = (result) => {
|
|
59
|
+
if (done)
|
|
60
|
+
return;
|
|
61
|
+
done = true;
|
|
62
|
+
cleanup();
|
|
63
|
+
resolve(result);
|
|
64
|
+
};
|
|
65
|
+
const onMessage = (event) => {
|
|
66
|
+
// Only trust a message from our own callback origin carrying our state.
|
|
67
|
+
if (event.origin !== expectedOrigin)
|
|
68
|
+
return;
|
|
69
|
+
const data = event.data;
|
|
70
|
+
if (!data || data.source !== 'atlas:silent-auth' || data.state !== state)
|
|
71
|
+
return;
|
|
72
|
+
// Trust only the boolean the callback computed — the raw OAuth code never
|
|
73
|
+
// travels over this channel, so there is nothing here to trust or leak.
|
|
74
|
+
finish({ signedIn: data.signedIn === true && !data.error, error: data.error });
|
|
75
|
+
};
|
|
76
|
+
const timer = setTimeout(() => finish({ signedIn: false, error: 'timeout' }), options.timeoutMs ?? DEFAULT_TIMEOUT);
|
|
77
|
+
window.addEventListener('message', onMessage);
|
|
78
|
+
iframe.src = authUrl.toString();
|
|
79
|
+
document.body.appendChild(iframe);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Call this from the page you host at `redirectUri`. It reads the OIDC result
|
|
84
|
+
* from the current URL and postMessages ONLY a signed-in/out verdict to the
|
|
85
|
+
* opener (the {@link checkSession} iframe parent), then the iframe is discarded.
|
|
86
|
+
* Safe to call unconditionally on that page; it no-ops outside an iframe.
|
|
87
|
+
*
|
|
88
|
+
* The single-use authorization `code` is deliberately NOT forwarded: silent auth
|
|
89
|
+
* only needs to know *whether* the user is signed in, and putting a live OAuth
|
|
90
|
+
* code on a postMessage channel is a needless secret exposure (a misconfigured
|
|
91
|
+
* `targetOrigin`, or any other listener, could capture and redeem it). We send a
|
|
92
|
+
* boolean instead; the code stays in this callback frame and is never redeemed.
|
|
93
|
+
*/
|
|
94
|
+
function handleSilentCallback(targetOrigin) {
|
|
95
|
+
if (typeof window === 'undefined' || window.parent === window)
|
|
96
|
+
return;
|
|
97
|
+
const params = new URLSearchParams(window.location.search);
|
|
98
|
+
const error = params.get('error') ?? undefined;
|
|
99
|
+
window.parent.postMessage({
|
|
100
|
+
source: 'atlas:silent-auth',
|
|
101
|
+
state: params.get('state') ?? undefined,
|
|
102
|
+
signedIn: Boolean(params.get('code')) && !error,
|
|
103
|
+
error,
|
|
104
|
+
}, targetOrigin ?? window.location.origin);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=check-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-session.js","sourceRoot":"","sources":["../src/check-session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §6.6 Connect a NEW OAuth provider to the signed-in user.
|
|
3
|
+
*
|
|
4
|
+
* This is the SDK call behind a customer's OWN "Connect GitHub" button in their
|
|
5
|
+
* settings page — no Atlas widget, no panel. It starts the provider OAuth link
|
|
6
|
+
* flow and resolves with the `authorizationUrl` to send the browser to. On the
|
|
7
|
+
* customer's `redirectUrl` the provider returns with `?__atlas_status=connected`
|
|
8
|
+
* (or `error`), which {@link readRedirectResult} parses.
|
|
9
|
+
*
|
|
10
|
+
* The link is bound to the CURRENT session's user, so — unlike a sign-in — it
|
|
11
|
+
* never creates a new account or session, never resolves an identity by email,
|
|
12
|
+
* and is unaffected by the account-linking `block` policy (the user is already
|
|
13
|
+
* authenticated and explicitly linking). Requires the browser session cookie, so
|
|
14
|
+
* the request is sent with credentials.
|
|
15
|
+
*/
|
|
16
|
+
export interface ConnectOptions {
|
|
17
|
+
/** FAPI base, e.g. 'https://accounts.acme.com' or '' for same-origin. */
|
|
18
|
+
api: string;
|
|
19
|
+
publishableKey: string;
|
|
20
|
+
/**
|
|
21
|
+
* Where the provider returns after the user authorizes — must be one of the
|
|
22
|
+
* instance's allowed origins. Read the outcome there with `readRedirectResult`.
|
|
23
|
+
*/
|
|
24
|
+
redirectUrl: string;
|
|
25
|
+
/** Extra provider scopes on top of the provider's defaults. */
|
|
26
|
+
additionalScopes?: string[];
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
export interface ConnectStart {
|
|
30
|
+
provider: string;
|
|
31
|
+
attemptId: string;
|
|
32
|
+
/** Navigate the browser here to let the user authorize the provider. */
|
|
33
|
+
authorizationUrl: string;
|
|
34
|
+
/** The scopes the connect will request (provider defaults ∪ additionalScopes). */
|
|
35
|
+
scopes: string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Begin linking `provider` to the signed-in user. Resolves with the provider
|
|
39
|
+
* authorize URL to navigate to; throws with the server's message (and a `.code`,
|
|
40
|
+
* e.g. `IDENTITY_ALREADY_LINKED`) on failure. Use {@link startConnect} if you
|
|
41
|
+
* just want it to redirect for you.
|
|
42
|
+
*/
|
|
43
|
+
export declare function connectExternalAccount(provider: string, opts: ConnectOptions): Promise<ConnectStart>;
|
|
44
|
+
/**
|
|
45
|
+
* Convenience wrapper: start the connection AND navigate the browser to the
|
|
46
|
+
* provider. A settings-page button handler can be just:
|
|
47
|
+
*
|
|
48
|
+
* onClick={() => startConnect('github', { api, publishableKey, redirectUrl })}
|
|
49
|
+
*/
|
|
50
|
+
export declare function startConnect(provider: string, opts: ConnectOptions): Promise<void>;
|
package/dist/connect.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* §6.6 Connect a NEW OAuth provider to the signed-in user.
|
|
4
|
+
*
|
|
5
|
+
* This is the SDK call behind a customer's OWN "Connect GitHub" button in their
|
|
6
|
+
* settings page — no Atlas widget, no panel. It starts the provider OAuth link
|
|
7
|
+
* flow and resolves with the `authorizationUrl` to send the browser to. On the
|
|
8
|
+
* customer's `redirectUrl` the provider returns with `?__atlas_status=connected`
|
|
9
|
+
* (or `error`), which {@link readRedirectResult} parses.
|
|
10
|
+
*
|
|
11
|
+
* The link is bound to the CURRENT session's user, so — unlike a sign-in — it
|
|
12
|
+
* never creates a new account or session, never resolves an identity by email,
|
|
13
|
+
* and is unaffected by the account-linking `block` policy (the user is already
|
|
14
|
+
* authenticated and explicitly linking). Requires the browser session cookie, so
|
|
15
|
+
* the request is sent with credentials.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.connectExternalAccount = connectExternalAccount;
|
|
19
|
+
exports.startConnect = startConnect;
|
|
20
|
+
/**
|
|
21
|
+
* Begin linking `provider` to the signed-in user. Resolves with the provider
|
|
22
|
+
* authorize URL to navigate to; throws with the server's message (and a `.code`,
|
|
23
|
+
* e.g. `IDENTITY_ALREADY_LINKED`) on failure. Use {@link startConnect} if you
|
|
24
|
+
* just want it to redirect for you.
|
|
25
|
+
*/
|
|
26
|
+
async function connectExternalAccount(provider, opts) {
|
|
27
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
28
|
+
const res = await doFetch(`${opts.api}/v1/client/me/external_accounts/connect`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'content-type': 'application/json', 'x-publishable-key': opts.publishableKey },
|
|
31
|
+
credentials: 'include',
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
provider,
|
|
34
|
+
redirect_url: opts.redirectUrl,
|
|
35
|
+
additional_scopes: opts.additionalScopes,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
let message = 'Could not start connecting that provider.';
|
|
40
|
+
let code;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = (await res.json());
|
|
43
|
+
message = parsed.errors?.[0]?.message ?? message;
|
|
44
|
+
code = parsed.errors?.[0]?.code;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* keep the default */
|
|
48
|
+
}
|
|
49
|
+
const err = new Error(message);
|
|
50
|
+
err.code = code;
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
return {
|
|
55
|
+
provider: data.provider,
|
|
56
|
+
attemptId: data.attempt_id,
|
|
57
|
+
authorizationUrl: data.authorization_url,
|
|
58
|
+
scopes: data.scopes ?? [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Convenience wrapper: start the connection AND navigate the browser to the
|
|
63
|
+
* provider. A settings-page button handler can be just:
|
|
64
|
+
*
|
|
65
|
+
* onClick={() => startConnect('github', { api, publishableKey, redirectUrl })}
|
|
66
|
+
*/
|
|
67
|
+
async function startConnect(provider, opts) {
|
|
68
|
+
const { authorizationUrl } = await connectExternalAccount(provider, opts);
|
|
69
|
+
if (typeof window !== 'undefined')
|
|
70
|
+
window.location.assign(authorizationUrl);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AA+BH,wDA2CC;AAQD,oCAGC;AA5DD;;;;;GAKG;AACI,KAAK,UAAU,sBAAsB,CAC1C,QAAgB,EAChB,IAAoB;IAEpB,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IACxC,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,yCAAyC,EAAE;QAC9E,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,IAAI,CAAC,cAAc,EAAE;QACzF,WAAW,EAAE,SAAS;QACtB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,QAAQ;YACR,YAAY,EAAE,IAAI,CAAC,WAAW;YAC9B,iBAAiB,EAAE,IAAI,CAAC,gBAAgB;SACzC,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,IAAI,OAAO,GAAG,2CAA2C,CAAC;QAC1D,IAAI,IAAwB,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAuD,CAAC;YACxF,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,OAAO,CAAC;YACjD,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,GAAyB,CAAC,IAAI,GAAG,IAAI,CAAC;QACvC,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAK7B,CAAC;IACF,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS,EAAE,IAAI,CAAC,UAAU;QAC1B,gBAAgB,EAAE,IAAI,CAAC,iBAAiB;QACxC,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;KAC1B,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,YAAY,CAAC,QAAgB,EAAE,IAAoB;IACvE,MAAM,EAAE,gBAAgB,EAAE,GAAG,MAAM,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC1E,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAC9E,CAAC"}
|
package/dist/fapi.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { type AttemptView, type FieldError } from './attempt';
|
|
2
|
+
/**
|
|
3
|
+
* The FAPI client and the sign-in flow driver.
|
|
4
|
+
*
|
|
5
|
+
* The driver's whole job is to answer one question — "given the step the server
|
|
6
|
+
* says we are on, which endpoint does this input go to?" — and to answer it in
|
|
7
|
+
* ONE place. Scattering that decision across a component is how a password ends
|
|
8
|
+
* up posted to the second-factor endpoint, or a TOTP code to
|
|
9
|
+
* `attempt_first_factor`: both produce a confusing 400 rather than an obvious
|
|
10
|
+
* bug, and both are easy to write.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here ever advances the flow locally. The status always comes back
|
|
13
|
+
* from the server (§5), so a client that guesses wrong is corrected on the next
|
|
14
|
+
* response rather than diverging silently.
|
|
15
|
+
*/
|
|
16
|
+
export interface FapiClientOptions {
|
|
17
|
+
publishableKey: string;
|
|
18
|
+
/** The instance's FAPI origin. Empty for same-origin. */
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
export interface FapiResponse<T> {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
status: number;
|
|
25
|
+
data: T | null;
|
|
26
|
+
errors: FieldError[];
|
|
27
|
+
}
|
|
28
|
+
export declare class FapiClient {
|
|
29
|
+
private readonly options;
|
|
30
|
+
constructor(options: FapiClientOptions);
|
|
31
|
+
request<T>(path: string, init?: RequestInit): Promise<FapiResponse<T>>;
|
|
32
|
+
post<T>(path: string, body?: unknown): Promise<FapiResponse<T>>;
|
|
33
|
+
get<T>(path: string): Promise<FapiResponse<T>>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* §6.6 client-side token vault. A signed-in user's OWN live provider access
|
|
37
|
+
* token, so the app can call e.g. the Google API from the browser — the
|
|
38
|
+
* `user.getToken({ provider })` equivalent.
|
|
39
|
+
*
|
|
40
|
+
* The server keys the lookup off the session, never a user id, so this can only
|
|
41
|
+
* ever return the caller's own token. The long-lived refresh token is never
|
|
42
|
+
* exposed — only the short-lived access token it mints.
|
|
43
|
+
*/
|
|
44
|
+
export interface ProviderToken {
|
|
45
|
+
provider: string;
|
|
46
|
+
accessToken: string;
|
|
47
|
+
/** Epoch ms, or null when the provider issues non-expiring tokens. */
|
|
48
|
+
expiresAt: number | null;
|
|
49
|
+
scopes: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fetch the signed-in user's provider access token. Returns null when there is
|
|
53
|
+
* no usable token (no linked account, or the provider granted none / revoked
|
|
54
|
+
* it) — the caller's cue to prompt a re-connect rather than treat it as fatal.
|
|
55
|
+
* The server refreshes a stale token on read, single-flight, so the token
|
|
56
|
+
* handed back is always live.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getProviderToken(client: FapiClient, provider: string): Promise<ProviderToken | null>;
|
|
59
|
+
export interface FlowState {
|
|
60
|
+
attempt: AttemptView | null;
|
|
61
|
+
errors: FieldError[];
|
|
62
|
+
busy: boolean;
|
|
63
|
+
/** Held only for the magic-link path; see §5.3. */
|
|
64
|
+
pollSecret: string | null;
|
|
65
|
+
/**
|
|
66
|
+
* §7.1: the one-time ticket a completion response carries. Present for exactly
|
|
67
|
+
* one step — the one that reached `complete` — so the caller can exchange it
|
|
68
|
+
* for cookies and then it is gone.
|
|
69
|
+
*/
|
|
70
|
+
ticket: string | null;
|
|
71
|
+
}
|
|
72
|
+
export declare const initialFlow: FlowState;
|
|
73
|
+
/**
|
|
74
|
+
* Which request the current step needs.
|
|
75
|
+
*
|
|
76
|
+
* Exported and tested separately from the network call, because this mapping is
|
|
77
|
+
* the part that can be wrong in a way nobody notices until a user is stuck.
|
|
78
|
+
*/
|
|
79
|
+
export declare function requestForStep(attempt: AttemptView | null, values: Record<string, string>): {
|
|
80
|
+
path: string;
|
|
81
|
+
body: Record<string, unknown>;
|
|
82
|
+
} | null;
|
|
83
|
+
/**
|
|
84
|
+
* §5.3: seed a flow from an attempt the app was handed mid-stream — an OAuth
|
|
85
|
+
* redirect that came back needing a second factor (`__atlas_status`), with no
|
|
86
|
+
* ticket because the server withheld the session until the factor is passed.
|
|
87
|
+
*
|
|
88
|
+
* The attempt id lets the next submit target the right attempt; the status
|
|
89
|
+
* routes the UI to the step the server is demanding. A null pending attempt —
|
|
90
|
+
* the ordinary fresh sign-in — yields the empty initial flow.
|
|
91
|
+
*/
|
|
92
|
+
export declare function flowFromPending(pending: {
|
|
93
|
+
id: string;
|
|
94
|
+
status: string;
|
|
95
|
+
} | null): FlowState;
|
|
96
|
+
/** Advance the flow by one step. Never decides the next status itself. */
|
|
97
|
+
export declare function advance(client: FapiClient, state: FlowState, values: Record<string, string>): Promise<FlowState>;
|
|
98
|
+
/**
|
|
99
|
+
* §5 Begin a password sign-up. POSTs email + password + the tenant's configured
|
|
100
|
+
* extra fields (name, company, phone, custom…) to `/v1/client/sign_ups` and
|
|
101
|
+
* returns a flow. Usually `needs_email_verification` — the shared `advance` then
|
|
102
|
+
* drives the emailed code to completion — or, when the instance doesn't gate on
|
|
103
|
+
* verification, a `complete` attempt carrying the session ticket.
|
|
104
|
+
*/
|
|
105
|
+
export declare function startSignUp(client: FapiClient, input: {
|
|
106
|
+
email: string;
|
|
107
|
+
password: string;
|
|
108
|
+
fields?: Record<string, string>;
|
|
109
|
+
consent?: boolean;
|
|
110
|
+
captchaToken?: string;
|
|
111
|
+
}): Promise<FlowState>;
|
|
112
|
+
/** §5.3: ask the server to send a code or a magic link. */
|
|
113
|
+
export declare function prepareFactor(client: FapiClient, state: FlowState, strategy: 'email_code' | 'email_link'): Promise<FlowState>;
|
|
114
|
+
/**
|
|
115
|
+
* §5.3 polling. Returns the updated flow, and a ticket once the link has been
|
|
116
|
+
* opened somewhere.
|
|
117
|
+
*/
|
|
118
|
+
export declare function pollAttempt(client: FapiClient, state: FlowState): Promise<{
|
|
119
|
+
state: FlowState;
|
|
120
|
+
ticket: string | null;
|
|
121
|
+
}>;
|
|
122
|
+
/** Whether polling should continue, so a tab does not poll a dead attempt forever. */
|
|
123
|
+
export declare function shouldKeepPolling(state: FlowState): boolean;
|