@ai-matrx/auth 0.2.0 → 0.2.2
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/CHANGELOG.md +47 -0
- package/dist/active-organization-Dy4UtiyR.d.cts +594 -0
- package/dist/active-organization-Dy4UtiyR.d.ts +594 -0
- package/dist/guest.cjs +213 -0
- package/dist/guest.d.cts +189 -0
- package/dist/guest.d.ts +189 -0
- package/dist/guest.js +192 -0
- package/dist/index.cjs +514 -112
- package/dist/index.d.cts +161 -5
- package/dist/index.d.ts +161 -5
- package/dist/index.js +515 -113
- package/dist/matrx-oauth-_Ji8X5Wm.d.cts +264 -0
- package/dist/matrx-oauth-_Ji8X5Wm.d.ts +264 -0
- package/dist/matrx-oauth.cjs +77 -10
- package/dist/matrx-oauth.d.cts +1 -181
- package/dist/matrx-oauth.d.ts +1 -181
- package/dist/matrx-oauth.js +77 -10
- package/dist/organizations.cjs +269 -13
- package/dist/organizations.d.cts +3 -225
- package/dist/organizations.d.ts +3 -225
- package/dist/organizations.js +269 -13
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/package.json +12 -2
- package/dist/session-DdQiStp6.d.cts +0 -112
- package/dist/session-DdQiStp6.d.ts +0 -112
- package/dist/storage-Dqnye4KF.d.cts +0 -51
- package/dist/storage-Dqnye4KF.d.ts +0 -51
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session storage per platform — where a Supabase session lives between runs.
|
|
3
|
+
*
|
|
4
|
+
* supabase-js takes any `{ getItem, setItem, removeItem }` (sync or async). These are the ones
|
|
5
|
+
* every Matrx client needs, written once:
|
|
6
|
+
*
|
|
7
|
+
* browser — `browserStorage("local")` (supabase-js's own default; `"session"` = this tab only)
|
|
8
|
+
* Next.js — the cookie-backed clients in `@ai-matrx/auth/next` (the domain-wide auth cookie)
|
|
9
|
+
* extension — `extensionStorage()` over `chrome.storage.local` (shared by popup, side panel and
|
|
10
|
+
* service worker; `localStorage` does not exist in a service worker)
|
|
11
|
+
* desktop — `keyValueStorage({ get, set, remove })` over the host's store (Tauri store, keychain)
|
|
12
|
+
* Node — `fileStorage(path)` in `@ai-matrx/auth/node`, or `memoryStorage()` for a one-shot run
|
|
13
|
+
*
|
|
14
|
+
* None of these is ever silently swapped for another: a storage the platform does not have throws
|
|
15
|
+
* a named error at creation, never a quiet in-memory fallback that signs the person out on reload.
|
|
16
|
+
*/
|
|
17
|
+
/** The storage contract supabase-js reads (sync or async methods). */
|
|
18
|
+
interface SessionStorage {
|
|
19
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
20
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
21
|
+
removeItem(key: string): void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
declare class StorageUnavailableError extends Error {
|
|
24
|
+
readonly storage: string;
|
|
25
|
+
constructor(storage: string, reason: string);
|
|
26
|
+
}
|
|
27
|
+
/** In memory only: the session ends with the process (one-shot scripts, tests, server renders). */
|
|
28
|
+
declare function memoryStorage(): SessionStorage & {
|
|
29
|
+
readonly size: () => number;
|
|
30
|
+
};
|
|
31
|
+
/** The browser's `localStorage` (shared by every tab of the origin) or `sessionStorage` (this tab). */
|
|
32
|
+
declare function browserStorage(kind?: "local" | "session"): SessionStorage;
|
|
33
|
+
/** The structural slice of `chrome.storage.<area>` read here (MV3 promise API). */
|
|
34
|
+
interface ChromeStorageAreaLike {
|
|
35
|
+
get(keys: string): Promise<Record<string, unknown>>;
|
|
36
|
+
set(items: Record<string, unknown>): Promise<void>;
|
|
37
|
+
remove(keys: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A browser extension's `chrome.storage` area (default `local`): one session for the popup, side
|
|
41
|
+
* panel, content scripts' relay and the service worker. Pass `area` to inject one (tests, Firefox).
|
|
42
|
+
*/
|
|
43
|
+
declare function extensionStorage(area?: ChromeStorageAreaLike): SessionStorage;
|
|
44
|
+
/** Any async key-value store the host owns (a desktop app's Tauri store or keychain). */
|
|
45
|
+
declare function keyValueStorage(store: {
|
|
46
|
+
get(key: string): Promise<string | null | undefined>;
|
|
47
|
+
set(key: string, value: string): Promise<void>;
|
|
48
|
+
remove(key: string): Promise<void>;
|
|
49
|
+
}): SessionStorage;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* "Sign in with AI Matrx" — OAuth 2.1 authorization-code + PKCE (S256) against the AI Matrx auth
|
|
53
|
+
* server (Supabase's OAuth 2.1 server on https://db.matrxserver.com), written ONCE, host-neutral.
|
|
54
|
+
*
|
|
55
|
+
* Two apps used to carry hand-made copies; both now import this:
|
|
56
|
+
* - Chrome extension (matrx-extend/src/lib/auth/flow.ts): storage = chrome.storage, redirect
|
|
57
|
+
* captured by chrome.identity.launchWebAuthFlow, cross-context `navigator.locks`, AES-GCM
|
|
58
|
+
* sealed refresh token.
|
|
59
|
+
* - Desktop (matrx-desktop/app/main/auth): storage = the OS-sealed vault, authorize URL opened
|
|
60
|
+
* in the system browser, redirect captured by a loopback server on localhost.
|
|
61
|
+
*
|
|
62
|
+
* NOTHING host-specific is imported here (no chrome.*, electron, node:*, window). The host injects
|
|
63
|
+
* ports: `storage` (the package's SessionStorage shape), `openAuthorization`, and optionally
|
|
64
|
+
* `fetch`, `now`, `lock`, `seal`, `pendingStorage`, `beforeCommit`. Runs on WebCrypto
|
|
65
|
+
* (browser, extension service worker, Node >= 20).
|
|
66
|
+
*
|
|
67
|
+
* Config each app passes in (values they use today; this module hard-codes none of the ids):
|
|
68
|
+
* authUrl both: https://db.matrxserver.com (endpoints: /auth/v1/oauth/{authorize,token},
|
|
69
|
+
* /auth/v1/logout?scope=local)
|
|
70
|
+
* clientId extension: its own public PKCE client (env WXT_EXTENSION_OAUTH_CLIENT_ID);
|
|
71
|
+
* desktop: "Matrx 2" public client (see matrx-desktop/app/main/auth/config.ts)
|
|
72
|
+
* redirectUri extension: chrome.identity.getRedirectURL() = https://<id>.chromiumapp.org/
|
|
73
|
+
* (Safari: https://www.aimatrx.com/auth/extension-callback);
|
|
74
|
+
* desktop: http://localhost:22140/auth/callback (dev: 22240)
|
|
75
|
+
* scope both: "email profile". NEVER add "openid": the server then tries to mint an
|
|
76
|
+
* ID token and answers 500 "Error generating ID token".
|
|
77
|
+
* publishableKey desktop sends it as `apikey` on token + logout calls; extension on logout.
|
|
78
|
+
* Optional here; sent whenever given.
|
|
79
|
+
* refreshLeadMs extension 60_000, desktop 120_000 (default 60_000)
|
|
80
|
+
*
|
|
81
|
+
* Behaviour kept from the extension: verifier PERSISTED locally and only a random nonce travels as
|
|
82
|
+
* `state` (never the verifier in the URL); one verifier per attempt keyed by state; a newer
|
|
83
|
+
* attempt/sign-out supersedes an older one; state mismatch refused; the exact user-facing
|
|
84
|
+
* sentences; the cross-context refresh race guard (if the stored refresh token changed while our
|
|
85
|
+
* refresh was refused, adopt the winner instead of signing out); optional pre-commit validation.
|
|
86
|
+
* Kept from the desktop: constant-time state compare, 10 min attempt expiry, terminal-vs-transient
|
|
87
|
+
* error split (a refused grant ends the session; a network blip / 5xx / captive portal keeps it),
|
|
88
|
+
* HTML-on-200 is never a token, client-changed sessions dropped, refresh timeout, person id from JWT.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
type MatrxOAuthErrorCode = "config_missing" | "state_mismatch" | "callback_error" | "callback_incomplete" | "attempt_expired" | "verifier_lost" | "superseded" | "token_exchange_failed" | "invalid_token" | "grant_refused" | "transient" | "unsupported_environment";
|
|
92
|
+
declare class MatrxOAuthError extends Error {
|
|
93
|
+
readonly code: MatrxOAuthErrorCode;
|
|
94
|
+
constructor(code: MatrxOAuthErrorCode, message: string);
|
|
95
|
+
}
|
|
96
|
+
/** The server refused the grant (revoked, reused, expired): the session is over. Never retried. */
|
|
97
|
+
declare class GrantRefusedError extends MatrxOAuthError {
|
|
98
|
+
readonly serverCode: string;
|
|
99
|
+
constructor(serverCode: string, detail: string);
|
|
100
|
+
}
|
|
101
|
+
/** Network down, captive portal, 5xx, 429: try again later; the session is still good. */
|
|
102
|
+
declare class TransientAuthError extends MatrxOAuthError {
|
|
103
|
+
constructor(detail: string);
|
|
104
|
+
}
|
|
105
|
+
/** RFC 7636 §4.1: 43 chars of base64url from 32 random bytes. */
|
|
106
|
+
declare function generateCodeVerifier(): string;
|
|
107
|
+
/** RFC 7636 §4.2 S256: BASE64URL(SHA256(ASCII(verifier))). */
|
|
108
|
+
declare function codeChallengeFor(verifier: string): Promise<string>;
|
|
109
|
+
/** The `state` value: 256 random bits, ties the callback to this attempt. */
|
|
110
|
+
declare function generateState(): string;
|
|
111
|
+
/** Constant-time string comparison. */
|
|
112
|
+
declare function constantTimeEqual(a: string, b: string): boolean;
|
|
113
|
+
interface AuthorizeUrlInput {
|
|
114
|
+
authUrl: string;
|
|
115
|
+
clientId: string;
|
|
116
|
+
redirectUri: string;
|
|
117
|
+
state: string;
|
|
118
|
+
challenge: string;
|
|
119
|
+
scope: string;
|
|
120
|
+
}
|
|
121
|
+
declare function buildAuthorizeUrl(input: AuthorizeUrlInput): string;
|
|
122
|
+
/** Read `code` and `state` from the redirect the browser landed on; throws on error / missing parts. */
|
|
123
|
+
declare function parseCallbackUrl(callbackUrl: string): {
|
|
124
|
+
code: string;
|
|
125
|
+
returnedState: string;
|
|
126
|
+
};
|
|
127
|
+
interface AccessTokenClaims {
|
|
128
|
+
/** The person id (`sub`). */
|
|
129
|
+
personId: string;
|
|
130
|
+
email: string | null;
|
|
131
|
+
/** Expiry in ms since epoch, or null when the token carries no `exp`. */
|
|
132
|
+
expiresAtMs: number | null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Read `sub`, `email`, `exp` from an access token. The signature is NOT verified here: the token
|
|
136
|
+
* came straight from the issuer over TLS. Verify-on-receipt belongs to servers.
|
|
137
|
+
*/
|
|
138
|
+
declare function personIdFromAccessToken(token: string): AccessTokenClaims;
|
|
139
|
+
interface MatrxOAuthConfig {
|
|
140
|
+
/** Auth server origin, e.g. https://db.matrxserver.com */
|
|
141
|
+
authUrl: string;
|
|
142
|
+
/** This app's public PKCE client id. */
|
|
143
|
+
clientId: string;
|
|
144
|
+
/**
|
|
145
|
+
* Where the authorization server sends the browser back. A function is read at the moment a
|
|
146
|
+
* sign-in starts or completes (never at creation and never cached), for hosts that only know it
|
|
147
|
+
* once their identity API is up (the extension's `chrome.identity.getRedirectURL()`). A blank
|
|
148
|
+
* answer, or `about:blank`, refuses the sign-in with `config_missing`; read-only calls
|
|
149
|
+
* (`getSession`, `getAccessToken`, `refresh`, `signOut`) never need it.
|
|
150
|
+
*/
|
|
151
|
+
redirectUri: string | (() => string);
|
|
152
|
+
/**
|
|
153
|
+
* Renew through a different endpoint than `/auth/v1/oauth/token` (desktop's test lane: a session
|
|
154
|
+
* opened by Supabase's own auth renews on `/auth/v1/token?grant_type=refresh_token` with a JSON
|
|
155
|
+
* body `{ refresh_token }`). Only refresh requests are re-aimed; the code exchange never is.
|
|
156
|
+
*/
|
|
157
|
+
refreshEndpoint?: {
|
|
158
|
+
url: string;
|
|
159
|
+
body?: "form" | "json";
|
|
160
|
+
};
|
|
161
|
+
/** Default "email profile". Never include "openid". */
|
|
162
|
+
scope?: string;
|
|
163
|
+
/** Sent as `apikey` on token + logout calls when given. */
|
|
164
|
+
publishableKey?: string;
|
|
165
|
+
/** Refresh this long before expiry. Default 60_000. */
|
|
166
|
+
refreshLeadMs?: number;
|
|
167
|
+
/** A started sign-in expires after this. Default 600_000. */
|
|
168
|
+
attemptTtlMs?: number;
|
|
169
|
+
/** Token endpoint timeout. Default 15_000. */
|
|
170
|
+
requestTimeoutMs?: number;
|
|
171
|
+
/** Storage key namespace. Default "matrx-oauth". */
|
|
172
|
+
storageKeyPrefix?: string;
|
|
173
|
+
}
|
|
174
|
+
interface OpenAuthorizationContext {
|
|
175
|
+
state: string;
|
|
176
|
+
redirectUri: string;
|
|
177
|
+
}
|
|
178
|
+
interface MatrxOAuthPorts {
|
|
179
|
+
/** Where the session lives between runs (the package's SessionStorage shape). */
|
|
180
|
+
storage: SessionStorage;
|
|
181
|
+
/**
|
|
182
|
+
* Open the authorize URL and resolve with the full redirect URL the browser landed on.
|
|
183
|
+
* Reject if the person cancelled. Extension: chrome.identity.launchWebAuthFlow.
|
|
184
|
+
* Desktop: open the system browser and wait on the loopback server.
|
|
185
|
+
*/
|
|
186
|
+
openAuthorization(authorizeUrl: string, context: OpenAuthorizationContext): Promise<string>;
|
|
187
|
+
fetch?: typeof fetch;
|
|
188
|
+
now?: () => number;
|
|
189
|
+
/** Short-lived attempt state (verifier). Default: `storage`. Extension: chrome.storage.session. */
|
|
190
|
+
pendingStorage?: SessionStorage;
|
|
191
|
+
/** Serialises storage mutations across contexts (extension: navigator.locks). Default: in-process. */
|
|
192
|
+
lock?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
193
|
+
/** Seal/unseal the refresh token at rest (extension: AES-GCM). Default: stored as-is. */
|
|
194
|
+
seal?: {
|
|
195
|
+
encrypt(plain: string): Promise<string>;
|
|
196
|
+
decrypt(sealed: string): Promise<string>;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Runs on freshly exchanged tokens BEFORE they displace a working session; throw to refuse
|
|
200
|
+
* (extension: GET /auth/v1/user). Never call per request.
|
|
201
|
+
*/
|
|
202
|
+
beforeCommit?: (accessToken: string) => Promise<void>;
|
|
203
|
+
}
|
|
204
|
+
interface MatrxOAuthSession {
|
|
205
|
+
personId: string;
|
|
206
|
+
email: string | null;
|
|
207
|
+
accessToken: string;
|
|
208
|
+
expiresAtMs: number;
|
|
209
|
+
}
|
|
210
|
+
type MatrxOAuthEvent = {
|
|
211
|
+
type: "signed_in";
|
|
212
|
+
session: MatrxOAuthSession;
|
|
213
|
+
} | {
|
|
214
|
+
type: "refreshed";
|
|
215
|
+
session: MatrxOAuthSession;
|
|
216
|
+
} | {
|
|
217
|
+
type: "signed_out";
|
|
218
|
+
reason: "user" | "session_ended" | "client_changed" | "person_changed" | "unreadable";
|
|
219
|
+
};
|
|
220
|
+
interface TokenSet {
|
|
221
|
+
accessToken: string;
|
|
222
|
+
/** Null only if the server chose not to rotate it. */
|
|
223
|
+
refreshToken: string | null;
|
|
224
|
+
expiresAtMs: number;
|
|
225
|
+
}
|
|
226
|
+
interface MatrxOAuthClient {
|
|
227
|
+
/** Run the whole interactive sign-in. Resolves once tokens are stored. */
|
|
228
|
+
signIn(): Promise<MatrxOAuthSession>;
|
|
229
|
+
/**
|
|
230
|
+
* Finish a sign-in whose authorization code was already captured but whose `signIn()` call is gone
|
|
231
|
+
* (Safari: the background page caught the redirect, then the popup/worker was torn down). Pass the
|
|
232
|
+
* redirect URL, or the `code` + `state` from it. The PKCE verifier is recovered from the attempt
|
|
233
|
+
* still held in `pendingStorage` under that `state`; a `state` with no live attempt, or one past
|
|
234
|
+
* its lifetime, is refused with `verifier_lost` / `attempt_expired` ("start again"), never guessed.
|
|
235
|
+
*/
|
|
236
|
+
completeSignIn(captured: {
|
|
237
|
+
callbackUrl: string;
|
|
238
|
+
} | {
|
|
239
|
+
code: string;
|
|
240
|
+
state: string;
|
|
241
|
+
}): Promise<MatrxOAuthSession>;
|
|
242
|
+
/**
|
|
243
|
+
* Store a token set obtained without the browser (a host that already holds a Supabase-minted
|
|
244
|
+
* session, e.g. desktop's test-only copy sign-in) as this client's session. Runs `beforeCommit`
|
|
245
|
+
* and emits `signed_in`; needs a refresh token.
|
|
246
|
+
*/
|
|
247
|
+
adoptTokens(tokens: TokenSet): Promise<MatrxOAuthSession>;
|
|
248
|
+
/** A usable access token, refreshing on expiry (single-flight). Null = signed out. Throws on transient failure with no valid token left. */
|
|
249
|
+
getAccessToken(): Promise<string | null>;
|
|
250
|
+
/** Force a refresh (alarms, 401 retry). Pass the token the server rejected to refuse adopting it. */
|
|
251
|
+
refresh(rejectedAccessToken?: string): Promise<string | null>;
|
|
252
|
+
/** The stored session without refreshing or touching the network. */
|
|
253
|
+
getSession(): Promise<MatrxOAuthSession | null>;
|
|
254
|
+
/** The person id from the stored access token, no network. Null = signed out. */
|
|
255
|
+
getPersonId(): Promise<string | null>;
|
|
256
|
+
/** Clear local state, then best-effort revoke on the server. Returns whether the server agreed (null = nothing to revoke). */
|
|
257
|
+
signOut(): Promise<{
|
|
258
|
+
revoked: boolean | null;
|
|
259
|
+
}>;
|
|
260
|
+
onChange(listener: (event: MatrxOAuthEvent) => void): () => void;
|
|
261
|
+
}
|
|
262
|
+
declare function createMatrxOAuth(config: MatrxOAuthConfig, ports: MatrxOAuthPorts): MatrxOAuthClient;
|
|
263
|
+
|
|
264
|
+
export { type AccessTokenClaims as A, type ChromeStorageAreaLike as C, GrantRefusedError as G, type MatrxOAuthClient as M, type OpenAuthorizationContext as O, type SessionStorage as S, type TokenSet as T, StorageUnavailableError as a, browserStorage as b, type AuthorizeUrlInput as c, type MatrxOAuthConfig as d, extensionStorage as e, MatrxOAuthError as f, type MatrxOAuthErrorCode as g, type MatrxOAuthEvent as h, type MatrxOAuthPorts as i, type MatrxOAuthSession as j, keyValueStorage as k, TransientAuthError as l, memoryStorage as m, buildAuthorizeUrl as n, codeChallengeFor as o, constantTimeEqual as p, createMatrxOAuth as q, generateCodeVerifier as r, generateState as s, parseCallbackUrl as t, personIdFromAccessToken as u };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session storage per platform — where a Supabase session lives between runs.
|
|
3
|
+
*
|
|
4
|
+
* supabase-js takes any `{ getItem, setItem, removeItem }` (sync or async). These are the ones
|
|
5
|
+
* every Matrx client needs, written once:
|
|
6
|
+
*
|
|
7
|
+
* browser — `browserStorage("local")` (supabase-js's own default; `"session"` = this tab only)
|
|
8
|
+
* Next.js — the cookie-backed clients in `@ai-matrx/auth/next` (the domain-wide auth cookie)
|
|
9
|
+
* extension — `extensionStorage()` over `chrome.storage.local` (shared by popup, side panel and
|
|
10
|
+
* service worker; `localStorage` does not exist in a service worker)
|
|
11
|
+
* desktop — `keyValueStorage({ get, set, remove })` over the host's store (Tauri store, keychain)
|
|
12
|
+
* Node — `fileStorage(path)` in `@ai-matrx/auth/node`, or `memoryStorage()` for a one-shot run
|
|
13
|
+
*
|
|
14
|
+
* None of these is ever silently swapped for another: a storage the platform does not have throws
|
|
15
|
+
* a named error at creation, never a quiet in-memory fallback that signs the person out on reload.
|
|
16
|
+
*/
|
|
17
|
+
/** The storage contract supabase-js reads (sync or async methods). */
|
|
18
|
+
interface SessionStorage {
|
|
19
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
20
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
21
|
+
removeItem(key: string): void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
declare class StorageUnavailableError extends Error {
|
|
24
|
+
readonly storage: string;
|
|
25
|
+
constructor(storage: string, reason: string);
|
|
26
|
+
}
|
|
27
|
+
/** In memory only: the session ends with the process (one-shot scripts, tests, server renders). */
|
|
28
|
+
declare function memoryStorage(): SessionStorage & {
|
|
29
|
+
readonly size: () => number;
|
|
30
|
+
};
|
|
31
|
+
/** The browser's `localStorage` (shared by every tab of the origin) or `sessionStorage` (this tab). */
|
|
32
|
+
declare function browserStorage(kind?: "local" | "session"): SessionStorage;
|
|
33
|
+
/** The structural slice of `chrome.storage.<area>` read here (MV3 promise API). */
|
|
34
|
+
interface ChromeStorageAreaLike {
|
|
35
|
+
get(keys: string): Promise<Record<string, unknown>>;
|
|
36
|
+
set(items: Record<string, unknown>): Promise<void>;
|
|
37
|
+
remove(keys: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A browser extension's `chrome.storage` area (default `local`): one session for the popup, side
|
|
41
|
+
* panel, content scripts' relay and the service worker. Pass `area` to inject one (tests, Firefox).
|
|
42
|
+
*/
|
|
43
|
+
declare function extensionStorage(area?: ChromeStorageAreaLike): SessionStorage;
|
|
44
|
+
/** Any async key-value store the host owns (a desktop app's Tauri store or keychain). */
|
|
45
|
+
declare function keyValueStorage(store: {
|
|
46
|
+
get(key: string): Promise<string | null | undefined>;
|
|
47
|
+
set(key: string, value: string): Promise<void>;
|
|
48
|
+
remove(key: string): Promise<void>;
|
|
49
|
+
}): SessionStorage;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* "Sign in with AI Matrx" — OAuth 2.1 authorization-code + PKCE (S256) against the AI Matrx auth
|
|
53
|
+
* server (Supabase's OAuth 2.1 server on https://db.matrxserver.com), written ONCE, host-neutral.
|
|
54
|
+
*
|
|
55
|
+
* Two apps used to carry hand-made copies; both now import this:
|
|
56
|
+
* - Chrome extension (matrx-extend/src/lib/auth/flow.ts): storage = chrome.storage, redirect
|
|
57
|
+
* captured by chrome.identity.launchWebAuthFlow, cross-context `navigator.locks`, AES-GCM
|
|
58
|
+
* sealed refresh token.
|
|
59
|
+
* - Desktop (matrx-desktop/app/main/auth): storage = the OS-sealed vault, authorize URL opened
|
|
60
|
+
* in the system browser, redirect captured by a loopback server on localhost.
|
|
61
|
+
*
|
|
62
|
+
* NOTHING host-specific is imported here (no chrome.*, electron, node:*, window). The host injects
|
|
63
|
+
* ports: `storage` (the package's SessionStorage shape), `openAuthorization`, and optionally
|
|
64
|
+
* `fetch`, `now`, `lock`, `seal`, `pendingStorage`, `beforeCommit`. Runs on WebCrypto
|
|
65
|
+
* (browser, extension service worker, Node >= 20).
|
|
66
|
+
*
|
|
67
|
+
* Config each app passes in (values they use today; this module hard-codes none of the ids):
|
|
68
|
+
* authUrl both: https://db.matrxserver.com (endpoints: /auth/v1/oauth/{authorize,token},
|
|
69
|
+
* /auth/v1/logout?scope=local)
|
|
70
|
+
* clientId extension: its own public PKCE client (env WXT_EXTENSION_OAUTH_CLIENT_ID);
|
|
71
|
+
* desktop: "Matrx 2" public client (see matrx-desktop/app/main/auth/config.ts)
|
|
72
|
+
* redirectUri extension: chrome.identity.getRedirectURL() = https://<id>.chromiumapp.org/
|
|
73
|
+
* (Safari: https://www.aimatrx.com/auth/extension-callback);
|
|
74
|
+
* desktop: http://localhost:22140/auth/callback (dev: 22240)
|
|
75
|
+
* scope both: "email profile". NEVER add "openid": the server then tries to mint an
|
|
76
|
+
* ID token and answers 500 "Error generating ID token".
|
|
77
|
+
* publishableKey desktop sends it as `apikey` on token + logout calls; extension on logout.
|
|
78
|
+
* Optional here; sent whenever given.
|
|
79
|
+
* refreshLeadMs extension 60_000, desktop 120_000 (default 60_000)
|
|
80
|
+
*
|
|
81
|
+
* Behaviour kept from the extension: verifier PERSISTED locally and only a random nonce travels as
|
|
82
|
+
* `state` (never the verifier in the URL); one verifier per attempt keyed by state; a newer
|
|
83
|
+
* attempt/sign-out supersedes an older one; state mismatch refused; the exact user-facing
|
|
84
|
+
* sentences; the cross-context refresh race guard (if the stored refresh token changed while our
|
|
85
|
+
* refresh was refused, adopt the winner instead of signing out); optional pre-commit validation.
|
|
86
|
+
* Kept from the desktop: constant-time state compare, 10 min attempt expiry, terminal-vs-transient
|
|
87
|
+
* error split (a refused grant ends the session; a network blip / 5xx / captive portal keeps it),
|
|
88
|
+
* HTML-on-200 is never a token, client-changed sessions dropped, refresh timeout, person id from JWT.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
type MatrxOAuthErrorCode = "config_missing" | "state_mismatch" | "callback_error" | "callback_incomplete" | "attempt_expired" | "verifier_lost" | "superseded" | "token_exchange_failed" | "invalid_token" | "grant_refused" | "transient" | "unsupported_environment";
|
|
92
|
+
declare class MatrxOAuthError extends Error {
|
|
93
|
+
readonly code: MatrxOAuthErrorCode;
|
|
94
|
+
constructor(code: MatrxOAuthErrorCode, message: string);
|
|
95
|
+
}
|
|
96
|
+
/** The server refused the grant (revoked, reused, expired): the session is over. Never retried. */
|
|
97
|
+
declare class GrantRefusedError extends MatrxOAuthError {
|
|
98
|
+
readonly serverCode: string;
|
|
99
|
+
constructor(serverCode: string, detail: string);
|
|
100
|
+
}
|
|
101
|
+
/** Network down, captive portal, 5xx, 429: try again later; the session is still good. */
|
|
102
|
+
declare class TransientAuthError extends MatrxOAuthError {
|
|
103
|
+
constructor(detail: string);
|
|
104
|
+
}
|
|
105
|
+
/** RFC 7636 §4.1: 43 chars of base64url from 32 random bytes. */
|
|
106
|
+
declare function generateCodeVerifier(): string;
|
|
107
|
+
/** RFC 7636 §4.2 S256: BASE64URL(SHA256(ASCII(verifier))). */
|
|
108
|
+
declare function codeChallengeFor(verifier: string): Promise<string>;
|
|
109
|
+
/** The `state` value: 256 random bits, ties the callback to this attempt. */
|
|
110
|
+
declare function generateState(): string;
|
|
111
|
+
/** Constant-time string comparison. */
|
|
112
|
+
declare function constantTimeEqual(a: string, b: string): boolean;
|
|
113
|
+
interface AuthorizeUrlInput {
|
|
114
|
+
authUrl: string;
|
|
115
|
+
clientId: string;
|
|
116
|
+
redirectUri: string;
|
|
117
|
+
state: string;
|
|
118
|
+
challenge: string;
|
|
119
|
+
scope: string;
|
|
120
|
+
}
|
|
121
|
+
declare function buildAuthorizeUrl(input: AuthorizeUrlInput): string;
|
|
122
|
+
/** Read `code` and `state` from the redirect the browser landed on; throws on error / missing parts. */
|
|
123
|
+
declare function parseCallbackUrl(callbackUrl: string): {
|
|
124
|
+
code: string;
|
|
125
|
+
returnedState: string;
|
|
126
|
+
};
|
|
127
|
+
interface AccessTokenClaims {
|
|
128
|
+
/** The person id (`sub`). */
|
|
129
|
+
personId: string;
|
|
130
|
+
email: string | null;
|
|
131
|
+
/** Expiry in ms since epoch, or null when the token carries no `exp`. */
|
|
132
|
+
expiresAtMs: number | null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Read `sub`, `email`, `exp` from an access token. The signature is NOT verified here: the token
|
|
136
|
+
* came straight from the issuer over TLS. Verify-on-receipt belongs to servers.
|
|
137
|
+
*/
|
|
138
|
+
declare function personIdFromAccessToken(token: string): AccessTokenClaims;
|
|
139
|
+
interface MatrxOAuthConfig {
|
|
140
|
+
/** Auth server origin, e.g. https://db.matrxserver.com */
|
|
141
|
+
authUrl: string;
|
|
142
|
+
/** This app's public PKCE client id. */
|
|
143
|
+
clientId: string;
|
|
144
|
+
/**
|
|
145
|
+
* Where the authorization server sends the browser back. A function is read at the moment a
|
|
146
|
+
* sign-in starts or completes (never at creation and never cached), for hosts that only know it
|
|
147
|
+
* once their identity API is up (the extension's `chrome.identity.getRedirectURL()`). A blank
|
|
148
|
+
* answer, or `about:blank`, refuses the sign-in with `config_missing`; read-only calls
|
|
149
|
+
* (`getSession`, `getAccessToken`, `refresh`, `signOut`) never need it.
|
|
150
|
+
*/
|
|
151
|
+
redirectUri: string | (() => string);
|
|
152
|
+
/**
|
|
153
|
+
* Renew through a different endpoint than `/auth/v1/oauth/token` (desktop's test lane: a session
|
|
154
|
+
* opened by Supabase's own auth renews on `/auth/v1/token?grant_type=refresh_token` with a JSON
|
|
155
|
+
* body `{ refresh_token }`). Only refresh requests are re-aimed; the code exchange never is.
|
|
156
|
+
*/
|
|
157
|
+
refreshEndpoint?: {
|
|
158
|
+
url: string;
|
|
159
|
+
body?: "form" | "json";
|
|
160
|
+
};
|
|
161
|
+
/** Default "email profile". Never include "openid". */
|
|
162
|
+
scope?: string;
|
|
163
|
+
/** Sent as `apikey` on token + logout calls when given. */
|
|
164
|
+
publishableKey?: string;
|
|
165
|
+
/** Refresh this long before expiry. Default 60_000. */
|
|
166
|
+
refreshLeadMs?: number;
|
|
167
|
+
/** A started sign-in expires after this. Default 600_000. */
|
|
168
|
+
attemptTtlMs?: number;
|
|
169
|
+
/** Token endpoint timeout. Default 15_000. */
|
|
170
|
+
requestTimeoutMs?: number;
|
|
171
|
+
/** Storage key namespace. Default "matrx-oauth". */
|
|
172
|
+
storageKeyPrefix?: string;
|
|
173
|
+
}
|
|
174
|
+
interface OpenAuthorizationContext {
|
|
175
|
+
state: string;
|
|
176
|
+
redirectUri: string;
|
|
177
|
+
}
|
|
178
|
+
interface MatrxOAuthPorts {
|
|
179
|
+
/** Where the session lives between runs (the package's SessionStorage shape). */
|
|
180
|
+
storage: SessionStorage;
|
|
181
|
+
/**
|
|
182
|
+
* Open the authorize URL and resolve with the full redirect URL the browser landed on.
|
|
183
|
+
* Reject if the person cancelled. Extension: chrome.identity.launchWebAuthFlow.
|
|
184
|
+
* Desktop: open the system browser and wait on the loopback server.
|
|
185
|
+
*/
|
|
186
|
+
openAuthorization(authorizeUrl: string, context: OpenAuthorizationContext): Promise<string>;
|
|
187
|
+
fetch?: typeof fetch;
|
|
188
|
+
now?: () => number;
|
|
189
|
+
/** Short-lived attempt state (verifier). Default: `storage`. Extension: chrome.storage.session. */
|
|
190
|
+
pendingStorage?: SessionStorage;
|
|
191
|
+
/** Serialises storage mutations across contexts (extension: navigator.locks). Default: in-process. */
|
|
192
|
+
lock?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
193
|
+
/** Seal/unseal the refresh token at rest (extension: AES-GCM). Default: stored as-is. */
|
|
194
|
+
seal?: {
|
|
195
|
+
encrypt(plain: string): Promise<string>;
|
|
196
|
+
decrypt(sealed: string): Promise<string>;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Runs on freshly exchanged tokens BEFORE they displace a working session; throw to refuse
|
|
200
|
+
* (extension: GET /auth/v1/user). Never call per request.
|
|
201
|
+
*/
|
|
202
|
+
beforeCommit?: (accessToken: string) => Promise<void>;
|
|
203
|
+
}
|
|
204
|
+
interface MatrxOAuthSession {
|
|
205
|
+
personId: string;
|
|
206
|
+
email: string | null;
|
|
207
|
+
accessToken: string;
|
|
208
|
+
expiresAtMs: number;
|
|
209
|
+
}
|
|
210
|
+
type MatrxOAuthEvent = {
|
|
211
|
+
type: "signed_in";
|
|
212
|
+
session: MatrxOAuthSession;
|
|
213
|
+
} | {
|
|
214
|
+
type: "refreshed";
|
|
215
|
+
session: MatrxOAuthSession;
|
|
216
|
+
} | {
|
|
217
|
+
type: "signed_out";
|
|
218
|
+
reason: "user" | "session_ended" | "client_changed" | "person_changed" | "unreadable";
|
|
219
|
+
};
|
|
220
|
+
interface TokenSet {
|
|
221
|
+
accessToken: string;
|
|
222
|
+
/** Null only if the server chose not to rotate it. */
|
|
223
|
+
refreshToken: string | null;
|
|
224
|
+
expiresAtMs: number;
|
|
225
|
+
}
|
|
226
|
+
interface MatrxOAuthClient {
|
|
227
|
+
/** Run the whole interactive sign-in. Resolves once tokens are stored. */
|
|
228
|
+
signIn(): Promise<MatrxOAuthSession>;
|
|
229
|
+
/**
|
|
230
|
+
* Finish a sign-in whose authorization code was already captured but whose `signIn()` call is gone
|
|
231
|
+
* (Safari: the background page caught the redirect, then the popup/worker was torn down). Pass the
|
|
232
|
+
* redirect URL, or the `code` + `state` from it. The PKCE verifier is recovered from the attempt
|
|
233
|
+
* still held in `pendingStorage` under that `state`; a `state` with no live attempt, or one past
|
|
234
|
+
* its lifetime, is refused with `verifier_lost` / `attempt_expired` ("start again"), never guessed.
|
|
235
|
+
*/
|
|
236
|
+
completeSignIn(captured: {
|
|
237
|
+
callbackUrl: string;
|
|
238
|
+
} | {
|
|
239
|
+
code: string;
|
|
240
|
+
state: string;
|
|
241
|
+
}): Promise<MatrxOAuthSession>;
|
|
242
|
+
/**
|
|
243
|
+
* Store a token set obtained without the browser (a host that already holds a Supabase-minted
|
|
244
|
+
* session, e.g. desktop's test-only copy sign-in) as this client's session. Runs `beforeCommit`
|
|
245
|
+
* and emits `signed_in`; needs a refresh token.
|
|
246
|
+
*/
|
|
247
|
+
adoptTokens(tokens: TokenSet): Promise<MatrxOAuthSession>;
|
|
248
|
+
/** A usable access token, refreshing on expiry (single-flight). Null = signed out. Throws on transient failure with no valid token left. */
|
|
249
|
+
getAccessToken(): Promise<string | null>;
|
|
250
|
+
/** Force a refresh (alarms, 401 retry). Pass the token the server rejected to refuse adopting it. */
|
|
251
|
+
refresh(rejectedAccessToken?: string): Promise<string | null>;
|
|
252
|
+
/** The stored session without refreshing or touching the network. */
|
|
253
|
+
getSession(): Promise<MatrxOAuthSession | null>;
|
|
254
|
+
/** The person id from the stored access token, no network. Null = signed out. */
|
|
255
|
+
getPersonId(): Promise<string | null>;
|
|
256
|
+
/** Clear local state, then best-effort revoke on the server. Returns whether the server agreed (null = nothing to revoke). */
|
|
257
|
+
signOut(): Promise<{
|
|
258
|
+
revoked: boolean | null;
|
|
259
|
+
}>;
|
|
260
|
+
onChange(listener: (event: MatrxOAuthEvent) => void): () => void;
|
|
261
|
+
}
|
|
262
|
+
declare function createMatrxOAuth(config: MatrxOAuthConfig, ports: MatrxOAuthPorts): MatrxOAuthClient;
|
|
263
|
+
|
|
264
|
+
export { type AccessTokenClaims as A, type ChromeStorageAreaLike as C, GrantRefusedError as G, type MatrxOAuthClient as M, type OpenAuthorizationContext as O, type SessionStorage as S, type TokenSet as T, StorageUnavailableError as a, browserStorage as b, type AuthorizeUrlInput as c, type MatrxOAuthConfig as d, extensionStorage as e, MatrxOAuthError as f, type MatrxOAuthErrorCode as g, type MatrxOAuthEvent as h, type MatrxOAuthPorts as i, type MatrxOAuthSession as j, keyValueStorage as k, TransientAuthError as l, memoryStorage as m, buildAuthorizeUrl as n, codeChallengeFor as o, constantTimeEqual as p, createMatrxOAuth as q, generateCodeVerifier as r, generateState as s, parseCallbackUrl as t, personIdFromAccessToken as u };
|