@flow-industries/id 0.14.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.
@@ -393,7 +393,13 @@ export function createFlow(options = {}) {
393
393
  });
394
394
  await installSession(refreshToken, session.jwt);
395
395
  deliverAdditionalSessions(additionalSessions);
396
- if (accessKeyModule && accessKeyPrep && webauthn) {
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) {
397
403
  await accessKeyModule.finalizeAccessKey({
398
404
  address: session.address,
399
405
  credential: session.credential,
@@ -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
- const address = credentialToAddress(credential);
45
- await idb.set(CREDENTIAL_KEY, credential);
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 };
@@ -30,7 +30,8 @@ export type AuthResponse = {
30
30
  jwt: string;
31
31
  refreshToken: string;
32
32
  user: FlowUser;
33
- credential: FlowCredential;
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_epoch_stale" | "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
- credential: FlowCredential;
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
  };
@@ -140,8 +140,12 @@ export type LoginOptions = {
140
140
  export type Session = {
141
141
  user: FlowUser;
142
142
  jwt: string;
143
- credential: FlowCredential;
144
- address: Address;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "main": "./dist/sdk/client/index.js",
5
5
  "module": "./dist/sdk/client/index.js",
6
6
  "types": "./dist/sdk/client/index.d.ts",