@hosty-sdk/app 0.19.1 → 0.21.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/activity-react.d.ts +5 -1
- package/dist/activity-react.js +34 -11
- package/dist/app-code.d.ts +6 -0
- package/dist/app-code.js +80 -0
- package/dist/browser-auth.d.ts +44 -3
- package/dist/browser-auth.js +463 -27
- package/dist/embedder.d.ts +2 -1
- package/dist/embedder.js +6 -5
- package/dist/index.d.ts +12 -12
- package/dist/index.js +11 -10
- package/dist/react.d.ts +2 -1
- package/dist/react.js +155 -39
- package/dist/server.d.ts +6 -4
- package/dist/server.js +24 -8
- package/package.json +6 -1
package/dist/activity-react.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { AppAuthProtocol } from "./app-code.js";
|
|
1
2
|
/** Keep mounted beside app content so expiry never discards drafts or component state. */
|
|
2
|
-
export declare function AppActivityBridge({ openUrl, appCodePath, activeUntil, activityRequired }: {
|
|
3
|
+
export declare function AppActivityBridge({ openUrl, appCodePath, activeUntil, activityRequired, appAuthProtocol, probePath, native }: {
|
|
3
4
|
openUrl: string;
|
|
4
5
|
appCodePath: string;
|
|
5
6
|
activeUntil?: string | null;
|
|
6
7
|
activityRequired?: boolean;
|
|
8
|
+
appAuthProtocol?: AppAuthProtocol | null;
|
|
9
|
+
probePath?: string;
|
|
10
|
+
native?: boolean;
|
|
7
11
|
}): import("react").JSX.Element | null;
|
package/dist/activity-react.js
CHANGED
|
@@ -3,8 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { AuthNotice, AuthNoticeButton } from "./auth-notice.js";
|
|
5
5
|
import { APP_SESSION_ENDED, APP_ACTIVITY_RENEWED, appActivityNeedsRenewal, configureAppActivity, updateAppActivity, renewAppActivity, cancelActivityRenewal } from "./browser-auth.js";
|
|
6
|
+
import { resolveLaunchMode, detectLaunchMode, normalizeLaunchMode, LAUNCH_MODE_ATTRIBUTE, LAUNCH_MODE_PARAM, LAUNCH_MODE_STORAGE_KEY } from "./index.js";
|
|
7
|
+
import { acceptAppAuthProtocol, refreshAppAuthProtocol } from "./browser-auth.js";
|
|
6
8
|
/** Keep mounted beside app content so expiry never discards drafts or component state. */
|
|
7
|
-
export function AppActivityBridge({ openUrl, appCodePath, activeUntil, activityRequired = false }) {
|
|
9
|
+
export function AppActivityBridge({ openUrl, appCodePath, activeUntil, activityRequired = false, appAuthProtocol, probePath = "/api/auth/identity", native }) {
|
|
8
10
|
const [needed, setNeeded] = useState(false);
|
|
9
11
|
const [pending, setPending] = useState(false);
|
|
10
12
|
const [error, setError] = useState("");
|
|
@@ -15,14 +17,35 @@ export function AppActivityBridge({ openUrl, appCodePath, activeUntil, activityR
|
|
|
15
17
|
void operation.catch(e => { setNeeded(true); setError(String(e.message ?? e)); }).finally(() => setPending(false));
|
|
16
18
|
};
|
|
17
19
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
let cleanup;
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const configure = (protocol) => {
|
|
24
|
+
if (cancelled || !protocol || acceptAppAuthProtocol(new URL(openUrl).origin, protocol) !== protocol)
|
|
25
|
+
return;
|
|
26
|
+
let stored = null;
|
|
27
|
+
try {
|
|
28
|
+
stored = window.sessionStorage.getItem(LAUNCH_MODE_STORAGE_KEY);
|
|
29
|
+
}
|
|
30
|
+
catch { /* The applied document mode remains available. */ }
|
|
31
|
+
const mode = normalizeLaunchMode(document.documentElement.getAttribute(LAUNCH_MODE_ATTRIBUTE)) ?? resolveLaunchMode({
|
|
32
|
+
param: new URL(window.location.href).searchParams.get(LAUNCH_MODE_PARAM), stored, heuristic: detectLaunchMode(window),
|
|
33
|
+
}).mode;
|
|
34
|
+
cleanup = configureAppActivity({ openUrl, appAuthProtocol: protocol, probePath, native: native ?? mode === "native", activeUntil, activityRequired, exchangeCode: async (proof) => {
|
|
35
|
+
const response = await fetch(appCodePath, { method: "POST", credentials: "same-origin", redirect: "error",
|
|
36
|
+
headers: { "Content-Type": "application/json" }, body: JSON.stringify(proof) });
|
|
37
|
+
const body = await response.json();
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
throw new Error(body.message ?? "Could not renew app activity.");
|
|
40
|
+
return body;
|
|
41
|
+
} });
|
|
42
|
+
};
|
|
43
|
+
if (appAuthProtocol !== undefined && !(appAuthProtocol === 1 && acceptAppAuthProtocol(new URL(openUrl).origin, 1) === null))
|
|
44
|
+
configure(appAuthProtocol);
|
|
45
|
+
else
|
|
46
|
+
void refreshAppAuthProtocol(openUrl, probePath, controller.signal).then(configure);
|
|
47
|
+
// A stale protocol-1 prop cannot overwrite a protocol-2 minimum already verified
|
|
48
|
+
// by this app. Unknown discovery leaves renewal disabled instead of downgrading.
|
|
26
49
|
const expired = () => setNeeded(true);
|
|
27
50
|
const renewed = () => { setNeeded(false); setError(""); };
|
|
28
51
|
const click = (event) => {
|
|
@@ -35,8 +58,8 @@ export function AppActivityBridge({ openUrl, appCodePath, activeUntil, activityR
|
|
|
35
58
|
window.addEventListener(APP_SESSION_ENDED, expired);
|
|
36
59
|
window.addEventListener(APP_ACTIVITY_RENEWED, renewed);
|
|
37
60
|
document.addEventListener("click", click, true);
|
|
38
|
-
return () => { cleanup(); window.removeEventListener(APP_SESSION_ENDED, expired); window.removeEventListener(APP_ACTIVITY_RENEWED, renewed); document.removeEventListener("click", click, true); };
|
|
39
|
-
}, [openUrl, appCodePath]);
|
|
61
|
+
return () => { cancelled = true; controller.abort(); cleanup?.(); window.removeEventListener(APP_SESSION_ENDED, expired); window.removeEventListener(APP_ACTIVITY_RENEWED, renewed); document.removeEventListener("click", click, true); };
|
|
62
|
+
}, [openUrl, appCodePath, appAuthProtocol, probePath, native]);
|
|
40
63
|
useEffect(() => { updateAppActivity(activeUntil, activityRequired); }, [activeUntil, activityRequired, openUrl, appCodePath]);
|
|
41
64
|
if (!needed)
|
|
42
65
|
return null;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared by framework server adapters and plain Node apps. Never sends an app credential. */
|
|
2
|
+
export type AppAuthProtocol = 1 | 2;
|
|
3
|
+
export declare function isValidCodeVerifier(value: unknown): value is string;
|
|
4
|
+
export declare function appAuthProtocolMinimum(coreOrigin: string | null): 2 | null;
|
|
5
|
+
/** A 404 alone is insufficient: only an identified, older Core permits the old navigation. */
|
|
6
|
+
export declare function resolveAppAuthProtocol(coreOrigin: string | null): Promise<AppAuthProtocol | null>;
|
package/dist/app-code.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export function isValidCodeVerifier(value) {
|
|
2
|
+
return typeof value === "string" && /^[A-Za-z0-9._~-]{43,128}$/.test(value);
|
|
3
|
+
}
|
|
4
|
+
const minimumProtocols = new Set();
|
|
5
|
+
const discoveries = new Map();
|
|
6
|
+
export function appAuthProtocolMinimum(coreOrigin) {
|
|
7
|
+
try {
|
|
8
|
+
return minimumProtocols.has(new URL(coreOrigin ?? "").origin) ? 2 : null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isOlderCoreVersion(value) {
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
return false;
|
|
17
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/.exec(value);
|
|
18
|
+
if (!match)
|
|
19
|
+
return false;
|
|
20
|
+
for (const [index, suffix] of [[4, match[4]], [5, match[5]]]) {
|
|
21
|
+
if (suffix && suffix.split(".").some(part => !/^[0-9A-Za-z-]+$/.test(part) || (index === 4 && /^0\d+$/.test(part))))
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const major = Number(match[1]), minor = Number(match[2]), patch = Number(match[3]);
|
|
25
|
+
return [major, minor, patch].every(Number.isSafeInteger) && major === 0 && minor < 120;
|
|
26
|
+
}
|
|
27
|
+
/** A 404 alone is insufficient: only an identified, older Core permits the old navigation. */
|
|
28
|
+
export async function resolveAppAuthProtocol(coreOrigin) {
|
|
29
|
+
let origin;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(coreOrigin ?? "");
|
|
32
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password ||
|
|
33
|
+
parsed.pathname !== "/" || parsed.search || parsed.hash)
|
|
34
|
+
return null;
|
|
35
|
+
origin = parsed.origin;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const pending = discoveries.get(origin);
|
|
41
|
+
if (pending)
|
|
42
|
+
return pending;
|
|
43
|
+
const operation = (async () => {
|
|
44
|
+
const request = (path) => fetch(new URL(path, origin), {
|
|
45
|
+
cache: "no-store", redirect: "error", headers: { accept: "application/json" },
|
|
46
|
+
signal: AbortSignal.timeout(1_500),
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const response = await request("/api/auth/apps/protocol");
|
|
50
|
+
if (response.status === 200) {
|
|
51
|
+
if (!response.headers.get("content-type")?.toLowerCase().startsWith("application/json"))
|
|
52
|
+
return null;
|
|
53
|
+
const body = await response.json().catch(() => null);
|
|
54
|
+
if (body?.version !== 2)
|
|
55
|
+
return null;
|
|
56
|
+
minimumProtocols.add(origin);
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
if (response.status !== 404 || minimumProtocols.has(origin))
|
|
60
|
+
return null;
|
|
61
|
+
const status = await request("/api/core/status");
|
|
62
|
+
if (status.status !== 200)
|
|
63
|
+
return null;
|
|
64
|
+
if (!status.headers.get("content-type")?.toLowerCase().startsWith("application/json"))
|
|
65
|
+
return null;
|
|
66
|
+
const body = await status.json().catch(() => null);
|
|
67
|
+
return body?.component === "hosty-core" && body.status === "running" && isOlderCoreVersion(body.version) ? 1 : null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
discoveries.set(origin, operation);
|
|
74
|
+
try {
|
|
75
|
+
return await operation;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
discoveries.delete(origin);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/browser-auth.d.ts
CHANGED
|
@@ -1,17 +1,58 @@
|
|
|
1
|
+
import type { AppAuthProtocol } from "./app-code.js";
|
|
2
|
+
export type AppSignInResult = {
|
|
3
|
+
code: string;
|
|
4
|
+
codeVerifier: string;
|
|
5
|
+
};
|
|
6
|
+
export type AppAuthAttempt = {
|
|
7
|
+
state: string;
|
|
8
|
+
codeVerifier: string;
|
|
9
|
+
codeChallenge: string;
|
|
10
|
+
appId: string;
|
|
11
|
+
coreOrigin: string;
|
|
12
|
+
redirectUri: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
mode: "standalone" | "silent" | "popup" | "native";
|
|
15
|
+
protocol: AppAuthProtocol;
|
|
16
|
+
};
|
|
17
|
+
export declare const APP_AUTH_ATTEMPT_PREFIX = "hosty.auth.attempt:";
|
|
18
|
+
export declare function appCodeChallenge(verifier: string): string;
|
|
19
|
+
export declare function acceptAppAuthProtocol(coreOrigin: string, protocol: AppAuthProtocol | null): AppAuthProtocol | null;
|
|
20
|
+
export declare function createAppAuthAttempt(openUrl: string, mode: AppAuthAttempt["mode"], protocol: AppAuthProtocol): AppAuthAttempt;
|
|
21
|
+
/** Do not leave the app document unless this exact proof survives a storage round trip. */
|
|
22
|
+
export declare function persistAppAuthAttempt(attempt: AppAuthAttempt): boolean;
|
|
23
|
+
/** Correlation is local; URL fields never supply a verifier or a pending attempt. */
|
|
24
|
+
export declare function takeAppAuthAttempt(callback: URL, allowLegacyUpgrade?: boolean): AppAuthAttempt | null;
|
|
25
|
+
export declare function clearAppAuthAttempts(): void;
|
|
26
|
+
export declare function appAuthNavigationUrl(attempt: AppAuthAttempt): string;
|
|
27
|
+
/** Constant document and DOM-built fields prevent a public URL from laundering a challenge. */
|
|
28
|
+
export declare function submitAppAuthIntent(attempt: AppAuthAttempt, doc?: Document): void;
|
|
1
29
|
export declare const APP_SESSION_ENDED = "hosty:app-session-ended";
|
|
30
|
+
export declare const NATIVE_APP_AUTH_RESULT = "hosty:native-auth-result";
|
|
31
|
+
export declare const APP_GRANT_STORAGE_KEY = "hosty.auth.app-grant";
|
|
32
|
+
/** Restore before the first identity probe; standalone documents use their first-party cookie. */
|
|
33
|
+
export declare function restoreAppGrant(): boolean;
|
|
2
34
|
export declare function rememberAppGrant(token: string): void;
|
|
3
35
|
export declare function forgetAppGrant(): void;
|
|
36
|
+
/** Snapshot the grant generation so older probes cannot invalidate a renewed identity. */
|
|
37
|
+
export declare function appGrantRevision(): number;
|
|
38
|
+
/** Activity expiry keeps identity; only explicit identity rejection discards the credential. */
|
|
39
|
+
export declare function forgetRejectedAppGrant(code: unknown, expectedRevision?: number): boolean;
|
|
4
40
|
export declare function appSessionActive(): void;
|
|
5
41
|
/** Use for app-local API calls, including streams. Redirects cannot carry the credential away. */
|
|
6
42
|
export declare function appFetch(input: string | URL, init?: RequestInit, recover?: boolean): Promise<Response>;
|
|
7
|
-
/**
|
|
8
|
-
export declare function
|
|
43
|
+
/** Refresh only the app-local, server-verified metadata for this frozen app/Core pair. */
|
|
44
|
+
export declare function refreshAppAuthProtocol(openUrl: string, probePath?: string, signal?: AbortSignal): Promise<AppAuthProtocol | null>;
|
|
45
|
+
/** Must be called directly from a user gesture. Native hosts intercept before any request. */
|
|
46
|
+
export declare function openAppSignIn(openUrl: string, signal?: AbortSignal, appAuthProtocol?: AppAuthProtocol, native?: boolean, probePath?: string): Promise<AppSignInResult>;
|
|
9
47
|
export declare const APP_ACTIVITY_RENEWED = "hosty:app-activity-renewed";
|
|
10
48
|
export type AppActivityConfig = {
|
|
11
49
|
openUrl: string;
|
|
50
|
+
appAuthProtocol: AppAuthProtocol;
|
|
51
|
+
native?: boolean;
|
|
52
|
+
probePath?: string;
|
|
12
53
|
activeUntil?: string | null;
|
|
13
54
|
activityRequired?: boolean;
|
|
14
|
-
exchangeCode: (
|
|
55
|
+
exchangeCode: (result: AppSignInResult) => Promise<{
|
|
15
56
|
accessToken?: string;
|
|
16
57
|
activeUntil?: string | null;
|
|
17
58
|
}>;
|