@hosty-sdk/app 0.1.1 → 0.2.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/embedder.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // re-run your normal open flow for that app (which mints a fresh launch code), rate-limited.
7
7
  // The re-open must take the full launch-code path — an "already open → reuse the URL
8
8
  // without a code" optimization must not short-circuit recovery.
9
- import { AUTH_REQUIRED_INTENT_TYPE } from "./index";
9
+ import { AUTH_REQUIRED_INTENT_TYPE } from "./index.js";
10
10
  /**
11
11
  * Verifies that a `message` event is a genuine auth-required intent from the active app
12
12
  * frame. Every check is about facts only the embedder can know:
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AppSessionStatus } from "./index";
1
+ import { type AppSessionStatus } from "./index.js";
2
2
  /**
3
3
  * Tolerant probe reader: accepts both identity-route shapes in the fleet — a top-level
4
4
  * `status` (marketplace/telemetry) and a nested `appSession.status` (demo-app,
package/dist/react.js CHANGED
@@ -4,7 +4,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
4
4
  // cards. Prop-less by design: the app id and browser-reachable Core origin arrive in the
5
5
  // probe response's `recovery` field (request-time values, never build-time props).
6
6
  import { useEffect, useState } from "react";
7
- import { buildCoreOpenUrl, decideRecoveryAction, detectLaunchMode, readRecoveryParams, } from "./index";
7
+ import { buildCoreOpenUrl, decideRecoveryAction, detectLaunchMode, readRecoveryParams, } from "./index.js";
8
8
  // Once-per-tab guard so a standalone app that returns from Core still unauthorized does
9
9
  // not bounce through /open forever. Cleared on a successful code exchange.
10
10
  const RECOVERY_GUARD_KEY = "hosty.auth.recovery-attempted";
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import "server-only";
2
- import { type AppSessionFailureStatus, type AppSessionStatus, type SessionRecoveryParams } from "./index";
2
+ import { type AppSessionFailureStatus, type AppSessionStatus, type SessionRecoveryParams } from "./index.js";
3
3
  /** Per-app parameters: everything that legitimately diverges between apps stays a
4
4
  * parameter — cookie namespaces and role models are per-app by decision, never unified. */
5
5
  export interface HostyAppConfig {
@@ -22,11 +22,21 @@ export interface HostIdentity {
22
22
  appRole: string | null;
23
23
  expiresAt: string | null;
24
24
  }
25
+ /** Structured detail for a failed resolution: the Core HTTP status (null when Core was
26
+ * never reached) plus a machine-readable code and human-readable message. Codes pass
27
+ * through from Core's error body where one exists; classification never branches on them
28
+ * (decision 10) — they exist for diagnostics and API responses. */
29
+ export interface ResolutionError {
30
+ status: number | null;
31
+ code: string;
32
+ message: string;
33
+ }
25
34
  export type AppSessionResolution = {
26
35
  status: "active";
27
36
  identity: HostIdentity;
28
37
  } | {
29
38
  status: AppSessionFailureStatus;
39
+ error?: ResolutionError;
30
40
  };
31
41
  export declare function getAppId(config: HostyAppConfig): string;
32
42
  export declare function getCoreOrigin(): string | null;
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Server slice: Next route-handler factories and Core revalidation. Never reaches a client
2
2
  // bundle — the service token lives here.
3
3
  import "server-only";
4
- import { classifyRevalidationHttpStatus, } from "./index";
4
+ import { classifyRevalidationHttpStatus, } from "./index.js";
5
5
  // Core runs on the same host as the app services, so a short budget is plenty; a stalled
6
6
  // call must not hold a route open indefinitely.
7
7
  const CORE_AUTH_TIMEOUT_MS = 1_500;
@@ -91,7 +91,12 @@ export async function resolveAppSession(token, config) {
91
91
  const serviceToken = getServiceToken();
92
92
  const coreOrigin = getCoreOrigin();
93
93
  if (!serviceToken || !coreOrigin) {
94
- return { status: "misconfigured" };
94
+ return {
95
+ status: "misconfigured",
96
+ error: serviceToken
97
+ ? { status: null, code: "core_origin_missing", message: "HOSTY_CORE_ORIGIN is not configured." }
98
+ : { status: null, code: "app_service_token_missing", message: "HOSTY_APP_SERVICE_TOKEN is not configured." },
99
+ };
95
100
  }
96
101
  const now = Date.now();
97
102
  const cached = revalidationCache.get(token);
@@ -120,7 +125,10 @@ async function revalidateWithCore(token, coreOrigin, serviceToken, config) {
120
125
  endpoint = new URL("/api/auth/apps/revalidate", coreOrigin).toString();
121
126
  }
122
127
  catch {
123
- return { status: "misconfigured" };
128
+ return {
129
+ status: "misconfigured",
130
+ error: { status: null, code: "core_origin_invalid", message: "HOSTY_CORE_ORIGIN is not a valid URL." },
131
+ };
124
132
  }
125
133
  let response;
126
134
  try {
@@ -135,12 +143,28 @@ async function revalidateWithCore(token, coreOrigin, serviceToken, config) {
135
143
  signal: AbortSignal.timeout(CORE_AUTH_TIMEOUT_MS),
136
144
  });
137
145
  }
138
- catch {
146
+ catch (error) {
139
147
  // Network failure or timeout — transient either way; never cached.
140
- return { status: "unavailable" };
148
+ const timeout = error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError");
149
+ return {
150
+ status: "unavailable",
151
+ error: {
152
+ status: null,
153
+ code: timeout ? "core_revalidation_timeout" : "app_session_revalidation_error",
154
+ message: error instanceof Error ? error.message : "Core identity revalidation failed.",
155
+ },
156
+ };
141
157
  }
142
158
  if (!response.ok) {
143
- return { status: classifyRevalidationHttpStatus(response.status) };
159
+ const body = (await response.json().catch(() => null));
160
+ return {
161
+ status: classifyRevalidationHttpStatus(response.status),
162
+ error: {
163
+ status: response.status,
164
+ code: readErrorField(body, "code") ?? "app_session_revalidation_failed",
165
+ message: readErrorField(body, "message") ?? `Core revalidation returned HTTP ${response.status}.`,
166
+ },
167
+ };
144
168
  }
145
169
  let payload;
146
170
  try {
@@ -148,12 +172,18 @@ async function revalidateWithCore(token, coreOrigin, serviceToken, config) {
148
172
  }
149
173
  catch {
150
174
  // A non-JSON / truncated body means the session is unverifiable, not proof it is invalid.
151
- return { status: "unavailable" };
175
+ return {
176
+ status: "unavailable",
177
+ error: { status: response.status, code: "core_response_invalid", message: "Core returned an unreadable revalidation body." },
178
+ };
152
179
  }
153
180
  const appId = readString(payload?.appId);
154
181
  if (appId && appId !== getAppId(config)) {
155
182
  // A token minted for a different app is Core's token_app_mismatch — terminal, like 403.
156
- return { status: "forbidden" };
183
+ return {
184
+ status: "forbidden",
185
+ error: { status: response.status, code: "app_identity_app_mismatch", message: "Identity token was issued for a different app." },
186
+ };
157
187
  }
158
188
  const userId = readString(payload?.userId);
159
189
  // An "active" grant without a subject is unusable; classify it as recoverable so a probe
@@ -329,6 +359,16 @@ function readCookie(cookieHeader, name) {
329
359
  }
330
360
  return null;
331
361
  }
362
+ function readErrorField(payload, field) {
363
+ if (!payload || typeof payload !== "object") {
364
+ return null;
365
+ }
366
+ const nested = payload.error;
367
+ if (nested && typeof nested === "object") {
368
+ return readString(nested[field]);
369
+ }
370
+ return readString(payload[field]);
371
+ }
332
372
  function readString(value) {
333
373
  return typeof value === "string" && value.trim() ? value.trim() : null;
334
374
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Hosty app auth: session state machine, recovery bridge, Core revalidation, and the embedder responder.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "scripts": {
26
26
  "test": "vitest run",
27
- "build": "tsc -p tsconfig.build.json"
27
+ "build": "tsc -p tsconfig.build.json && node scripts/fix-dist-extensions.mjs"
28
28
  },
29
29
  "dependencies": {
30
30
  "server-only": "^0.0.1"