@flow-industries/id 0.13.0 → 0.15.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/sdk/client/create-flow.js +32 -1
- package/dist/sdk/client/session.js +6 -2
- package/dist/sdk/client/signing.js +5 -0
- package/dist/sdk/client/static-flow.js +1 -0
- package/dist/sdk/session-route.js +29 -0
- package/dist/sdk/types/auth.d.ts +2 -1
- package/dist/sdk/types/events.d.ts +3 -3
- package/dist/sdk/types/protocol.d.ts +2 -1
- package/dist/sdk/types/sdk.d.ts +17 -2
- package/dist/sdk/types/server.d.ts +7 -1
- package/package.json +1 -1
|
@@ -151,6 +151,30 @@ export function createFlow(options = {}) {
|
|
|
151
151
|
return false;
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Silently mints a session for an additional audience via this app's own
|
|
156
|
+
* session route (which authorizes it with the primary refresh cookie and
|
|
157
|
+
* proxies to the issuer). The consumer calls this only when its embed for
|
|
158
|
+
* `audience` reports it has no session, so nothing is minted for an audience
|
|
159
|
+
* that already holds a live cookie. Returns null when there's no primary
|
|
160
|
+
* session or the audience isn't provisioned/allowed for this app.
|
|
161
|
+
*/
|
|
162
|
+
async function mintAudience(audience) {
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(sessionPath, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify({ mint: [audience] }),
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok)
|
|
170
|
+
return null;
|
|
171
|
+
const { additionalSessions } = (await res.json());
|
|
172
|
+
return additionalSessions?.find((s) => s.audience === audience) ?? null;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
154
178
|
/**
|
|
155
179
|
* One-time delivery of any additional-audience sessions a mint returned
|
|
156
180
|
* (AUTH-39). Handed straight to the consumer's callback and dropped — the
|
|
@@ -369,7 +393,13 @@ export function createFlow(options = {}) {
|
|
|
369
393
|
});
|
|
370
394
|
await installSession(refreshToken, session.jwt);
|
|
371
395
|
deliverAdditionalSessions(additionalSessions);
|
|
372
|
-
|
|
396
|
+
// webauthn implies a passkey ceremony ran, but the types allow an
|
|
397
|
+
// email-only session — finalize only with a real credential/address.
|
|
398
|
+
if (accessKeyModule &&
|
|
399
|
+
accessKeyPrep &&
|
|
400
|
+
webauthn &&
|
|
401
|
+
session.credential &&
|
|
402
|
+
session.address) {
|
|
373
403
|
await accessKeyModule.finalizeAccessKey({
|
|
374
404
|
address: session.address,
|
|
375
405
|
credential: session.credential,
|
|
@@ -436,6 +466,7 @@ export function createFlow(options = {}) {
|
|
|
436
466
|
ensureGuest,
|
|
437
467
|
refreshJwt,
|
|
438
468
|
getToken,
|
|
469
|
+
mintAudience,
|
|
439
470
|
rooms: createRoomsApi(host, getToken),
|
|
440
471
|
signMessage: async (args) => {
|
|
441
472
|
const mod = await import("./signing");
|
|
@@ -41,8 +41,12 @@ export async function runLogin(params) {
|
|
|
41
41
|
{ capabilities },
|
|
42
42
|
]));
|
|
43
43
|
const { jwt, refreshToken, user, credential, webauthn, additionalSessions } = result;
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// An email-only login carries no credential — mirror the guest handling:
|
|
45
|
+
// nothing is persisted to IDB (reserved for a real passkey credential) and
|
|
46
|
+
// the address stays null until a passkey is added.
|
|
47
|
+
const address = credential ? credentialToAddress(credential) : null;
|
|
48
|
+
if (credential)
|
|
49
|
+
await idb.set(CREDENTIAL_KEY, credential);
|
|
46
50
|
store.setState({ user, jwt, credential, address });
|
|
47
51
|
return {
|
|
48
52
|
session: { user, jwt, credential, address },
|
|
@@ -4,6 +4,11 @@ import { Account, walletNamespaceCompat } from "viem/tempo";
|
|
|
4
4
|
import { buildAccessKeyAccount, clearAccessKey, consumePendingAuthorization, isExpired, loadAccessKey, } from "./access-key";
|
|
5
5
|
function requireAuth(state) {
|
|
6
6
|
if (!state.credential || !state.address) {
|
|
7
|
+
// An email-only or guest session is authenticated but has no passkey to
|
|
8
|
+
// sign with — distinguish that from being signed out entirely.
|
|
9
|
+
if (state.user) {
|
|
10
|
+
throw new Error("This session has no passkey — add one to your account to sign");
|
|
11
|
+
}
|
|
7
12
|
throw new Error("Not authenticated — call flow.login() first");
|
|
8
13
|
}
|
|
9
14
|
return { credential: state.credential, address: state.address };
|
|
@@ -49,6 +49,7 @@ export function createStaticFlow(state, options = {}) {
|
|
|
49
49
|
ensureGuest: () => unavailable("ensureGuest"),
|
|
50
50
|
refreshJwt: async () => false,
|
|
51
51
|
getToken,
|
|
52
|
+
mintAudience: async () => null,
|
|
52
53
|
rooms: createRoomsApi(host, getToken),
|
|
53
54
|
signMessage: () => unavailable("signMessage"),
|
|
54
55
|
signTypedData: () => unavailable("signTypedData"),
|
|
@@ -66,6 +66,35 @@ export async function handleSessionRequest(request, opts = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
case "POST": {
|
|
68
68
|
const body = (await request.json().catch(() => null));
|
|
69
|
+
// Reconcile mint: the browser asks its own server to obtain additional-
|
|
70
|
+
// audience sessions (e.g. talk.flow.game) authorized by the PRIMARY
|
|
71
|
+
// refresh cookie, for an embed that booted with no session of its own.
|
|
72
|
+
// The refresh token never touches page JS — it's read from the HttpOnly
|
|
73
|
+
// cookie here and presented to the issuer as a bearer.
|
|
74
|
+
if (Array.isArray(body?.mint)) {
|
|
75
|
+
const audiences = body.mint.filter((a) => typeof a === "string");
|
|
76
|
+
const names = cookieNamesFor(audience);
|
|
77
|
+
const token = parseCookieHeader(request.headers.get("Cookie"))[names.refresh];
|
|
78
|
+
let additionalSessions = [];
|
|
79
|
+
if (token && audiences.length > 0) {
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(`${issuerUrl}/api/session/additional`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
Authorization: `Bearer ${token}`,
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify({ audiences }),
|
|
88
|
+
});
|
|
89
|
+
if (res.ok) {
|
|
90
|
+
const data = (await res.json());
|
|
91
|
+
additionalSessions = data.additionalSessions ?? [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
}
|
|
96
|
+
return json({ state: null, additionalSessions }, 200);
|
|
97
|
+
}
|
|
69
98
|
const refreshToken = body?.refreshToken;
|
|
70
99
|
const jwt = body?.jwt;
|
|
71
100
|
if (typeof refreshToken !== "string" ||
|
package/dist/sdk/types/auth.d.ts
CHANGED
|
@@ -30,7 +30,8 @@ export type AuthResponse = {
|
|
|
30
30
|
jwt: string;
|
|
31
31
|
refreshToken: string;
|
|
32
32
|
user: FlowUser;
|
|
33
|
-
|
|
33
|
+
/** Null for an email-only session (no passkey on the account yet). */
|
|
34
|
+
credential: FlowCredential | null;
|
|
34
35
|
additionalSessions?: AdditionalSession[];
|
|
35
36
|
};
|
|
36
37
|
export type AuthConfig = {
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
export type AuthOutcome = "success" | "failure" | "info";
|
|
8
8
|
export type AuthMode = "sign-up" | "sign-in";
|
|
9
9
|
/** Client-only funnel steps reported via the `/api/events` beacon. */
|
|
10
|
-
export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "done_shown";
|
|
11
|
-
export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.refresh.grace" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.audience.rejected" | "auth.audience.minted" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.done_shown";
|
|
12
|
-
export type AuthErrorCode = "username_taken" | "credential_taken" | "email_taken" | "email_not_verified" | "no_email" | "image_upload_forbidden" | "image_upload_rate_limited" | "invalid_image_type" | "image_too_large" | "image_upload_failed" | "otp_invalid" | "otp_expired" | "otp_attempts_exceeded" | "otp_resend_cooldown" | "otp_resend_limit" | "otp_global_limit" | "otp_send_failed" | "challenge_expired" | "unknown_credential" | "invalid_assertion_type" | "invalid_assertion_origin" | "user_verification_required" | "invalid_signature" | "user_not_found" | "missing_audience" | "audience_not_allowed" | "audience_mismatch" | "no_session" | "no_passkey" | "refresh_token_invalid" | "refresh_token_expired" | "refresh_reuse_detected" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "internal_error";
|
|
10
|
+
export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "ceremony_failed" | "done_shown";
|
|
11
|
+
export type AuthEventName = "auth.username.checked" | "auth.signup.succeeded" | "auth.signup.failed" | "auth.otp.sent" | "auth.otp.verified" | "auth.otp.failed" | "auth.email.verify.sent" | "auth.email.verify.succeeded" | "auth.email.verify.failed" | "auth.email.changed" | "auth.avatar.upload.succeeded" | "auth.avatar.upload.failed" | "auth.challenge.issued" | "auth.signin.succeeded" | "auth.signin.failed" | "auth.passkey.added" | "auth.passkey.add_failed" | "auth.restore.succeeded" | "auth.restore.failed" | "auth.restore.no_session" | "auth.refresh.succeeded" | "auth.refresh.failed" | "auth.refresh.reuse" | "auth.refresh.grace" | "auth.jwt.verified" | "auth.jwt.rejected" | "auth.signout" | "auth.audience.rejected" | "auth.audience.minted" | "auth.guest.created" | "auth.guest.restored" | "auth.guest.upgraded" | "auth.guest.failed" | "auth.funnel.mode_selected" | "auth.funnel.email_entered" | "auth.funnel.ceremony_started" | "auth.funnel.ceremony_failed" | "auth.funnel.done_shown";
|
|
12
|
+
export type AuthErrorCode = "username_taken" | "credential_taken" | "email_taken" | "email_not_verified" | "no_email" | "image_upload_forbidden" | "image_upload_rate_limited" | "invalid_image_type" | "image_too_large" | "image_upload_failed" | "otp_invalid" | "otp_expired" | "otp_attempts_exceeded" | "otp_resend_cooldown" | "otp_resend_limit" | "otp_global_limit" | "otp_send_failed" | "challenge_expired" | "unknown_credential" | "invalid_assertion_type" | "invalid_assertion_origin" | "user_verification_required" | "invalid_signature" | "user_not_found" | "missing_audience" | "audience_not_allowed" | "audience_mismatch" | "no_session" | "no_passkey" | "refresh_token_invalid" | "refresh_token_expired" | "refresh_epoch_stale" | "refresh_reuse_detected" | "guest_rate_limited" | "guest_global_limit" | "guest_username_exhausted" | "guest_session" | "passkey_exists" | "already_upgraded" | "malformed_token" | "unknown_key" | "verification_failed" | "webauthn_not_allowed" | "webauthn_security" | "webauthn_invalid_state" | "webauthn_not_supported" | "webauthn_constraint" | "webauthn_aborted" | "webauthn_unknown" | "internal_error";
|
|
13
13
|
/** One flat record per event = one row in the `auth_events` stream. */
|
|
14
14
|
export interface AuthEventRecord {
|
|
15
15
|
service: "auth";
|
|
@@ -29,7 +29,8 @@ export type ConnectResponse = {
|
|
|
29
29
|
/** First-party rotating refresh token; the SDK persists it in the app origin. */
|
|
30
30
|
refreshToken: string;
|
|
31
31
|
user: FlowUser;
|
|
32
|
-
|
|
32
|
+
/** Null when the user registered or signed in with email only (no passkey). */
|
|
33
|
+
credential: FlowCredential | null;
|
|
33
34
|
webauthn?: WebAuthnSignature;
|
|
34
35
|
additionalSessions?: AdditionalSession[];
|
|
35
36
|
};
|
package/dist/sdk/types/sdk.d.ts
CHANGED
|
@@ -140,8 +140,12 @@ export type LoginOptions = {
|
|
|
140
140
|
export type Session = {
|
|
141
141
|
user: FlowUser;
|
|
142
142
|
jwt: string;
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Null for an email-only session (registered or signed in without a
|
|
145
|
+
* passkey). Signing requires a credential — gate wallet UI on its presence.
|
|
146
|
+
*/
|
|
147
|
+
credential: FlowCredential | null;
|
|
148
|
+
address: Address | null;
|
|
145
149
|
};
|
|
146
150
|
export type RootCredential = {
|
|
147
151
|
credential: FlowCredential;
|
|
@@ -250,6 +254,17 @@ export type Flow = {
|
|
|
250
254
|
* Prefer this over reading `jwt` directly when calling a backend.
|
|
251
255
|
*/
|
|
252
256
|
getToken(): Promise<string | null>;
|
|
257
|
+
/**
|
|
258
|
+
* Silently obtains a session for an ADDITIONAL audience (e.g. an in-game
|
|
259
|
+
* chat embed's `talk.flow.game`) authorized by this app's primary cookie,
|
|
260
|
+
* for the consumer to install into that audience's own origin. Returns null
|
|
261
|
+
* when there is no primary session or the audience isn't provisioned for this
|
|
262
|
+
* app. Nothing is minted unless called — a consumer calls it only when its
|
|
263
|
+
* embed reports it has no session, so an audience that already holds a live
|
|
264
|
+
* cookie is never re-minted. This is the reconcile path that heals a
|
|
265
|
+
* returning/guest user whose extra-audience cookie is missing or was retired.
|
|
266
|
+
*/
|
|
267
|
+
mintAudience(audience: string): Promise<AdditionalSession | null>;
|
|
253
268
|
signMessage(args: SignMessageArgs): Promise<Hex>;
|
|
254
269
|
signTypedData(args: SignTypedDataArgs): Promise<Hex>;
|
|
255
270
|
sendTransaction(args: SendTransactionArgs): Promise<Hex>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlowCredential, FlowUser, VerifiedFlowJWT } from "./auth";
|
|
1
|
+
import type { AdditionalSession, FlowCredential, FlowUser, VerifiedFlowJWT } from "./auth";
|
|
2
2
|
import type { Address, FlowSessionState } from "./sdk";
|
|
3
3
|
import type { PublicProfile } from "./xp";
|
|
4
4
|
export type ResolveSessionOptions = {
|
|
@@ -53,4 +53,10 @@ export type SessionRouteSession = {
|
|
|
53
53
|
};
|
|
54
54
|
export type SessionRouteResponse = {
|
|
55
55
|
state: SessionRouteSession | null;
|
|
56
|
+
/**
|
|
57
|
+
* Additional-audience sessions minted by a reconcile request (POST `{ mint }`)
|
|
58
|
+
* — one per allowed audience the caller asked to provision. The consumer
|
|
59
|
+
* forwards each to its audience's own origin to be sealed HttpOnly there.
|
|
60
|
+
*/
|
|
61
|
+
additionalSessions?: AdditionalSession[];
|
|
56
62
|
};
|