@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 +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/server.d.ts +11 -1
- package/dist/server.js +48 -8
- package/package.json +2 -2
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
|
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"
|