@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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §6.3 step 3 / §7.1: completing a redirect-based sign-in.
|
|
3
|
+
*
|
|
4
|
+
* OAuth and hosted-page sign-ins finish by redirecting the browser back to the
|
|
5
|
+
* customer's app with params appended to the URL:
|
|
6
|
+
* - `__atlas_attempt` — the sign-in attempt id (always present)
|
|
7
|
+
* - `__atlas_ticket` — a one-time, 60-second ticket to exchange for cookies
|
|
8
|
+
* - `__atlas_status` — the attempt status, present when a step REMAINS
|
|
9
|
+
*
|
|
10
|
+
* A ticket means the sign-in is complete and may be turned into a session. A
|
|
11
|
+
* status WITHOUT a ticket (e.g. `needs_second_factor`) means the server
|
|
12
|
+
* deliberately withheld the session because a factor is still owed — the client
|
|
13
|
+
* must not treat that as signed-in. Parsing this is pure and lives here so both
|
|
14
|
+
* the React provider and any custom integration read the exact same contract.
|
|
15
|
+
*/
|
|
16
|
+
export declare const REDIRECT_PARAMS: readonly ["__atlas_attempt", "__atlas_ticket", "__atlas_status"];
|
|
17
|
+
export interface RedirectResult {
|
|
18
|
+
attemptId: string;
|
|
19
|
+
/** Present only when the sign-in completed — exchange it for cookies. */
|
|
20
|
+
ticket?: string;
|
|
21
|
+
/** Present when a step remains (e.g. `needs_second_factor`); no session yet. */
|
|
22
|
+
status?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read Atlas's redirect params from a query string (`window.location.search`).
|
|
26
|
+
*
|
|
27
|
+
* Returns null when this is not an Atlas redirect: an `__atlas_attempt` on its
|
|
28
|
+
* own — with neither a ticket nor a status — is not actionable and is treated
|
|
29
|
+
* as absent, so a stray param cannot put the SDK into a half-state.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readRedirectResult(search: string): RedirectResult | null;
|
|
32
|
+
/**
|
|
33
|
+
* Return `href` with every Atlas redirect param removed, leaving the customer's
|
|
34
|
+
* own query intact. Called after handling a redirect so a reload — or a link
|
|
35
|
+
* copied from the address bar — cannot replay a spent ticket or resurrect a
|
|
36
|
+
* stale status.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stripRedirectParams(href: string): string;
|
package/dist/redirect.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* §6.3 step 3 / §7.1: completing a redirect-based sign-in.
|
|
4
|
+
*
|
|
5
|
+
* OAuth and hosted-page sign-ins finish by redirecting the browser back to the
|
|
6
|
+
* customer's app with params appended to the URL:
|
|
7
|
+
* - `__atlas_attempt` — the sign-in attempt id (always present)
|
|
8
|
+
* - `__atlas_ticket` — a one-time, 60-second ticket to exchange for cookies
|
|
9
|
+
* - `__atlas_status` — the attempt status, present when a step REMAINS
|
|
10
|
+
*
|
|
11
|
+
* A ticket means the sign-in is complete and may be turned into a session. A
|
|
12
|
+
* status WITHOUT a ticket (e.g. `needs_second_factor`) means the server
|
|
13
|
+
* deliberately withheld the session because a factor is still owed — the client
|
|
14
|
+
* must not treat that as signed-in. Parsing this is pure and lives here so both
|
|
15
|
+
* the React provider and any custom integration read the exact same contract.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.REDIRECT_PARAMS = void 0;
|
|
19
|
+
exports.readRedirectResult = readRedirectResult;
|
|
20
|
+
exports.stripRedirectParams = stripRedirectParams;
|
|
21
|
+
exports.REDIRECT_PARAMS = ['__atlas_attempt', '__atlas_ticket', '__atlas_status'];
|
|
22
|
+
/**
|
|
23
|
+
* Read Atlas's redirect params from a query string (`window.location.search`).
|
|
24
|
+
*
|
|
25
|
+
* Returns null when this is not an Atlas redirect: an `__atlas_attempt` on its
|
|
26
|
+
* own — with neither a ticket nor a status — is not actionable and is treated
|
|
27
|
+
* as absent, so a stray param cannot put the SDK into a half-state.
|
|
28
|
+
*/
|
|
29
|
+
function readRedirectResult(search) {
|
|
30
|
+
const params = new URLSearchParams(search);
|
|
31
|
+
const attemptId = params.get('__atlas_attempt');
|
|
32
|
+
if (!attemptId)
|
|
33
|
+
return null;
|
|
34
|
+
const ticket = params.get('__atlas_ticket');
|
|
35
|
+
const status = params.get('__atlas_status');
|
|
36
|
+
if (!ticket && !status)
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
attemptId,
|
|
40
|
+
...(ticket ? { ticket } : {}),
|
|
41
|
+
...(status ? { status } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Return `href` with every Atlas redirect param removed, leaving the customer's
|
|
46
|
+
* own query intact. Called after handling a redirect so a reload — or a link
|
|
47
|
+
* copied from the address bar — cannot replay a spent ticket or resurrect a
|
|
48
|
+
* stale status.
|
|
49
|
+
*/
|
|
50
|
+
function stripRedirectParams(href) {
|
|
51
|
+
const url = new URL(href);
|
|
52
|
+
for (const key of exports.REDIRECT_PARAMS)
|
|
53
|
+
url.searchParams.delete(key);
|
|
54
|
+
return url.toString();
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=redirect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect.js","sourceRoot":"","sources":["../src/redirect.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAmBH,gDAcC;AAQD,kDAIC;AA3CY,QAAA,eAAe,GAAG,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,CAAU,CAAC;AAUhG;;;;;;GAMG;AACH,SAAgB,kBAAkB,CAAC,MAAc;IAC/C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAChD,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAE5B,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEpC,OAAO;QACL,SAAS;QACT,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7B,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9B,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,IAAY;IAC9C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1B,KAAK,MAAM,GAAG,IAAI,uBAAe;QAAE,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChE,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC"}
|
package/dist/siwe.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §6 Sign-in with Ethereum (SIWE, EIP-4361) — the client half.
|
|
3
|
+
*
|
|
4
|
+
* Two steps, both pure shapers (the app owns fetch):
|
|
5
|
+
* 1. POST `/v1/oauth/siwe/nonce` (no body) and read the nonce with
|
|
6
|
+
* `parseSiweNonceResponse`.
|
|
7
|
+
* 2. Have the wallet sign an EIP-4361 message carrying that nonce, then POST
|
|
8
|
+
* `siweVerifyBody({ message, signature })` to `/v1/oauth/siwe/verify` and
|
|
9
|
+
* read the result with the shared `parseNativeSignInResponse`.
|
|
10
|
+
*/
|
|
11
|
+
export interface SiweVerifyBody {
|
|
12
|
+
message: string;
|
|
13
|
+
signature: string;
|
|
14
|
+
}
|
|
15
|
+
/** Build the POST body for `/v1/oauth/siwe/verify`. */
|
|
16
|
+
export declare function siweVerifyBody(input: {
|
|
17
|
+
message: string;
|
|
18
|
+
signature: string;
|
|
19
|
+
}): SiweVerifyBody;
|
|
20
|
+
/** The single-use nonce returned by `/v1/oauth/siwe/nonce`, or null if malformed. */
|
|
21
|
+
export declare function parseSiweNonceResponse(body: unknown): {
|
|
22
|
+
nonce: string;
|
|
23
|
+
} | null;
|
package/dist/siwe.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* §6 Sign-in with Ethereum (SIWE, EIP-4361) — the client half.
|
|
4
|
+
*
|
|
5
|
+
* Two steps, both pure shapers (the app owns fetch):
|
|
6
|
+
* 1. POST `/v1/oauth/siwe/nonce` (no body) and read the nonce with
|
|
7
|
+
* `parseSiweNonceResponse`.
|
|
8
|
+
* 2. Have the wallet sign an EIP-4361 message carrying that nonce, then POST
|
|
9
|
+
* `siweVerifyBody({ message, signature })` to `/v1/oauth/siwe/verify` and
|
|
10
|
+
* read the result with the shared `parseNativeSignInResponse`.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.siweVerifyBody = siweVerifyBody;
|
|
14
|
+
exports.parseSiweNonceResponse = parseSiweNonceResponse;
|
|
15
|
+
/** Build the POST body for `/v1/oauth/siwe/verify`. */
|
|
16
|
+
function siweVerifyBody(input) {
|
|
17
|
+
return { message: input.message, signature: input.signature };
|
|
18
|
+
}
|
|
19
|
+
/** The single-use nonce returned by `/v1/oauth/siwe/nonce`, or null if malformed. */
|
|
20
|
+
function parseSiweNonceResponse(body) {
|
|
21
|
+
if (!body || typeof body !== 'object')
|
|
22
|
+
return null;
|
|
23
|
+
const b = body;
|
|
24
|
+
if (typeof b.nonce !== 'string' || b.nonce.length === 0)
|
|
25
|
+
return null;
|
|
26
|
+
return { nonce: b.nonce };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=siwe.js.map
|
package/dist/siwe.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"siwe.js","sourceRoot":"","sources":["../src/siwe.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;AAQH,wCAEC;AAGD,wDAKC;AAXD,uDAAuD;AACvD,SAAgB,cAAc,CAAC,KAA6C;IAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;AAChE,CAAC;AAED,qFAAqF;AACrF,SAAgB,sBAAsB,CAAC,IAAa;IAClD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,CAAC,GAAG,IAA2B,CAAC;IACtC,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §7.4: "a BroadcastChannel elects one refreshing tab."
|
|
3
|
+
*
|
|
4
|
+
* Twenty open tabs must produce ONE refresh, not twenty. That is not only a
|
|
5
|
+
* load concern: refresh tokens rotate (§7.4 step 2), so twenty simultaneous
|
|
6
|
+
* refreshes send the same token twenty times, nineteen of them land outside the
|
|
7
|
+
* grace window, and the session is revoked for token reuse. Getting this wrong
|
|
8
|
+
* does not degrade the experience — it signs the user out.
|
|
9
|
+
*
|
|
10
|
+
* The election is deliberately simple. A tab that wants to refresh announces a
|
|
11
|
+
* claim carrying a random id and a timestamp; the lowest id within the claim
|
|
12
|
+
* window wins. No consensus protocol, because the cost of getting it wrong is
|
|
13
|
+
* bounded: two tabs refreshing within the 30-second grace window is exactly the
|
|
14
|
+
* race the grace window exists to absorb.
|
|
15
|
+
*
|
|
16
|
+
* BroadcastChannel is missing in Safari private mode and older browsers, so
|
|
17
|
+
* there is a localStorage fallback. A tab with NEITHER refreshes alone, which
|
|
18
|
+
* is correct — one tab cannot stampede.
|
|
19
|
+
*/
|
|
20
|
+
/** How long a claim stands before it is considered abandoned. */
|
|
21
|
+
export declare const CLAIM_WINDOW_MS = 3000;
|
|
22
|
+
export interface Claim {
|
|
23
|
+
id: string;
|
|
24
|
+
at: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether `mine` wins against the claims seen so far.
|
|
28
|
+
*
|
|
29
|
+
* Lowest id wins, and only claims inside the window count — otherwise a tab
|
|
30
|
+
* that crashed mid-refresh would block every other tab forever.
|
|
31
|
+
*/
|
|
32
|
+
export declare function winsElection(mine: Claim, others: readonly Claim[], now: number): boolean;
|
|
33
|
+
export interface Channel {
|
|
34
|
+
post(message: unknown): void;
|
|
35
|
+
subscribe(handler: (message: unknown) => void): () => void;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
/** BroadcastChannel where available. */
|
|
39
|
+
export declare function broadcastChannel(name: string): Channel | null;
|
|
40
|
+
/**
|
|
41
|
+
* localStorage fallback.
|
|
42
|
+
*
|
|
43
|
+
* The `storage` event fires only in OTHER tabs, which is exactly the semantics
|
|
44
|
+
* needed — a tab does not need to hear its own message. The key is written and
|
|
45
|
+
* immediately removed so the value never accumulates and a stale claim cannot
|
|
46
|
+
* outlive the session that made it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function storageChannel(name: string): Channel | null;
|
|
49
|
+
/**
|
|
50
|
+
* The best channel available, or null.
|
|
51
|
+
*
|
|
52
|
+
* Null is a supported state, not a failure: a single tab with no channel
|
|
53
|
+
* refreshes alone and cannot stampede by definition.
|
|
54
|
+
*/
|
|
55
|
+
export declare function bestChannel(name: string): Channel | null;
|
|
56
|
+
export type RefreshMessage = {
|
|
57
|
+
type: 'claim';
|
|
58
|
+
id: string;
|
|
59
|
+
at: number;
|
|
60
|
+
} | {
|
|
61
|
+
type: 'refreshed';
|
|
62
|
+
jwt: string;
|
|
63
|
+
expiresAt: number;
|
|
64
|
+
sessionId: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'signed_out';
|
|
67
|
+
};
|
|
68
|
+
export declare function isRefreshMessage(value: unknown): value is RefreshMessage;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* §7.4: "a BroadcastChannel elects one refreshing tab."
|
|
4
|
+
*
|
|
5
|
+
* Twenty open tabs must produce ONE refresh, not twenty. That is not only a
|
|
6
|
+
* load concern: refresh tokens rotate (§7.4 step 2), so twenty simultaneous
|
|
7
|
+
* refreshes send the same token twenty times, nineteen of them land outside the
|
|
8
|
+
* grace window, and the session is revoked for token reuse. Getting this wrong
|
|
9
|
+
* does not degrade the experience — it signs the user out.
|
|
10
|
+
*
|
|
11
|
+
* The election is deliberately simple. A tab that wants to refresh announces a
|
|
12
|
+
* claim carrying a random id and a timestamp; the lowest id within the claim
|
|
13
|
+
* window wins. No consensus protocol, because the cost of getting it wrong is
|
|
14
|
+
* bounded: two tabs refreshing within the 30-second grace window is exactly the
|
|
15
|
+
* race the grace window exists to absorb.
|
|
16
|
+
*
|
|
17
|
+
* BroadcastChannel is missing in Safari private mode and older browsers, so
|
|
18
|
+
* there is a localStorage fallback. A tab with NEITHER refreshes alone, which
|
|
19
|
+
* is correct — one tab cannot stampede.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.CLAIM_WINDOW_MS = void 0;
|
|
23
|
+
exports.winsElection = winsElection;
|
|
24
|
+
exports.broadcastChannel = broadcastChannel;
|
|
25
|
+
exports.storageChannel = storageChannel;
|
|
26
|
+
exports.bestChannel = bestChannel;
|
|
27
|
+
exports.isRefreshMessage = isRefreshMessage;
|
|
28
|
+
/** How long a claim stands before it is considered abandoned. */
|
|
29
|
+
exports.CLAIM_WINDOW_MS = 3_000;
|
|
30
|
+
/**
|
|
31
|
+
* Whether `mine` wins against the claims seen so far.
|
|
32
|
+
*
|
|
33
|
+
* Lowest id wins, and only claims inside the window count — otherwise a tab
|
|
34
|
+
* that crashed mid-refresh would block every other tab forever.
|
|
35
|
+
*/
|
|
36
|
+
function winsElection(mine, others, now) {
|
|
37
|
+
const live = others.filter((claim) => now - claim.at < exports.CLAIM_WINDOW_MS);
|
|
38
|
+
return live.every((claim) => mine.id <= claim.id);
|
|
39
|
+
}
|
|
40
|
+
/** BroadcastChannel where available. */
|
|
41
|
+
function broadcastChannel(name) {
|
|
42
|
+
if (typeof BroadcastChannel === 'undefined')
|
|
43
|
+
return null;
|
|
44
|
+
const channel = new BroadcastChannel(name);
|
|
45
|
+
return {
|
|
46
|
+
post: (message) => channel.postMessage(message),
|
|
47
|
+
subscribe(handler) {
|
|
48
|
+
const listener = (event) => handler(event.data);
|
|
49
|
+
channel.addEventListener('message', listener);
|
|
50
|
+
return () => channel.removeEventListener('message', listener);
|
|
51
|
+
},
|
|
52
|
+
close: () => channel.close(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* localStorage fallback.
|
|
57
|
+
*
|
|
58
|
+
* The `storage` event fires only in OTHER tabs, which is exactly the semantics
|
|
59
|
+
* needed — a tab does not need to hear its own message. The key is written and
|
|
60
|
+
* immediately removed so the value never accumulates and a stale claim cannot
|
|
61
|
+
* outlive the session that made it.
|
|
62
|
+
*/
|
|
63
|
+
function storageChannel(name) {
|
|
64
|
+
if (typeof window === 'undefined' || typeof window.localStorage === 'undefined')
|
|
65
|
+
return null;
|
|
66
|
+
const key = `atlas:${name}`;
|
|
67
|
+
return {
|
|
68
|
+
post(message) {
|
|
69
|
+
try {
|
|
70
|
+
window.localStorage.setItem(key, JSON.stringify({ message, at: Date.now() }));
|
|
71
|
+
window.localStorage.removeItem(key);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// A full or disabled localStorage must not break sign-in. Losing
|
|
75
|
+
// cross-tab coordination degrades to "each tab refreshes for itself",
|
|
76
|
+
// which the 30-second grace window absorbs.
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
subscribe(handler) {
|
|
80
|
+
const listener = (event) => {
|
|
81
|
+
if (event.key !== key || !event.newValue)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
handler(JSON.parse(event.newValue).message);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Another script writing to our key is not our problem to crash over.
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener('storage', listener);
|
|
91
|
+
return () => window.removeEventListener('storage', listener);
|
|
92
|
+
},
|
|
93
|
+
close: () => undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The best channel available, or null.
|
|
98
|
+
*
|
|
99
|
+
* Null is a supported state, not a failure: a single tab with no channel
|
|
100
|
+
* refreshes alone and cannot stampede by definition.
|
|
101
|
+
*/
|
|
102
|
+
function bestChannel(name) {
|
|
103
|
+
return broadcastChannel(name) ?? storageChannel(name);
|
|
104
|
+
}
|
|
105
|
+
function isRefreshMessage(value) {
|
|
106
|
+
if (!value || typeof value !== 'object')
|
|
107
|
+
return false;
|
|
108
|
+
const type = value.type;
|
|
109
|
+
return type === 'claim' || type === 'refreshed' || type === 'signed_out';
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=tab-election.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tab-election.js","sourceRoot":"","sources":["../src/tab-election.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §6 Telegram Login Widget — the client half.
|
|
3
|
+
*
|
|
4
|
+
* Like the native / One-Tap helper, the SDK does not own fetch: these are pure
|
|
5
|
+
* request/response shapers. The Telegram Login Widget calls the page back with a
|
|
6
|
+
* signed payload object; the app wraps it with `telegramSignInBody`, POSTs it to
|
|
7
|
+
* `/v1/oauth/telegram`, and reads the result with the shared
|
|
8
|
+
* `parseNativeSignInResponse` (a `ticket` means complete; a bare status means a
|
|
9
|
+
* factor is still owed — exactly the same contract as every other flow).
|
|
10
|
+
*/
|
|
11
|
+
export interface TelegramWidgetUser {
|
|
12
|
+
id: number | string;
|
|
13
|
+
first_name?: string;
|
|
14
|
+
last_name?: string;
|
|
15
|
+
username?: string;
|
|
16
|
+
photo_url?: string;
|
|
17
|
+
auth_date: number | string;
|
|
18
|
+
hash: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface TelegramSignInBody {
|
|
22
|
+
telegram: TelegramWidgetUser;
|
|
23
|
+
}
|
|
24
|
+
/** Build the POST body for `/v1/oauth/telegram` from the widget's user object. */
|
|
25
|
+
export declare function telegramSignInBody(payload: TelegramWidgetUser): TelegramSignInBody;
|
package/dist/telegram.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* §6 Telegram Login Widget — the client half.
|
|
4
|
+
*
|
|
5
|
+
* Like the native / One-Tap helper, the SDK does not own fetch: these are pure
|
|
6
|
+
* request/response shapers. The Telegram Login Widget calls the page back with a
|
|
7
|
+
* signed payload object; the app wraps it with `telegramSignInBody`, POSTs it to
|
|
8
|
+
* `/v1/oauth/telegram`, and reads the result with the shared
|
|
9
|
+
* `parseNativeSignInResponse` (a `ticket` means complete; a bare status means a
|
|
10
|
+
* factor is still owed — exactly the same contract as every other flow).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.telegramSignInBody = telegramSignInBody;
|
|
14
|
+
/** Build the POST body for `/v1/oauth/telegram` from the widget's user object. */
|
|
15
|
+
function telegramSignInBody(payload) {
|
|
16
|
+
return { telegram: payload };
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=telegram.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../src/telegram.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;AAkBH,gDAEC;AAHD,kFAAkF;AAClF,SAAgB,kBAAkB,CAAC,OAA2B;IAC5D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §bot — client-side signal collection for the anti-bot pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Two collectors, both privacy-preserving and both fully best-effort (they never
|
|
5
|
+
* throw, and degrade to nulls in any environment):
|
|
6
|
+
*
|
|
7
|
+
* - `collectDeviceSignals()` — a snapshot of the device/environment: screen,
|
|
8
|
+
* timezone, hardware, a coarse canvas/WebGL fingerprint, and AUTOMATION
|
|
9
|
+
* TELLS (navigator.webdriver, headless markers, Selenium/Puppeteer traces).
|
|
10
|
+
* - `BehaviorRecorder` — accumulates interaction TIMING AGGREGATES on the auth
|
|
11
|
+
* form: mouse kinematics, keystroke dwell/flight, time-to-submit, scrolls.
|
|
12
|
+
* It records COUNTS AND TIMINGS ONLY — never which keys were pressed, never
|
|
13
|
+
* field values. There is nothing sensitive to leak.
|
|
14
|
+
*
|
|
15
|
+
* `sendTelemetry()` posts a snapshot to `/v1/client/telemetry`. It is
|
|
16
|
+
* fire-and-forget: a failure resolves `false` and is swallowed, so telemetry can
|
|
17
|
+
* never interfere with a real sign-in.
|
|
18
|
+
*/
|
|
19
|
+
export interface DeviceSignals {
|
|
20
|
+
screenWidth: number | null;
|
|
21
|
+
screenHeight: number | null;
|
|
22
|
+
colorDepth: number | null;
|
|
23
|
+
pixelRatio: number | null;
|
|
24
|
+
timezone: string | null;
|
|
25
|
+
timezoneOffset: number | null;
|
|
26
|
+
language: string | null;
|
|
27
|
+
languages: string[];
|
|
28
|
+
platform: string | null;
|
|
29
|
+
hardwareConcurrency: number | null;
|
|
30
|
+
deviceMemory: number | null;
|
|
31
|
+
maxTouchPoints: number | null;
|
|
32
|
+
touchSupport: boolean;
|
|
33
|
+
cookieEnabled: boolean | null;
|
|
34
|
+
doNotTrack: boolean | null;
|
|
35
|
+
webglVendor: string | null;
|
|
36
|
+
webglRenderer: string | null;
|
|
37
|
+
pluginCount: number | null;
|
|
38
|
+
pointerType: string | null;
|
|
39
|
+
webdriver: boolean;
|
|
40
|
+
headless: boolean;
|
|
41
|
+
hasChrome: boolean;
|
|
42
|
+
webglSoftware: boolean;
|
|
43
|
+
automationHint: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function collectDeviceSignals(): DeviceSignals;
|
|
46
|
+
/** A stable pseudonymous client id, persisted in localStorage. */
|
|
47
|
+
export declare function deviceId(): string | null;
|
|
48
|
+
/** A short client-side fingerprint over the stable device signals. */
|
|
49
|
+
export declare function deviceFingerprint(device: DeviceSignals): string;
|
|
50
|
+
export interface BehaviorSignals {
|
|
51
|
+
mouseMoves: number;
|
|
52
|
+
mouseDistance: number;
|
|
53
|
+
mouseAvgSpeed: number | null;
|
|
54
|
+
mouseMaxSpeed: number;
|
|
55
|
+
mouseSpeedVariance: number | null;
|
|
56
|
+
mouseStraightLineRatio: number | null;
|
|
57
|
+
keyCount: number;
|
|
58
|
+
keyAvgDwellMs: number | null;
|
|
59
|
+
keyAvgFlightMs: number | null;
|
|
60
|
+
keyDwellVariance: number | null;
|
|
61
|
+
keyFlightVariance: number | null;
|
|
62
|
+
pasteCount: number;
|
|
63
|
+
backspaceCount: number;
|
|
64
|
+
timeToFirstInteractionMs: number | null;
|
|
65
|
+
timeToSubmitMs: number | null;
|
|
66
|
+
focusChanges: number;
|
|
67
|
+
scrollCount: number;
|
|
68
|
+
hadPointerMove: boolean;
|
|
69
|
+
hadKeydown: boolean;
|
|
70
|
+
interactionCount: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Records interaction TIMINGS on the page/form. Start it when the auth form
|
|
74
|
+
* mounts; call `snapshot()` at submit. It listens passively and captures no
|
|
75
|
+
* content — only counts, distances and millisecond timings.
|
|
76
|
+
*/
|
|
77
|
+
export declare class BehaviorRecorder {
|
|
78
|
+
private readonly start;
|
|
79
|
+
private firstInteraction;
|
|
80
|
+
private moves;
|
|
81
|
+
private distance;
|
|
82
|
+
private maxSpeed;
|
|
83
|
+
private speeds;
|
|
84
|
+
private lastX;
|
|
85
|
+
private lastY;
|
|
86
|
+
private lastMoveT;
|
|
87
|
+
private netDx;
|
|
88
|
+
private netDy;
|
|
89
|
+
private keyCount;
|
|
90
|
+
private downAt;
|
|
91
|
+
private dwell;
|
|
92
|
+
private flight;
|
|
93
|
+
private lastUpAt;
|
|
94
|
+
private pastes;
|
|
95
|
+
private backspaces;
|
|
96
|
+
private focusChanges;
|
|
97
|
+
private scrolls;
|
|
98
|
+
private target;
|
|
99
|
+
private bound;
|
|
100
|
+
constructor(target?: Document);
|
|
101
|
+
private mark;
|
|
102
|
+
private attach;
|
|
103
|
+
private onMove;
|
|
104
|
+
private onKeyDown;
|
|
105
|
+
private onKeyUp;
|
|
106
|
+
/** A snapshot of aggregates so far. Safe to call repeatedly. */
|
|
107
|
+
snapshot(): BehaviorSignals;
|
|
108
|
+
/** Detach all listeners. */
|
|
109
|
+
stop(): void;
|
|
110
|
+
}
|
|
111
|
+
export interface TelemetryOptions {
|
|
112
|
+
/** FAPI base, e.g. 'https://accounts.acme.com' or '' for same-origin. */
|
|
113
|
+
api: string;
|
|
114
|
+
publishableKey: string;
|
|
115
|
+
attemptId?: string;
|
|
116
|
+
kind?: 'sign_in' | 'sign_up' | 'telemetry';
|
|
117
|
+
recorder?: BehaviorRecorder;
|
|
118
|
+
fetchImpl?: typeof fetch;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Collect a device snapshot (+ behaviour from `recorder`, if given) and POST it.
|
|
122
|
+
* Resolves `true` when the beacon was accepted, `false` otherwise — never
|
|
123
|
+
* throws. Uses `navigator.sendBeacon` when possible so it survives navigation.
|
|
124
|
+
*/
|
|
125
|
+
export declare function sendTelemetry(opts: TelemetryOptions): Promise<boolean>;
|