@flow-industries/id 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/sdk/client/access-key.d.ts +1 -1
- package/dist/sdk/client/access-key.js +20 -6
- package/dist/sdk/client/create-flow.js +19 -11
- package/dist/sdk/client/dialog-host.js +8 -2
- package/dist/sdk/client/flow-widget.js +2 -1
- package/dist/sdk/client/idb.d.ts +2 -2
- package/dist/sdk/client/idb.js +17 -2
- package/dist/sdk/client/profile-button.js +1 -1
- package/dist/sdk/client/rooms.js +19 -4
- package/dist/sdk/client/session.js +8 -5
- package/dist/sdk/client/signing.js +22 -7
- package/dist/sdk/client/store.js +4 -5
- package/dist/sdk/cookies.d.ts +3 -1
- package/dist/sdk/cookies.js +11 -9
- package/dist/sdk/dialog/remote/Messenger.d.ts +0 -5
- package/dist/sdk/dialog/remote/Messenger.js +26 -14
- package/dist/sdk/driver-error.d.ts +8 -0
- package/dist/sdk/driver-error.js +17 -0
- package/dist/sdk/hex.d.ts +6 -0
- package/dist/sdk/hex.js +6 -0
- package/dist/sdk/id-host.d.ts +1 -1
- package/dist/sdk/id-host.js +4 -5
- package/dist/sdk/json.d.ts +17 -0
- package/dist/sdk/json.js +5 -0
- package/dist/sdk/react/flow-widget.js +2 -2
- package/dist/sdk/react/hooks.d.ts +8 -4
- package/dist/sdk/react/hooks.js +1 -10
- package/dist/sdk/react/profile-button.js +2 -2
- package/dist/sdk/server.js +2 -1
- package/dist/sdk/session-core.d.ts +1 -6
- package/dist/sdk/session-core.js +7 -4
- package/dist/sdk/session-route.d.ts +0 -20
- package/dist/sdk/session-route.js +25 -9
- package/dist/sdk/start/graceful-shutdown.d.ts +8 -0
- package/dist/sdk/start/graceful-shutdown.js +81 -0
- package/dist/sdk/start/index.d.ts +2 -0
- package/dist/sdk/start/index.js +3 -2
- package/dist/sdk/token-expiry.js +3 -1
- package/dist/sdk/types/cosmetics.d.ts +162 -0
- package/dist/sdk/types/cosmetics.js +78 -0
- package/dist/sdk/types/game.d.ts +58 -0
- package/dist/sdk/types/game.js +8 -0
- package/dist/sdk/types/index.d.ts +7 -3
- package/dist/sdk/types/index.js +2 -0
- package/dist/sdk/types/messenger.d.ts +1 -1
- package/dist/sdk/types/protocol.d.ts +6 -5
- package/dist/sdk/types/protocol.js +10 -4
- package/dist/sdk/types/room-events.d.ts +49 -1
- package/dist/sdk/types/rooms.d.ts +53 -3
- package/dist/sdk/types/rooms.js +0 -1
- package/dist/sdk/types/sdk.d.ts +2 -1
- package/dist/sdk/types/server.d.ts +18 -0
- package/dist/sdk/verify.js +1 -0
- package/dist/sdk/wagmi/index.d.ts +7 -3
- package/dist/sdk/wagmi/index.js +19 -32
- package/package.json +11 -8
- package/dist/sdk/client/refresh-store.d.ts +0 -22
- package/dist/sdk/client/refresh-store.js +0 -66
package/README.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Passkey-first identity for Flow applications. One passkey bound to `id.flow.industries`, usable across Flow apps with audience-bound JWTs and optional Tempo signing.
|
|
4
4
|
|
|
5
|
+
## Develop
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env
|
|
9
|
+
bun install
|
|
10
|
+
bun run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`bun run dev` is the only supported local application launcher. It starts the
|
|
14
|
+
API, dialog, playground, and landing page on the first free block of
|
|
15
|
+
four consecutive ports. Configure `DATABASE_URL` and `BETTER_AUTH_SECRET` in
|
|
16
|
+
`.env` first.
|
|
17
|
+
|
|
18
|
+
The launcher validates locally stored JWT signing keys before starting Auth.
|
|
19
|
+
If `BETTER_AUTH_SECRET` changed, only keys that can no longer be decrypted are
|
|
20
|
+
rotated so local session creation keeps working after environment changes.
|
|
21
|
+
It also refuses to start when migrations fail instead of serving against a
|
|
22
|
+
partially migrated schema.
|
|
23
|
+
|
|
5
24
|
**npm:** [`@flow-industries/id`](https://www.npmjs.com/package/@flow-industries/id)
|
|
6
25
|
|
|
7
26
|
## Installation
|
|
@@ -38,7 +38,7 @@ export declare function loadAccessKey(address: Hex): Promise<StoredAccessKey | u
|
|
|
38
38
|
* consumed at consensus, not execution — the keychain entry persists even
|
|
39
39
|
* if the call inside the tx reverts.
|
|
40
40
|
*/
|
|
41
|
-
export declare function consumePendingAuthorization(address: Hex): Promise<
|
|
41
|
+
export declare function consumePendingAuthorization(address: Hex): Promise<StoredAccessKey["keyAuthorization"] | null>;
|
|
42
42
|
export declare function isExpired(stored: StoredAccessKey): boolean;
|
|
43
43
|
export declare function clearAccessKey(address: Hex): Promise<void>;
|
|
44
44
|
/**
|
|
@@ -2,6 +2,7 @@ import * as Address from "ox/Address";
|
|
|
2
2
|
import * as PublicKey from "ox/PublicKey";
|
|
3
3
|
import { KeyAuthorization, SignatureEnvelope } from "ox/tempo";
|
|
4
4
|
import { Account, WebCryptoP256 } from "viem/tempo";
|
|
5
|
+
import { hex0x } from "../hex";
|
|
5
6
|
import { idb } from "./idb";
|
|
6
7
|
/**
|
|
7
8
|
* Step 1 of access-key creation — runs BEFORE the dialog opens.
|
|
@@ -27,6 +28,9 @@ export async function prepareAccessKey(options, chainId) {
|
|
|
27
28
|
expiry: options.expiry,
|
|
28
29
|
type: "p256",
|
|
29
30
|
});
|
|
31
|
+
/* SAFETY: tempo's KeyAuthorization.getSignPayload is typed for the signed variant only, but
|
|
32
|
+
the sign payload is by definition computed from the UNSIGNED authorization built above, and
|
|
33
|
+
it returns the hash as a hex string. */
|
|
30
34
|
const accessKeyHash = KeyAuthorization.getSignPayload(keyAuthUnsigned);
|
|
31
35
|
return { keyPair, keyAuthUnsigned, accessKeyHash, expiry: options.expiry };
|
|
32
36
|
}
|
|
@@ -43,7 +47,7 @@ export async function finalizeAccessKey(params) {
|
|
|
43
47
|
const { address, credential, webauthn, preparation } = params;
|
|
44
48
|
const signatureEnvelope = SignatureEnvelope.from({
|
|
45
49
|
metadata: {
|
|
46
|
-
authenticatorData: webauthn.metadata.authenticatorData,
|
|
50
|
+
authenticatorData: hex0x(webauthn.metadata.authenticatorData),
|
|
47
51
|
clientDataJSON: webauthn.metadata.clientDataJSON,
|
|
48
52
|
challengeIndex: webauthn.metadata.challengeIndex,
|
|
49
53
|
typeIndex: webauthn.metadata.typeIndex,
|
|
@@ -52,11 +56,14 @@ export async function finalizeAccessKey(params) {
|
|
|
52
56
|
r: BigInt(webauthn.signature.r),
|
|
53
57
|
s: BigInt(webauthn.signature.s),
|
|
54
58
|
},
|
|
55
|
-
publicKey: PublicKey.from(
|
|
59
|
+
publicKey: PublicKey.from(hex0x(credential.publicKey)),
|
|
56
60
|
type: "webAuthn",
|
|
57
61
|
});
|
|
62
|
+
/* SAFETY: this is the same authorization prepared in step 1, now carrying the passkey
|
|
63
|
+
signature. tempo types `from` against its signed union, which the spread cannot reconstruct
|
|
64
|
+
structurally, so the shape is named once here. */
|
|
58
65
|
const keyAuthorization = KeyAuthorization.from({
|
|
59
|
-
...preparation.keyAuthUnsigned,
|
|
66
|
+
...Object(preparation.keyAuthUnsigned),
|
|
60
67
|
signature: signatureEnvelope,
|
|
61
68
|
});
|
|
62
69
|
const stored = {
|
|
@@ -92,9 +99,13 @@ export async function consumePendingAuthorization(address) {
|
|
|
92
99
|
});
|
|
93
100
|
return auth;
|
|
94
101
|
}
|
|
102
|
+
/** Older stored keys kept the expiry inside the authorization; tempo does not type that field. */
|
|
103
|
+
function expiryOf(keyAuthorization) {
|
|
104
|
+
const found = Object.entries(Object(keyAuthorization)).find(([k]) => k === "expiry")?.[1];
|
|
105
|
+
return Number(found) === found ? found : undefined;
|
|
106
|
+
}
|
|
95
107
|
export function isExpired(stored) {
|
|
96
|
-
const
|
|
97
|
-
const expiry = stored.expiry ?? auth?.expiry;
|
|
108
|
+
const expiry = stored.expiry ?? expiryOf(stored.keyAuthorization);
|
|
98
109
|
if (!expiry)
|
|
99
110
|
return false;
|
|
100
111
|
return expiry < Date.now() / 1000;
|
|
@@ -110,8 +121,11 @@ export async function clearAccessKey(address) {
|
|
|
110
121
|
* allowed to act for the address.
|
|
111
122
|
*/
|
|
112
123
|
export function buildAccessKeyAccount(stored, rootCredential, rpId) {
|
|
124
|
+
/* SAFETY: tempo's Account constructors are typed against porto's own credential and key
|
|
125
|
+
types, which Flow's equivalents mirror field-for-field but are not nominally the same. The
|
|
126
|
+
four assertions below are that nominal gap, not a claim about the runtime values. */
|
|
113
127
|
const rootAccount = Account.fromWebAuthnP256(rootCredential, {
|
|
114
|
-
|
|
128
|
+
rpId: rpId ? rpId : undefined,
|
|
115
129
|
});
|
|
116
130
|
return Account.fromWebCryptoP256({
|
|
117
131
|
privateKey: stored.privateKey,
|
|
@@ -14,7 +14,10 @@ import { createStore, initialFlowState } from "./store";
|
|
|
14
14
|
* where Web Locks is unavailable.
|
|
15
15
|
*/
|
|
16
16
|
function withOriginLock(name, fn) {
|
|
17
|
-
const locks =
|
|
17
|
+
const locks =
|
|
18
|
+
/* SAFETY: the SDK stashes its singleton on globalThis so two bundles of it share one
|
|
19
|
+
instance; the property is namespaced and written only here. */
|
|
20
|
+
globalThis.navigator?.locks;
|
|
18
21
|
if (locks?.request)
|
|
19
22
|
return locks.request(name, fn);
|
|
20
23
|
return fn();
|
|
@@ -65,7 +68,7 @@ export function createFlow(options = {}) {
|
|
|
65
68
|
// and IDB writes while components/hooks that captured it keep reading
|
|
66
69
|
// stale state. For genuine multi-instance scenarios (tests), call
|
|
67
70
|
// resetFlow() first or pass explicit Flow instances.
|
|
68
|
-
if (
|
|
71
|
+
if (!("window" in globalThis)) {
|
|
69
72
|
throw new Error("createFlow() is browser-only (IndexedDB, iframes, fetch with " +
|
|
70
73
|
"cookies). For server rendering, resolve state with resolveSession() " +
|
|
71
74
|
"from @flow-industries/id/server and render with createStaticFlow().");
|
|
@@ -112,13 +115,15 @@ export function createFlow(options = {}) {
|
|
|
112
115
|
* knows neither, and overwriting would erase state restored from IDB.
|
|
113
116
|
*/
|
|
114
117
|
function commitSession(session) {
|
|
115
|
-
|
|
118
|
+
const next = {
|
|
116
119
|
user: session.user,
|
|
117
120
|
jwt: session.jwt,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
};
|
|
122
|
+
if (session.credential !== undefined) {
|
|
123
|
+
next.credential = session.credential;
|
|
124
|
+
next.address = session.address ?? null;
|
|
125
|
+
}
|
|
126
|
+
store.setState(next);
|
|
122
127
|
// A guest carries a null credential; never persist that — the IDB store is
|
|
123
128
|
// reserved for a real passkey credential and writing null would erase a
|
|
124
129
|
// previously stored one.
|
|
@@ -141,6 +146,7 @@ export function createFlow(options = {}) {
|
|
|
141
146
|
});
|
|
142
147
|
if (!res.ok)
|
|
143
148
|
return false;
|
|
149
|
+
// SAFETY: the app's own /flow/session route answers this shape.
|
|
144
150
|
const { state } = (await res.json());
|
|
145
151
|
if (!state)
|
|
146
152
|
return false;
|
|
@@ -168,6 +174,7 @@ export function createFlow(options = {}) {
|
|
|
168
174
|
});
|
|
169
175
|
if (!res.ok)
|
|
170
176
|
return null;
|
|
177
|
+
// SAFETY: the app's own /flow/session route answers this shape.
|
|
171
178
|
const { additionalSessions } = (await res.json());
|
|
172
179
|
return additionalSessions?.find((s) => s.audience === audience) ?? null;
|
|
173
180
|
}
|
|
@@ -298,7 +305,8 @@ export function createFlow(options = {}) {
|
|
|
298
305
|
// Only a guest has no credential. The server's idempotency path can
|
|
299
306
|
// return a full session here; in that case keep any credential/
|
|
300
307
|
// address already in state rather than stripping signing.
|
|
301
|
-
|
|
308
|
+
credential: result.user.isGuest ? null : undefined,
|
|
309
|
+
address: result.user.isGuest ? null : undefined,
|
|
302
310
|
});
|
|
303
311
|
await installSession(result.refreshToken, result.jwt);
|
|
304
312
|
deliverAdditionalSessions(result.additionalSessions);
|
|
@@ -337,8 +345,8 @@ export function createFlow(options = {}) {
|
|
|
337
345
|
getState: () => store.getSnapshot(),
|
|
338
346
|
getChain,
|
|
339
347
|
getTransport,
|
|
340
|
-
|
|
341
|
-
|
|
348
|
+
rpId: rpId ? rpId : undefined,
|
|
349
|
+
strict: accessKeyOptions?.strict ? accessKeyOptions.strict : undefined,
|
|
342
350
|
};
|
|
343
351
|
}
|
|
344
352
|
/**
|
|
@@ -389,7 +397,7 @@ export function createFlow(options = {}) {
|
|
|
389
397
|
dialog: dialogHost,
|
|
390
398
|
store,
|
|
391
399
|
options: loginOpts,
|
|
392
|
-
|
|
400
|
+
extraCapabilities: extraCapabilities ? extraCapabilities : undefined,
|
|
393
401
|
});
|
|
394
402
|
await installSession(refreshToken, session.jwt);
|
|
395
403
|
deliverAdditionalSessions(additionalSessions);
|
|
@@ -97,6 +97,7 @@ export function createDialogHost(options) {
|
|
|
97
97
|
if (initSent)
|
|
98
98
|
return;
|
|
99
99
|
initSent = true;
|
|
100
|
+
// SAFETY: the payload map types this topic; the dialog reads exactly these fields.
|
|
100
101
|
messenger?.send("__internal", {
|
|
101
102
|
type: "init",
|
|
102
103
|
mode: "iframe",
|
|
@@ -111,7 +112,9 @@ export function createDialogHost(options) {
|
|
|
111
112
|
// The dialog owns its open/close animation and mounts the overlay (playing
|
|
112
113
|
// the enter animation) on this signal — same mechanism as the profile
|
|
113
114
|
// widget, so every dialog appears and disappears identically.
|
|
114
|
-
void
|
|
115
|
+
void (
|
|
116
|
+
// SAFETY: the payload map types this topic; the dialog reads exactly these fields.
|
|
117
|
+
messenger?.send("__internal", { type: "dialog-shown" }));
|
|
115
118
|
}
|
|
116
119
|
function hide() {
|
|
117
120
|
if (!iframe)
|
|
@@ -121,7 +124,9 @@ export function createDialogHost(options) {
|
|
|
121
124
|
// requestAnimationFrame, making the close (and the next open) skip straight
|
|
122
125
|
// to the end. While hidden the overlay is transparent and click-through.
|
|
123
126
|
iframe.style.pointerEvents = "none";
|
|
124
|
-
void
|
|
127
|
+
void (
|
|
128
|
+
// SAFETY: the payload map types this topic; the dialog reads exactly these fields.
|
|
129
|
+
messenger?.send("__internal", { type: "dialog-hidden" }));
|
|
125
130
|
}
|
|
126
131
|
function open(opts) {
|
|
127
132
|
if (opts?.mode === "popup") {
|
|
@@ -160,6 +165,7 @@ export function createDialogHost(options) {
|
|
|
160
165
|
const rpcRequest = { id, method, params, jsonrpc: "2.0" };
|
|
161
166
|
return new Promise((resolve, reject) => {
|
|
162
167
|
pending.set(id, { resolve, reject });
|
|
168
|
+
// SAFETY: the rpc-requests payload shape.
|
|
163
169
|
messenger.send("rpc-requests", [
|
|
164
170
|
{ request: rpcRequest, status: "pending" },
|
|
165
171
|
]);
|
|
@@ -30,7 +30,7 @@ const WIDGET_ROUTE = {
|
|
|
30
30
|
* No-op under SSR.
|
|
31
31
|
*/
|
|
32
32
|
export function createFlowWidget(options) {
|
|
33
|
-
if (
|
|
33
|
+
if (!("document" in globalThis))
|
|
34
34
|
return {
|
|
35
35
|
frame: null,
|
|
36
36
|
post() { },
|
|
@@ -48,6 +48,7 @@ export function createFlowWidget(options) {
|
|
|
48
48
|
// Appended before bridging: `contentWindow` is null until the frame is in the
|
|
49
49
|
// document, and the bridge is bound to that window.
|
|
50
50
|
container.appendChild(frame);
|
|
51
|
+
/* SAFETY: the iframe was just appended, so its contentWindow exists. */
|
|
51
52
|
const bridge = bridgeToWindow(frame.contentWindow, {
|
|
52
53
|
targetOrigin: hostOrigin,
|
|
53
54
|
});
|
package/dist/sdk/client/idb.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const idb: {
|
|
2
2
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
3
|
-
set(key: string, value:
|
|
4
|
-
delete(key: string): Promise<
|
|
3
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
4
|
+
delete(key: string): Promise<void>;
|
|
5
5
|
};
|
package/dist/sdk/client/idb.js
CHANGED
|
@@ -16,10 +16,25 @@ function createStore(dbName, storeName) {
|
|
|
16
16
|
};
|
|
17
17
|
return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* `IDBRequest` settles on `success`/`error` and `IDBTransaction` on `complete`/`abort`, and only
|
|
21
|
+
* the request carries `result`. Both are handled with one promise by reading the members each
|
|
22
|
+
* type actually declares, rather than casting the union away.
|
|
23
|
+
*/
|
|
19
24
|
function promisify(request) {
|
|
20
25
|
return new Promise((resolve, reject) => {
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
const settleError = () => reject(request.error);
|
|
27
|
+
if (request instanceof IDBTransaction) {
|
|
28
|
+
/* SAFETY: a transaction has no result; callers of the transaction overload discard it. */
|
|
29
|
+
request.oncomplete = () => resolve(undefined);
|
|
30
|
+
request.onabort = settleError;
|
|
31
|
+
request.onerror = settleError;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
/* SAFETY: IndexedDB stores structured clones with no schema of its own, so the caller names
|
|
35
|
+
the type it previously stored under this key. */
|
|
36
|
+
request.onsuccess = () => resolve(request.result);
|
|
37
|
+
request.onerror = settleError;
|
|
23
38
|
});
|
|
24
39
|
}
|
|
25
40
|
export const idb = {
|
|
@@ -28,7 +28,7 @@ const OVERLAY_STYLE = {
|
|
|
28
28
|
* callers don't thread the instance through. No-op under SSR.
|
|
29
29
|
*/
|
|
30
30
|
export function createProfileButton(options) {
|
|
31
|
-
if (
|
|
31
|
+
if (!("document" in globalThis))
|
|
32
32
|
return { setTheme() { }, destroy() { } };
|
|
33
33
|
const flow = options.flow ?? getFlow() ?? requireFlow();
|
|
34
34
|
const host = resolveIdHost(options.host ?? flow.host);
|
package/dist/sdk/client/rooms.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Every rooms endpoint answers a failure as `{ error, reason? }`. */
|
|
3
|
+
const failureSchema = z.object({
|
|
4
|
+
error: z.string().optional(),
|
|
5
|
+
reason: z.string().optional(),
|
|
6
|
+
});
|
|
1
7
|
/** Thrown on any non-2xx rooms response; `reason` carries the machine-readable
|
|
2
8
|
* cause when the API provides one (e.g. "banned", "private"). */
|
|
3
9
|
export class RoomsRequestError extends Error {
|
|
@@ -31,12 +37,15 @@ export function createRoomsApi(host, getToken) {
|
|
|
31
37
|
headers,
|
|
32
38
|
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
33
39
|
});
|
|
34
|
-
const payload =
|
|
40
|
+
const payload = await res.json().catch(() => ({}));
|
|
35
41
|
if (!res.ok) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
const failure = failureSchema.safeParse(payload);
|
|
43
|
+
throw new RoomsRequestError(res.status, failure.success && failure.data.error
|
|
44
|
+
? failure.data.error
|
|
45
|
+
: `Rooms request failed (${res.status})`, failure.success ? failure.data.reason : undefined);
|
|
39
46
|
}
|
|
47
|
+
/* SAFETY: each caller names the response type of the rooms endpoint it just called; a
|
|
48
|
+
non-ok status has already thrown above with the server's own wording. */
|
|
40
49
|
return payload;
|
|
41
50
|
}
|
|
42
51
|
const slugPath = (slug) => `/${encodeURIComponent(slug)}`;
|
|
@@ -82,5 +91,11 @@ export function createRoomsApi(host, getToken) {
|
|
|
82
91
|
unban: (slug, userId) => act(slug, "unban", { userId }),
|
|
83
92
|
mute: (slug, userId, options) => act(slug, "mute", { userId, ...options }),
|
|
84
93
|
unmute: (slug, userId) => act(slug, "unmute", { userId }),
|
|
94
|
+
timeout: (slug, userId, options) => act(slug, "timeout", {
|
|
95
|
+
userId,
|
|
96
|
+
seconds: options.seconds,
|
|
97
|
+
reason: options.reason,
|
|
98
|
+
}),
|
|
99
|
+
removeTimeout: (slug, userId) => act(slug, "timeout/remove", { userId }),
|
|
85
100
|
};
|
|
86
101
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as OxAddress from "ox/Address";
|
|
2
2
|
import * as PublicKey from "ox/PublicKey";
|
|
3
|
+
import { hex0x } from "../hex";
|
|
3
4
|
import { idb } from "./idb";
|
|
4
5
|
import { METHODS } from "./methods";
|
|
5
6
|
import { initialFlowState } from "./store";
|
|
@@ -32,11 +33,12 @@ export async function runLogin(params) {
|
|
|
32
33
|
const { dialog, store, options, extraCapabilities } = params;
|
|
33
34
|
const signUp = Boolean(options?.signUp);
|
|
34
35
|
const capabilities = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
createAccount: signUp ? true : undefined,
|
|
37
|
+
signIn: !signUp && options?.signIn ? true : undefined,
|
|
38
|
+
signInHeadless: !signUp && options?.signInHeadless ? true : undefined,
|
|
38
39
|
...(extraCapabilities ?? {}),
|
|
39
40
|
};
|
|
41
|
+
/* SAFETY: the connect method's response shape, which the dialog route answers. */
|
|
40
42
|
const result = (await dialog.request(METHODS.connect, [
|
|
41
43
|
{ capabilities },
|
|
42
44
|
]));
|
|
@@ -52,7 +54,7 @@ export async function runLogin(params) {
|
|
|
52
54
|
session: { user, jwt, credential, address },
|
|
53
55
|
refreshToken,
|
|
54
56
|
webauthn,
|
|
55
|
-
|
|
57
|
+
additionalSessions: additionalSessions ? additionalSessions : undefined,
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
@@ -80,6 +82,7 @@ export async function runLogout(store) {
|
|
|
80
82
|
* the credential.
|
|
81
83
|
*/
|
|
82
84
|
export function credentialToAddress(credential) {
|
|
83
|
-
const pub = PublicKey.from(
|
|
85
|
+
const pub = PublicKey.from(hex0x(credential.publicKey));
|
|
86
|
+
// SAFETY: ox brands its Address; Flow's Address is the same checksummed 0x-string.
|
|
84
87
|
return OxAddress.fromPublicKey(pub);
|
|
85
88
|
}
|
|
@@ -23,7 +23,11 @@ function requireAuth(state) {
|
|
|
23
23
|
* background signing only ever uses the access key.
|
|
24
24
|
*/
|
|
25
25
|
async function resolveAccount(params) {
|
|
26
|
-
|
|
26
|
+
/* SAFETY: tempo types its account constructors against porto's own credential and options
|
|
27
|
+
types, which Flow's equivalents mirror field-for-field but are not nominally the same. */
|
|
28
|
+
const rootAccount = Account.fromWebAuthnP256(params.credential, {
|
|
29
|
+
rpId: params.rpId,
|
|
30
|
+
});
|
|
27
31
|
const stored = await loadAccessKey(params.address);
|
|
28
32
|
if (!stored)
|
|
29
33
|
return rootAccount;
|
|
@@ -51,6 +55,8 @@ async function resolveAccount(params) {
|
|
|
51
55
|
function withAccessKeyAuthorization(chain, address) {
|
|
52
56
|
const inheritedPrepare = chain.prepareTransactionRequest;
|
|
53
57
|
const hook = async (args, { phase }) => {
|
|
58
|
+
/* SAFETY: viem's parameter type is a wide union; PrepareArgsWithAuth names only the
|
|
59
|
+
key-authorization field this hook reads and writes. */
|
|
54
60
|
const argsWithAuth = args;
|
|
55
61
|
// Preserve auth threaded by an earlier phase invocation; otherwise
|
|
56
62
|
// consume one from IDB. Atomic read+clear so re-runs don't
|
|
@@ -61,11 +67,14 @@ function withAccessKeyAuthorization(chain, address) {
|
|
|
61
67
|
// (Tempo's chainConfig adds gas adjustments based on signature type
|
|
62
68
|
// and handles expiring nonces — we'd break those without this).
|
|
63
69
|
const inheritedResult = await runInheritedPrepare(inheritedPrepare, args, phase);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
...
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
const prepared = { ...args, ...inheritedResult };
|
|
71
|
+
const authorized = keyAuthorization
|
|
72
|
+
? { ...prepared, keyAuthorization }
|
|
73
|
+
: prepared;
|
|
74
|
+
/* SAFETY: this is the caller's own request plus the inherited fields and, when the account
|
|
75
|
+
authorized one, the key authorization. viem's parameter type is a 30-way union that its
|
|
76
|
+
own spread cannot reconstruct, so the shape is named here once. */
|
|
77
|
+
return authorized;
|
|
69
78
|
};
|
|
70
79
|
return defineChain({
|
|
71
80
|
...chain,
|
|
@@ -84,7 +93,7 @@ function withAccessKeyAuthorization(chain, address) {
|
|
|
84
93
|
async function runInheritedPrepare(inherited, args, phase) {
|
|
85
94
|
if (!inherited)
|
|
86
95
|
return {};
|
|
87
|
-
const [fn, options] =
|
|
96
|
+
const [fn, options] = inherited instanceof Function ? [inherited, undefined] : inherited;
|
|
88
97
|
if (!fn)
|
|
89
98
|
return {};
|
|
90
99
|
if (options && !options.runAt.includes(phase))
|
|
@@ -113,6 +122,7 @@ export async function buildWalletClient(ctx, chainId) {
|
|
|
113
122
|
return createWalletClient({
|
|
114
123
|
account,
|
|
115
124
|
chain: withAccessKeyAuthorization(chain, address),
|
|
125
|
+
// SAFETY: the compat wrapper only reads `account`; its options type is viem-internal.
|
|
116
126
|
transport: walletNamespaceCompat(transport, {
|
|
117
127
|
account,
|
|
118
128
|
}),
|
|
@@ -128,6 +138,8 @@ export async function signMessage(ctx, args) {
|
|
|
128
138
|
export async function signTypedData(ctx, args) {
|
|
129
139
|
const { chainId, ...rest } = args;
|
|
130
140
|
const client = await buildWalletClient(ctx, chainId);
|
|
141
|
+
/* SAFETY: viem's parameter type is generic over chain and account, which this wrapper
|
|
142
|
+
resolves at runtime; the fields spread below are the caller's own validated args. */
|
|
131
143
|
return viemSignTypedData(client, {
|
|
132
144
|
account: client.account,
|
|
133
145
|
...rest,
|
|
@@ -136,6 +148,8 @@ export async function signTypedData(ctx, args) {
|
|
|
136
148
|
export async function sendTransaction(ctx, args) {
|
|
137
149
|
const { chainId, ...rest } = args;
|
|
138
150
|
const client = await buildWalletClient(ctx, chainId);
|
|
151
|
+
/* SAFETY: viem's parameter type is generic over chain and account, which this wrapper
|
|
152
|
+
resolves at runtime; the fields spread below are the caller's own validated args. */
|
|
139
153
|
return viemSendTransaction(client, {
|
|
140
154
|
account: client.account,
|
|
141
155
|
chain: client.chain,
|
|
@@ -145,6 +159,7 @@ export async function sendTransaction(ctx, args) {
|
|
|
145
159
|
export async function sendCalls(ctx, args) {
|
|
146
160
|
const { chainId, ...rest } = args;
|
|
147
161
|
const client = await buildWalletClient(ctx, chainId);
|
|
162
|
+
// SAFETY: viem's generic parameter type, resolved at runtime.
|
|
148
163
|
const result = await viemSendCalls(client, {
|
|
149
164
|
account: client.account,
|
|
150
165
|
chain: client.chain,
|
package/dist/sdk/client/store.js
CHANGED
|
@@ -25,15 +25,14 @@ export const initialFlowState = {
|
|
|
25
25
|
credential: null,
|
|
26
26
|
address: null,
|
|
27
27
|
};
|
|
28
|
+
/** `Object(x) === x` holds only for objects, so this rules out primitives without a `typeof`. */
|
|
29
|
+
const isObject = (value) => Object(value) === value;
|
|
28
30
|
function shallowEqual(a, b) {
|
|
29
31
|
if (a === b)
|
|
30
32
|
return true;
|
|
31
|
-
if (
|
|
32
|
-
typeof b !== "object" ||
|
|
33
|
-
a === null ||
|
|
34
|
-
b === null) {
|
|
33
|
+
if (!isObject(a) || !isObject(b))
|
|
35
34
|
return false;
|
|
36
|
-
|
|
35
|
+
/* SAFETY: `a` was just narrowed to an object, so its own keys are keys of T. */
|
|
37
36
|
const ak = Object.keys(a);
|
|
38
37
|
if (Object.keys(b).length !== ak.length)
|
|
39
38
|
return false;
|
package/dist/sdk/cookies.d.ts
CHANGED
|
@@ -30,5 +30,7 @@ export declare function serializeCookie(name: string, value: string, opts: {
|
|
|
30
30
|
export declare function clearCookieString(name: string, opts: {
|
|
31
31
|
secure: boolean;
|
|
32
32
|
}): string;
|
|
33
|
+
/** Cookie names to their values, as parsed off a request. */
|
|
34
|
+
export type CookieJar = Record<string, string>;
|
|
33
35
|
/** Parses a Cookie request header into name → value; first occurrence wins (RFC 6265 practice). */
|
|
34
|
-
export declare function parseCookieHeader(header: string | null | undefined):
|
|
36
|
+
export declare function parseCookieHeader(header: string | null | undefined): CookieJar;
|
package/dist/sdk/cookies.js
CHANGED
|
@@ -49,17 +49,19 @@ export function clearCookieString(name, opts) {
|
|
|
49
49
|
}
|
|
50
50
|
/** Parses a Cookie request header into name → value; first occurrence wins (RFC 6265 practice). */
|
|
51
51
|
export function parseCookieHeader(header) {
|
|
52
|
-
const out = {};
|
|
53
52
|
if (!header)
|
|
54
|
-
return
|
|
55
|
-
|
|
53
|
+
return {};
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const entries = header.split(";").flatMap((part) => {
|
|
56
56
|
const eq = part.indexOf("=");
|
|
57
57
|
if (eq === -1)
|
|
58
|
-
|
|
58
|
+
return [];
|
|
59
59
|
const name = part.slice(0, eq).trim();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
// First occurrence wins (RFC 6265 practice), so a repeat is dropped.
|
|
61
|
+
if (!name || seen.has(name))
|
|
62
|
+
return [];
|
|
63
|
+
seen.add(name);
|
|
64
|
+
return [[name, part.slice(eq + 1).trim()]];
|
|
65
|
+
});
|
|
66
|
+
return Object.fromEntries(entries);
|
|
65
67
|
}
|
|
@@ -20,9 +20,4 @@ export declare function fromWindow(w: Window, options?: FromWindowOptions): Mess
|
|
|
20
20
|
* dropped against an iframe that hasn't booted yet.
|
|
21
21
|
*/
|
|
22
22
|
export declare function bridge(parameters: BridgeParameters): Bridge;
|
|
23
|
-
/**
|
|
24
|
-
* No-op bridge used in non-browser environments (SSR, tests, Node) where
|
|
25
|
-
* `window` doesn't exist. All sends resolve to undefined and never deliver.
|
|
26
|
-
* Lets the SDK be imported from anywhere without crashing on module load.
|
|
27
|
-
*/
|
|
28
23
|
export declare function noop(): Bridge;
|
|
@@ -6,22 +6,32 @@
|
|
|
6
6
|
* object with a method on it would throw a DataCloneError at runtime.
|
|
7
7
|
*/
|
|
8
8
|
function normalizeValue(value) {
|
|
9
|
-
|
|
9
|
+
/* The `as never`s below are all the same thing — this walks a value structurally and returns
|
|
10
|
+
a differently-shaped one (a stripped clone, or `undefined` for what cannot be cloned),
|
|
11
|
+
which a `<type> -> <type>` signature cannot express. */
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
// SAFETY: the walked copy replaces the array element-for-element.
|
|
10
14
|
return value.map(normalizeValue);
|
|
11
|
-
|
|
15
|
+
}
|
|
16
|
+
if (value instanceof Function) {
|
|
17
|
+
// SAFETY: a function cannot be structured-cloned, so it is dropped.
|
|
12
18
|
return undefined;
|
|
13
|
-
|
|
19
|
+
}
|
|
20
|
+
if (value === null || Object(value) !== value)
|
|
14
21
|
return value;
|
|
15
22
|
if (Object.getPrototypeOf(value) !== Object.prototype)
|
|
16
23
|
try {
|
|
17
24
|
return structuredClone(value);
|
|
18
25
|
}
|
|
19
26
|
catch {
|
|
27
|
+
// SAFETY: a value even structuredClone refuses is dropped.
|
|
20
28
|
return undefined;
|
|
21
29
|
}
|
|
22
30
|
const normalized = {};
|
|
23
|
-
for (const [k, v] of Object.entries(value))
|
|
31
|
+
for (const [k, v] of Object.entries(Object(value))) {
|
|
32
|
+
// SAFETY: the walked value is a structured-clonable primitive or plain object.
|
|
24
33
|
normalized[k] = normalizeValue(v);
|
|
34
|
+
}
|
|
25
35
|
return normalized;
|
|
26
36
|
}
|
|
27
37
|
export function from(messenger) {
|
|
@@ -65,11 +75,15 @@ export function fromWindow(w, options = {}) {
|
|
|
65
75
|
async send(topic, payload, target) {
|
|
66
76
|
const id = crypto.randomUUID();
|
|
67
77
|
w.postMessage(normalizeValue({ id, payload, topic }), target ?? targetOrigin ?? "*");
|
|
78
|
+
/* SAFETY: Bridge types `send`'s result from the topic's response map; the envelope this
|
|
79
|
+
transport resolves with is the request echo, which that map cannot name. */
|
|
68
80
|
return { id, payload, topic };
|
|
69
81
|
},
|
|
70
82
|
async sendAsync(topic, payload, target) {
|
|
71
83
|
const { id } = await this.send(topic, payload, target);
|
|
72
|
-
return new Promise((resolve) =>
|
|
84
|
+
return new Promise((resolve) =>
|
|
85
|
+
// SAFETY: `topic` is the same topic this method was called with.
|
|
86
|
+
this.on(topic, resolve, id));
|
|
73
87
|
},
|
|
74
88
|
});
|
|
75
89
|
}
|
|
@@ -128,6 +142,10 @@ export function bridge(parameters) {
|
|
|
128
142
|
* `window` doesn't exist. All sends resolve to undefined and never deliver.
|
|
129
143
|
* Lets the SDK be imported from anywhere without crashing on module load.
|
|
130
144
|
*/
|
|
145
|
+
/* Off-browser there is no peer to answer, so every send resolves to nothing while still
|
|
146
|
+
satisfying Bridge's per-topic response types. */
|
|
147
|
+
// SAFETY: the no-op's contract — nothing is ever delivered or answered.
|
|
148
|
+
const settle = () => Promise.resolve(undefined);
|
|
131
149
|
export function noop() {
|
|
132
150
|
return {
|
|
133
151
|
destroy() { },
|
|
@@ -135,14 +153,8 @@ export function noop() {
|
|
|
135
153
|
return () => { };
|
|
136
154
|
},
|
|
137
155
|
ready() { },
|
|
138
|
-
send
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
sendAsync() {
|
|
142
|
-
return Promise.resolve(undefined);
|
|
143
|
-
},
|
|
144
|
-
waitForReady() {
|
|
145
|
-
return Promise.resolve(undefined);
|
|
146
|
-
},
|
|
156
|
+
send: settle,
|
|
157
|
+
sendAsync: settle,
|
|
158
|
+
waitForReady: settle,
|
|
147
159
|
};
|
|
148
160
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres driver errors are not typed by the driver, and the wrapper nests differently across
|
|
3
|
+
* postgres-js and PGlite. These read the fields we branch on without asserting a shape onto the
|
|
4
|
+
* error, and return "" / undefined when the field is absent.
|
|
5
|
+
*/
|
|
6
|
+
/** Reads a named string field off a driver error. */
|
|
7
|
+
export declare function errField(cause: unknown, key: string): string;
|
|
8
|
+
export declare function errCause(cause: unknown): object | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres driver errors are not typed by the driver, and the wrapper nests differently across
|
|
3
|
+
* postgres-js and PGlite. These read the fields we branch on without asserting a shape onto the
|
|
4
|
+
* error, and return "" / undefined when the field is absent.
|
|
5
|
+
*/
|
|
6
|
+
/** Reads a named string field off a driver error. */
|
|
7
|
+
export function errField(cause, key) {
|
|
8
|
+
const value = Object.entries(Object(cause)).find(([k]) => k === key)?.[1];
|
|
9
|
+
return String(value) === value ? value : "";
|
|
10
|
+
}
|
|
11
|
+
/** The nested driver error, when the wrapper carries one. */
|
|
12
|
+
/** `Object(x) === x` holds only for objects, so this tests without narrowing a representation. */
|
|
13
|
+
const isObject = (cause) => Object(cause) === cause;
|
|
14
|
+
export function errCause(cause) {
|
|
15
|
+
const value = Object.entries(Object(cause)).find(([k]) => k === "cause")?.[1];
|
|
16
|
+
return isObject(value) ? value : undefined;
|
|
17
|
+
}
|