@wtfalch/auth 0.32.0 → 0.34.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 +88 -5
- package/dist/auth.d.ts +72 -0
- package/dist/auth.js +200 -37
- package/dist/body.d.ts +5 -0
- package/dist/body.js +35 -0
- package/dist/broker.d.ts +24 -1
- package/dist/broker.js +23 -2
- package/dist/cookies.d.ts +7 -0
- package/dist/cookies.js +9 -4
- package/dist/desktop-crypto.d.ts +26 -0
- package/dist/desktop-crypto.js +62 -0
- package/dist/desktop-store-contract.d.ts +13 -0
- package/dist/desktop-store-contract.js +106 -0
- package/dist/desktop-store-postgres.d.ts +40 -0
- package/dist/desktop-store-postgres.js +133 -0
- package/dist/desktop-store.d.ts +130 -0
- package/dist/desktop-store.js +111 -0
- package/dist/desktop.d.ts +11 -76
- package/dist/desktop.js +156 -286
- package/dist/fresh.d.ts +14 -0
- package/dist/fresh.js +21 -0
- package/dist/next.d.ts +74 -1
- package/dist/next.js +91 -0
- package/dist/preview.d.ts +11 -7
- package/dist/preview.js +20 -24
- package/dist/redirect.d.ts +8 -0
- package/dist/redirect.js +8 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.js +2 -0
- package/dist/workspaces.d.ts +79 -0
- package/dist/workspaces.js +137 -0
- package/package.json +12 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The handoff's row store: the interface a store implements over a table, and a
|
|
3
|
+
* memory implementation for one process. `createPostgresDesktopHandoffStore`
|
|
4
|
+
* (desktop-store-postgres.ts) is the shared Postgres one.
|
|
5
|
+
* `checkDesktopHandoffStore` (desktop-store-contract.ts) runs the contract below
|
|
6
|
+
* against any store.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* One process only. For tests and a single-instance development server: two
|
|
10
|
+
* replicas with separate memory stores do not share rows.
|
|
11
|
+
*/
|
|
12
|
+
export function createMemoryDesktopHandoffStore(options) {
|
|
13
|
+
const capacity = options?.capacity ?? 128;
|
|
14
|
+
const ttlMs = options?.ttlMs ?? 5 * 60 * 1000;
|
|
15
|
+
const pollGapMs = options?.pollGapMs ?? 1000;
|
|
16
|
+
const rows = new Map();
|
|
17
|
+
/** Per row id, the end of the line of transactions waiting for it. */
|
|
18
|
+
const lineEnds = new Map();
|
|
19
|
+
async function acquire(tx, id) {
|
|
20
|
+
if (tx.held.has(id))
|
|
21
|
+
return;
|
|
22
|
+
const before = lineEnds.get(id) ?? Promise.resolve();
|
|
23
|
+
let release;
|
|
24
|
+
const turn = new Promise((resolve) => {
|
|
25
|
+
release = resolve;
|
|
26
|
+
});
|
|
27
|
+
const end = before.then(() => turn);
|
|
28
|
+
lineEnds.set(id, end);
|
|
29
|
+
await before;
|
|
30
|
+
tx.held.set(id, () => {
|
|
31
|
+
if (lineEnds.get(id) === end)
|
|
32
|
+
lineEnds.delete(id);
|
|
33
|
+
release();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const sweep = () => {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
for (const [id, entry] of rows)
|
|
39
|
+
if (entry.expiresAt <= now)
|
|
40
|
+
rows.delete(id);
|
|
41
|
+
};
|
|
42
|
+
const found = (entry) => {
|
|
43
|
+
const { expiresAt, lastPoll: _poll, callerHash: _caller, ...row } = entry;
|
|
44
|
+
return { row, live: expiresAt > Date.now() };
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
async admit(row, limits) {
|
|
48
|
+
sweep();
|
|
49
|
+
if (rows.size >= capacity)
|
|
50
|
+
return false;
|
|
51
|
+
let mine = 0;
|
|
52
|
+
for (const entry of rows.values())
|
|
53
|
+
if (entry.callerHash === row.callerHash)
|
|
54
|
+
mine++;
|
|
55
|
+
if (mine >= limits.perCaller)
|
|
56
|
+
return false;
|
|
57
|
+
rows.set(row.id, {
|
|
58
|
+
id: row.id,
|
|
59
|
+
status: 'started',
|
|
60
|
+
challenge: row.challenge,
|
|
61
|
+
browserHash: row.browserHash,
|
|
62
|
+
stateHash: row.stateHash,
|
|
63
|
+
payload: null,
|
|
64
|
+
subject: null,
|
|
65
|
+
email: null,
|
|
66
|
+
revision: null,
|
|
67
|
+
expiresAt: Date.now() + ttlMs,
|
|
68
|
+
lastPoll: null,
|
|
69
|
+
callerHash: row.callerHash,
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
async transaction(fn) {
|
|
74
|
+
const tx = { held: new Map() };
|
|
75
|
+
try {
|
|
76
|
+
return await fn(tx);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
for (const release of tx.held.values())
|
|
80
|
+
release();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
async lock(tx, id) {
|
|
84
|
+
await acquire(tx, id);
|
|
85
|
+
const entry = rows.get(id);
|
|
86
|
+
return entry ? found(entry) : undefined;
|
|
87
|
+
},
|
|
88
|
+
async read(id) {
|
|
89
|
+
const entry = rows.get(id);
|
|
90
|
+
return entry ? found(entry) : undefined;
|
|
91
|
+
},
|
|
92
|
+
async patch(_tx, id, set) {
|
|
93
|
+
const entry = rows.get(id);
|
|
94
|
+
if (entry)
|
|
95
|
+
Object.assign(entry, set);
|
|
96
|
+
},
|
|
97
|
+
async remove(_tx, id) {
|
|
98
|
+
rows.delete(id);
|
|
99
|
+
},
|
|
100
|
+
async stampPoll(_tx, id) {
|
|
101
|
+
const entry = rows.get(id);
|
|
102
|
+
if (!entry)
|
|
103
|
+
return false;
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if (entry.lastPoll !== null && entry.lastPoll >= now - pollGapMs)
|
|
106
|
+
return false;
|
|
107
|
+
entry.lastPoll = now;
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/desktop.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Auth, User } from './auth.js';
|
|
2
|
+
import type { DesktopHandoffStatus, DesktopHandoffStore } from './desktop-store.js';
|
|
3
|
+
export { type DesktopHandoffFound, type DesktopHandoffPatch, type DesktopHandoffRow, type DesktopHandoffStatus, type DesktopHandoffStore, type DesktopHandoffTx, createMemoryDesktopHandoffStore, } from './desktop-store.js';
|
|
4
|
+
export { DESKTOP_HANDOFFS_TABLE_SQL, type DesktopHandoffQuery, type DesktopHandoffSqlClient, type PostgresDesktopHandoffStoreOptions, createPostgresDesktopHandoffStore, } from './desktop-store-postgres.js';
|
|
5
|
+
export { checkDesktopHandoffStore } from './desktop-store-contract.js';
|
|
2
6
|
/**
|
|
3
7
|
* The native sign-in handoff for a desktop or mobile shell, mounted under
|
|
4
8
|
* `/auth/desktop/*`. Merged from Boule's and Archon's own copies
|
|
@@ -29,8 +33,9 @@ import type { Auth, User } from './auth.js';
|
|
|
29
33
|
*
|
|
30
34
|
* The store is the app's: this package never connects to a database it does
|
|
31
35
|
* not own. `createMemoryDesktopHandoffStore` is enough for a single-instance
|
|
32
|
-
* server and for tests; a multi-replica app
|
|
33
|
-
*
|
|
36
|
+
* server and for tests; a multi-replica app passes its Postgres client to
|
|
37
|
+
* `createPostgresDesktopHandoffStore` over the table it migrates
|
|
38
|
+
* (`DESKTOP_HANDOFFS_TABLE_SQL`).
|
|
34
39
|
*/
|
|
35
40
|
export type DesktopHandoffAuth = Pick<Auth, 'sessionCookieName' | 'start' | 'complete' | 'readCookie'>;
|
|
36
41
|
export declare class DesktopHandoffError extends Error {
|
|
@@ -38,75 +43,6 @@ export declare class DesktopHandoffError extends Error {
|
|
|
38
43
|
readonly code: string;
|
|
39
44
|
constructor(status: number, code: string);
|
|
40
45
|
}
|
|
41
|
-
export type DesktopHandoffStatus = 'started' | 'completed' | 'approved';
|
|
42
|
-
export interface DesktopHandoffRow {
|
|
43
|
-
id: string;
|
|
44
|
-
status: DesktopHandoffStatus;
|
|
45
|
-
challenge: string;
|
|
46
|
-
browserHash: string | null;
|
|
47
|
-
stateHash: string | null;
|
|
48
|
-
payload: string | null;
|
|
49
|
-
subject: string | null;
|
|
50
|
-
email: string | null;
|
|
51
|
-
revision: string | null;
|
|
52
|
-
}
|
|
53
|
-
export interface DesktopHandoffFound {
|
|
54
|
-
row: DesktopHandoffRow;
|
|
55
|
-
/** The store's own clock decides this, read after any wait for a lock. */
|
|
56
|
-
live: boolean;
|
|
57
|
-
/** Whether a redeem attempt may be stamped now: at most one a second per row. */
|
|
58
|
-
pollOk: boolean;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* The handoff's row store. The app's own, not this package's: an app
|
|
62
|
-
* migrates one table and implements this interface over it.
|
|
63
|
-
*
|
|
64
|
-
* A real store must make `lock` exclusive: inside `transaction`, `lock` is
|
|
65
|
-
* `SELECT ... FOR UPDATE` on the row, held until the transaction ends. The
|
|
66
|
-
* handler's one-use guarantee (a redeem returns the cookie once, a callback
|
|
67
|
-
* admits once) rests on two concurrent callers on one row taking turns. A store
|
|
68
|
-
* whose `lock` is a plain read lets both pass, and both get the cookie.
|
|
69
|
-
*/
|
|
70
|
-
export interface DesktopHandoffStore<Tx = unknown> {
|
|
71
|
-
/**
|
|
72
|
-
* Prunes expired rows, then inserts a `started` row if there is room: fewer
|
|
73
|
-
* than `perCaller` unexpired rows with the same `callerHash`, and the store's
|
|
74
|
-
* own total below its capacity. False when either limit is reached.
|
|
75
|
-
* `callerHash` is a digest of the caller's address, null when the request
|
|
76
|
-
* carried none; null callers share one bucket.
|
|
77
|
-
*/
|
|
78
|
-
admit(row: {
|
|
79
|
-
id: string;
|
|
80
|
-
challenge: string;
|
|
81
|
-
browserHash: string;
|
|
82
|
-
stateHash: string;
|
|
83
|
-
callerHash: string | null;
|
|
84
|
-
}, limits: {
|
|
85
|
-
perCaller: number;
|
|
86
|
-
}): Promise<boolean>;
|
|
87
|
-
transaction<T>(fn: (tx: Tx) => Promise<T>): Promise<T>;
|
|
88
|
-
/** The row, locked until the transaction ends (`SELECT ... FOR UPDATE`). A second caller waits for the first to finish. */
|
|
89
|
-
lock(tx: Tx, id: string): Promise<DesktopHandoffFound | undefined>;
|
|
90
|
-
/** The row without a lock, for a page that only shows it. */
|
|
91
|
-
read(id: string): Promise<DesktopHandoffFound | undefined>;
|
|
92
|
-
patch(tx: Tx, id: string, set: Partial<DesktopHandoffRow>): Promise<void>;
|
|
93
|
-
/** Deletes the row: a sign-in that ended holds no payload, email or subject, and frees its slot. */
|
|
94
|
-
remove(tx: Tx, id: string): Promise<void>;
|
|
95
|
-
/** Stamps a redeem attempt, which `pollOk` reads. */
|
|
96
|
-
polled(tx: Tx, id: string): Promise<void>;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* One process only. For tests and a single-instance development server: two
|
|
100
|
-
* replicas with separate memory stores do not share rows.
|
|
101
|
-
*/
|
|
102
|
-
export declare function createMemoryDesktopHandoffStore(options?: {
|
|
103
|
-
/** At most this many unexpired rows exist at once; a start beyond it is refused. Defaults to 128. */
|
|
104
|
-
capacity?: number;
|
|
105
|
-
/** How long a row lives after `admit`. Defaults to 5 minutes. */
|
|
106
|
-
ttlMs?: number;
|
|
107
|
-
/** The minimum gap `pollOk` allows between two redeem attempts on the same row. Defaults to 1 second. */
|
|
108
|
-
pollGapMs?: number;
|
|
109
|
-
}): DesktopHandoffStore;
|
|
110
46
|
/**
|
|
111
47
|
* What the admission hook answers before a session is handed to the shell.
|
|
112
48
|
* Boule's and Archon's copies differed here: Boule handed over whoever the
|
|
@@ -139,7 +75,7 @@ export interface DesktopHandoffCurrent {
|
|
|
139
75
|
*/
|
|
140
76
|
returnUrl: string | null;
|
|
141
77
|
}
|
|
142
|
-
export interface DesktopHandoffConfig
|
|
78
|
+
export interface DesktopHandoffConfig {
|
|
143
79
|
/** The SDK instance the browser side signs in with: a second `createAuth`, its `basePath` moved to `/auth/desktop`, registered at the issuer next to the app's own callback. */
|
|
144
80
|
auth: DesktopHandoffAuth;
|
|
145
81
|
/** The app's own origin. */
|
|
@@ -150,7 +86,7 @@ export interface DesktopHandoffConfig<Tx = unknown> {
|
|
|
150
86
|
appId: string;
|
|
151
87
|
/** The shell's own return scheme (`cookieHandoff({ scheme })` in `@wtfalch/desktop`, or a mobile shell's own); lowercase letters, digits, `+`, `.` and `-`, starting with a letter. The app's config, never the request, decides this. */
|
|
152
88
|
scheme: string;
|
|
153
|
-
store: DesktopHandoffStore
|
|
89
|
+
store: DesktopHandoffStore;
|
|
154
90
|
/**
|
|
155
91
|
* Defaults to `alwaysAdmit`, which admits whoever the issuer signed in,
|
|
156
92
|
* unchanged (Boule's own behaviour). That is fail-open: an app that needs an
|
|
@@ -175,7 +111,7 @@ export interface DesktopHandoffErrorEvent {
|
|
|
175
111
|
reason: string;
|
|
176
112
|
errorName: string;
|
|
177
113
|
}
|
|
178
|
-
export declare function createDesktopHandoff
|
|
114
|
+
export declare function createDesktopHandoff({ auth, appUrl, secret, appId, scheme, store, admit, onError, clientAddressHeader, maxStartsPerCaller, }: DesktopHandoffConfig): {
|
|
179
115
|
/** The cookie name, for the one place a test or a route needs to read it. */
|
|
180
116
|
cookieName: string;
|
|
181
117
|
/** GET /auth/desktop/start */
|
|
@@ -194,6 +130,5 @@ export declare function createDesktopHandoff<Tx = unknown>({ auth, appUrl, secre
|
|
|
194
130
|
cancel: (request: Request) => Promise<Response>;
|
|
195
131
|
/** POST /auth/desktop/redeem, with the body still unread: refuses one that is not small and declared. */
|
|
196
132
|
redeemRequest(request: Request): Promise<Response>;
|
|
197
|
-
|
|
198
|
-
redeem(input: unknown): Promise<Response>;
|
|
133
|
+
redeem: (input: unknown) => Promise<Response>;
|
|
199
134
|
};
|