@flow-industries/id 0.13.0 → 0.14.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.
@@ -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
@@ -436,6 +460,7 @@ export function createFlow(options = {}) {
436
460
  ensureGuest,
437
461
  refreshJwt,
438
462
  getToken,
463
+ mintAudience,
439
464
  rooms: createRoomsApi(host, getToken),
440
465
  signMessage: async (args) => {
441
466
  const mod = await import("./signing");
@@ -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" ||
@@ -9,7 +9,7 @@ export type AuthMode = "sign-up" | "sign-in";
9
9
  /** Client-only funnel steps reported via the `/api/events` beacon. */
10
10
  export type FunnelStep = "mode_selected" | "email_entered" | "ceremony_started" | "done_shown";
11
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";
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";
13
13
  /** One flat record per event = one row in the `auth_events` stream. */
14
14
  export interface AuthEventRecord {
15
15
  service: "auth";
@@ -250,6 +250,17 @@ export type Flow = {
250
250
  * Prefer this over reading `jwt` directly when calling a backend.
251
251
  */
252
252
  getToken(): Promise<string | null>;
253
+ /**
254
+ * Silently obtains a session for an ADDITIONAL audience (e.g. an in-game
255
+ * chat embed's `talk.flow.game`) authorized by this app's primary cookie,
256
+ * for the consumer to install into that audience's own origin. Returns null
257
+ * when there is no primary session or the audience isn't provisioned for this
258
+ * app. Nothing is minted unless called — a consumer calls it only when its
259
+ * embed reports it has no session, so an audience that already holds a live
260
+ * cookie is never re-minted. This is the reconcile path that heals a
261
+ * returning/guest user whose extra-audience cookie is missing or was retired.
262
+ */
263
+ mintAudience(audience: string): Promise<AdditionalSession | null>;
253
264
  signMessage(args: SignMessageArgs): Promise<Hex>;
254
265
  signTypedData(args: SignTypedDataArgs): Promise<Hex>;
255
266
  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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flow-industries/id",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",