@hosty-sdk/app 0.6.0 → 0.8.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/README.md +22 -3
- package/dist/embedder.d.ts +40 -1
- package/dist/embedder.js +68 -12
- package/dist/index.d.ts +60 -0
- package/dist/index.js +59 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ npm install @hosty-sdk/app
|
|
|
10
10
|
|
|
11
11
|
| Entry | Runtime | Contents |
|
|
12
12
|
| --- | --- | --- |
|
|
13
|
-
| `@hosty-sdk/app` | anywhere | status taxonomy, recovery decision, `hosty:auth-required`
|
|
13
|
+
| `@hosty-sdk/app` | anywhere | status taxonomy, recovery decision, `hosty:auth-required` and `hosty:request-delegated-token` schemas, URL/env helpers |
|
|
14
14
|
| `@hosty-sdk/app/server` | server only | Core revalidation with caching, cookie helpers, the app-code route factory, the app secrets client |
|
|
15
15
|
| `@hosty-sdk/app/react` | client | `<AppIdentityBridge />` — probe, silent recovery, fallback cards |
|
|
16
|
-
| `@hosty-sdk/app/embedder` | client | verified
|
|
16
|
+
| `@hosty-sdk/app/embedder` | client | verified responders — launch-code recovery and delegated tokens — for anything that embeds Hosty apps |
|
|
17
17
|
|
|
18
18
|
Minimal Next.js wiring:
|
|
19
19
|
|
|
@@ -58,7 +58,26 @@ const claims = validateDelegatedToken(bearerToken);
|
|
|
58
58
|
if (claims?.role !== "host.admin") { /* 401/403 */ }
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
An embedded page cannot mint a delegated token itself — that needs the user's Core session in a
|
|
62
|
+
first-party context — so it asks whoever embeds it:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { DELEGATED_TOKEN_REQUEST_TYPE, DELEGATED_TOKEN_TYPE } from "@hosty-sdk/app";
|
|
66
|
+
import { parseActiveFrameDelegatedTokenRequest } from "@hosty-sdk/app/embedder";
|
|
67
|
+
|
|
68
|
+
// In the embedder, per app frame. A verified request says who asked, never whether to answer: the
|
|
69
|
+
// token is user-scoped, so grant it only to apps you decided to grant it to, and post it to that
|
|
70
|
+
// frame's own origin — never "*". Attach the listener before the frame can run (apps ask as soon as
|
|
71
|
+
// their document does, and they re-ask until answered), and honour `refresh`: it means the token the
|
|
72
|
+
// app holds was refused, so a cached mint must not be handed back.
|
|
73
|
+
const intent = parseActiveFrameDelegatedTokenRequest(event, frame.contentWindow, frame.src);
|
|
74
|
+
if (intent) {
|
|
75
|
+
const { token, expiresAt } = await mintDelegatedTokenFromCore(appId, { force: intent.refresh });
|
|
76
|
+
frame.contentWindow.postMessage({ type: DELEGATED_TOKEN_TYPE, token, expiresAt }, frameOrigin);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
61
80
|
The design contract lives in the Hosty repository:
|
|
62
|
-
[`docs/
|
|
81
|
+
[`docs/features/hosty-app-sdk/feature.md`](https://github.com/alex-de-haas/docker-host/blob/main/docs/features/hosty-app-sdk/feature.md).
|
|
63
82
|
|
|
64
83
|
License: AGPL-3.0-only.
|
package/dist/embedder.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/** The three fields of a `message` event these parsers read. */
|
|
2
|
+
export interface EmbedderMessage {
|
|
2
3
|
data: unknown;
|
|
3
4
|
origin: string;
|
|
4
5
|
source: unknown;
|
|
5
6
|
}
|
|
7
|
+
export type AuthRequiredMessage = EmbedderMessage;
|
|
6
8
|
/**
|
|
7
9
|
* Verifies that a `message` event is a genuine auth-required intent from the active app
|
|
8
10
|
* frame. Every check is about facts only the embedder can know:
|
|
@@ -13,6 +15,43 @@ export interface AuthRequiredMessage {
|
|
|
13
15
|
* and the embedder answers by reissuing a code through its own authenticated Core call.
|
|
14
16
|
*/
|
|
15
17
|
export declare function parseActiveFrameAuthRequired(event: AuthRequiredMessage, activeFrameWindow: unknown, activeFrameUrl: string, expectedAppId: string): boolean;
|
|
18
|
+
/** A verified request, and the one thing its payload carries. */
|
|
19
|
+
export interface DelegatedTokenRequestIntent {
|
|
20
|
+
/** The app says the token it holds was refused, so a cached mint must not be replayed. */
|
|
21
|
+
refresh: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Verifies that a `message` event is a delegated-token request from the active app frame, using
|
|
25
|
+
* the same sender checks as `parseActiveFrameAuthRequired`. There is nothing app-specific in the
|
|
26
|
+
* payload to check — and nothing to gain from one, since a frame's claim about which app it is
|
|
27
|
+
* would be the weakest fact in the room next to the embedder's own DOM.
|
|
28
|
+
*
|
|
29
|
+
* A non-null result says only *who asked*, never *whether to answer*. Answering hands the frame a
|
|
30
|
+
* user-scoped credential, so the embedder decides per app which ones it grants — Hosty Shell
|
|
31
|
+
* answers for the assistant gateway alone, the app it already mints delegated tokens for, so the
|
|
32
|
+
* handshake gives that app nothing it did not already hold. Wiring this responder to every frame
|
|
33
|
+
* would silently widen the delegated-token trust story to whatever an operator installed.
|
|
34
|
+
*
|
|
35
|
+
* An app may repeat the request (the first one can land before a listener is attached, and nothing
|
|
36
|
+
* makes an embedder attach one early), so answering must be idempotent — which minting from Core
|
|
37
|
+
* is.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseActiveFrameDelegatedTokenRequest(event: EmbedderMessage, activeFrameWindow: unknown, activeFrameUrl: string): DelegatedTokenRequestIntent | null;
|
|
40
|
+
/** A verified ask, and the one thing its payload carries. */
|
|
41
|
+
export interface AskAssistantIntent {
|
|
42
|
+
/** Trimmed and capped here, so no embedder has to remember to do it. */
|
|
43
|
+
text: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verifies that a `message` event is an ask-the-assistant intent from the active app frame, using
|
|
47
|
+
* the same sender checks as the pair above.
|
|
48
|
+
*
|
|
49
|
+
* The text is capped in the parser rather than by the caller: a cap is part of the contract, and an
|
|
50
|
+
* embedder that forgot it would let one app's page paste itself into the operator's draft. What the
|
|
51
|
+
* embedder must still decide is *where* the text goes — this function says an app asked, never that
|
|
52
|
+
* the assistant should send anything. Nothing here may be auto-sent.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseActiveFrameAskAssistant(event: EmbedderMessage, activeFrameWindow: unknown, activeFrameUrl: string): AskAssistantIntent | null;
|
|
16
55
|
/**
|
|
17
56
|
* Per-app reissue throttle: a frame that keeps re-posting (e.g. it never accepts the new
|
|
18
57
|
* code) must not drive an unbounded reissue storm. One reissue per app per interval is
|
package/dist/embedder.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
// Embedder slice: the shell half of the
|
|
2
|
-
// apps in iframes (Hosty Shell or a third-party UI client). Pure
|
|
3
|
-
// rate limiter — the embedder wires them to its own DOM
|
|
1
|
+
// Embedder slice: the shell half of the contracts an embedded app cannot serve itself, for
|
|
2
|
+
// anything that embeds Hosty apps in iframes (Hosty Shell or a third-party UI client). Pure
|
|
3
|
+
// functions plus a small rate limiter — the embedder wires them to its own DOM, its own "open
|
|
4
|
+
// this app" flow, and its own Core session.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
+
// Recovery, in one sentence: on a VERIFIED `hosty:auth-required` from an app you embed,
|
|
6
7
|
// re-run your normal open flow for that app (which mints a fresh launch code), rate-limited.
|
|
7
8
|
// The re-open must take the full launch-code path — an "already open → reuse the URL
|
|
8
9
|
// without a code" optimization must not short-circuit recovery.
|
|
9
|
-
|
|
10
|
+
//
|
|
11
|
+
// Delegated tokens, in one sentence: on a VERIFIED `hosty:request-delegated-token` from an app
|
|
12
|
+
// you have decided to grant one, mint a token from Core with your own session and post it back
|
|
13
|
+
// to that frame's origin. Both requests are unauthenticated by design; what makes them safe is
|
|
14
|
+
// that the embedder answers from facts about its own DOM rather than from what the frame claims.
|
|
15
|
+
import { ASK_ASSISTANT_MAX_CHARS, ASK_ASSISTANT_TYPE, AUTH_REQUIRED_INTENT_TYPE, DELEGATED_TOKEN_REQUEST_TYPE, } from "./index.js";
|
|
10
16
|
/**
|
|
11
17
|
* Verifies that a `message` event is a genuine auth-required intent from the active app
|
|
12
18
|
* frame. Every check is about facts only the embedder can know:
|
|
@@ -17,6 +23,62 @@ import { AUTH_REQUIRED_INTENT_TYPE } from "./index.js";
|
|
|
17
23
|
* and the embedder answers by reissuing a code through its own authenticated Core call.
|
|
18
24
|
*/
|
|
19
25
|
export function parseActiveFrameAuthRequired(event, activeFrameWindow, activeFrameUrl, expectedAppId) {
|
|
26
|
+
if (!isActiveFrameMessage(event, activeFrameWindow, activeFrameUrl)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const candidate = event.data;
|
|
30
|
+
return (candidate.type === AUTH_REQUIRED_INTENT_TYPE &&
|
|
31
|
+
typeof candidate.appId === "string" &&
|
|
32
|
+
candidate.appId === expectedAppId);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Verifies that a `message` event is a delegated-token request from the active app frame, using
|
|
36
|
+
* the same sender checks as `parseActiveFrameAuthRequired`. There is nothing app-specific in the
|
|
37
|
+
* payload to check — and nothing to gain from one, since a frame's claim about which app it is
|
|
38
|
+
* would be the weakest fact in the room next to the embedder's own DOM.
|
|
39
|
+
*
|
|
40
|
+
* A non-null result says only *who asked*, never *whether to answer*. Answering hands the frame a
|
|
41
|
+
* user-scoped credential, so the embedder decides per app which ones it grants — Hosty Shell
|
|
42
|
+
* answers for the assistant gateway alone, the app it already mints delegated tokens for, so the
|
|
43
|
+
* handshake gives that app nothing it did not already hold. Wiring this responder to every frame
|
|
44
|
+
* would silently widen the delegated-token trust story to whatever an operator installed.
|
|
45
|
+
*
|
|
46
|
+
* An app may repeat the request (the first one can land before a listener is attached, and nothing
|
|
47
|
+
* makes an embedder attach one early), so answering must be idempotent — which minting from Core
|
|
48
|
+
* is.
|
|
49
|
+
*/
|
|
50
|
+
export function parseActiveFrameDelegatedTokenRequest(event, activeFrameWindow, activeFrameUrl) {
|
|
51
|
+
if (!isActiveFrameMessage(event, activeFrameWindow, activeFrameUrl)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const candidate = event.data;
|
|
55
|
+
if (candidate.type !== DELEGATED_TOKEN_REQUEST_TYPE) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return { refresh: candidate.refresh === true };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Verifies that a `message` event is an ask-the-assistant intent from the active app frame, using
|
|
62
|
+
* the same sender checks as the pair above.
|
|
63
|
+
*
|
|
64
|
+
* The text is capped in the parser rather than by the caller: a cap is part of the contract, and an
|
|
65
|
+
* embedder that forgot it would let one app's page paste itself into the operator's draft. What the
|
|
66
|
+
* embedder must still decide is *where* the text goes — this function says an app asked, never that
|
|
67
|
+
* the assistant should send anything. Nothing here may be auto-sent.
|
|
68
|
+
*/
|
|
69
|
+
export function parseActiveFrameAskAssistant(event, activeFrameWindow, activeFrameUrl) {
|
|
70
|
+
if (!isActiveFrameMessage(event, activeFrameWindow, activeFrameUrl)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const candidate = event.data;
|
|
74
|
+
if (candidate.type !== ASK_ASSISTANT_TYPE || typeof candidate.text !== "string") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const text = candidate.text.trim().slice(0, ASK_ASSISTANT_MAX_CHARS);
|
|
78
|
+
return text ? { text } : null;
|
|
79
|
+
}
|
|
80
|
+
/** The sender half both parsers share: right frame, right origin, object payload. */
|
|
81
|
+
function isActiveFrameMessage(event, activeFrameWindow, activeFrameUrl) {
|
|
20
82
|
if (!activeFrameWindow || event.source !== activeFrameWindow) {
|
|
21
83
|
return false;
|
|
22
84
|
}
|
|
@@ -28,13 +90,7 @@ export function parseActiveFrameAuthRequired(event, activeFrameWindow, activeFra
|
|
|
28
90
|
catch {
|
|
29
91
|
return false;
|
|
30
92
|
}
|
|
31
|
-
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
const candidate = event.data;
|
|
35
|
-
return (candidate.type === AUTH_REQUIRED_INTENT_TYPE &&
|
|
36
|
-
typeof candidate.appId === "string" &&
|
|
37
|
-
candidate.appId === expectedAppId);
|
|
93
|
+
return Boolean(event.data) && typeof event.data === "object" && !Array.isArray(event.data);
|
|
38
94
|
}
|
|
39
95
|
/**
|
|
40
96
|
* Per-app reissue throttle: a frame that keeps re-posting (e.g. it never accepts the new
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,66 @@ export interface AuthRequiredIntent {
|
|
|
10
10
|
appId: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function createAuthRequiredIntent(appId: string): AuthRequiredIntent;
|
|
13
|
+
/**
|
|
14
|
+
* The postMessage types of the delegated-token handshake: an embedded app page asks its embedder
|
|
15
|
+
* for a short-TTL token scoped to itself, and the embedder answers with one it minted from Core.
|
|
16
|
+
* Same shape of contract as the launch-code recovery above — the app cannot self-serve, because
|
|
17
|
+
* minting needs the user's Core session in a first-party context, which only the embedder has.
|
|
18
|
+
*
|
|
19
|
+
* The request carries no secret, so it is safe to broadcast; the answer does, so an embedder must
|
|
20
|
+
* post it to the frame's own origin and never to `*`. Answering is a decision, not a reflex: the
|
|
21
|
+
* token is a user-scoped credential, so an embedder answers only for apps it deliberately grants
|
|
22
|
+
* one (Hosty Shell: the assistant gateway, which it already mints tokens for).
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* An embedded app asking its embedder to put text in the assistant's draft.
|
|
26
|
+
*
|
|
27
|
+
* The embedder reveals its assistant surface and forwards the text there; the assistant fills the
|
|
28
|
+
* **draft** and stops. Nothing is sent. That is the contract, not a UX choice: app-provided text
|
|
29
|
+
* entering a model that holds host shell is the same trust boundary that keeps MCP providers off by
|
|
30
|
+
* default, and an error message is exactly the shape a prompt injection arrives in. The operator
|
|
31
|
+
* reads it, with its source shown, and decides.
|
|
32
|
+
*
|
|
33
|
+
* Plain text only, and no reply. A structured payload would invite apps to smuggle shape the
|
|
34
|
+
* operator cannot read at a glance, and an answer would turn a hand-off into a channel.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ASK_ASSISTANT_TYPE = "hosty:ask-assistant";
|
|
37
|
+
/** Longer than a prompt fragment is a page dumping itself into someone's draft. */
|
|
38
|
+
export declare const ASK_ASSISTANT_MAX_CHARS = 4000;
|
|
39
|
+
export interface AskAssistantMessage {
|
|
40
|
+
type: typeof ASK_ASSISTANT_TYPE;
|
|
41
|
+
/** Trimmed and capped by the embedder's parser; the app need not pre-truncate. */
|
|
42
|
+
text: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Asks the embedder to put `text` in the operator's assistant draft.
|
|
46
|
+
*
|
|
47
|
+
* Safe to call unconditionally: standalone there is no embedder to hear it, and an embedder that
|
|
48
|
+
* does not offer an assistant simply ignores it. Returns whether the message could be posted at
|
|
49
|
+
* all — never whether anyone acted on it, which is the operator's business and not the app's.
|
|
50
|
+
*/
|
|
51
|
+
export declare function askAssistant(text: string): boolean;
|
|
52
|
+
export declare const DELEGATED_TOKEN_REQUEST_TYPE = "hosty:request-delegated-token";
|
|
53
|
+
export declare const DELEGATED_TOKEN_TYPE = "hosty:delegated-token";
|
|
54
|
+
export interface DelegatedTokenRequest {
|
|
55
|
+
type: typeof DELEGATED_TOKEN_REQUEST_TYPE;
|
|
56
|
+
/**
|
|
57
|
+
* Set when the app is asking because the token it holds was refused. Only the app learns that —
|
|
58
|
+
* it is the one calling the API — so without this flag an embedder that caches its mints would
|
|
59
|
+
* keep answering with the very token that just came back 401, and the app could not recover until
|
|
60
|
+
* the embedder's own copy aged out. Absent rather than false on an ordinary request.
|
|
61
|
+
*/
|
|
62
|
+
refresh?: true;
|
|
63
|
+
}
|
|
64
|
+
export interface DelegatedTokenGrant {
|
|
65
|
+
type: typeof DELEGATED_TOKEN_TYPE;
|
|
66
|
+
token: string;
|
|
67
|
+
/** ISO-8601 expiry, so the page can reuse the token instead of asking once per request. */
|
|
68
|
+
expiresAt: string;
|
|
69
|
+
}
|
|
70
|
+
export declare function createDelegatedTokenRequest(options?: {
|
|
71
|
+
refresh?: boolean;
|
|
72
|
+
}): DelegatedTokenRequest;
|
|
13
73
|
/**
|
|
14
74
|
* Recovery classification of an app session, per the platform identity error contract:
|
|
15
75
|
* - `not-present`: no token at all (recoverable — same handling as `expired`).
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// server.mjs). No React, no Next, no environment reads — everything is passed in.
|
|
3
3
|
//
|
|
4
4
|
// This is the single source of truth for the Hosty app-session contract described in
|
|
5
|
-
// docker-host docs/
|
|
5
|
+
// docker-host docs/features/hosty-app-sdk/feature.md and docs/features/auth-session-lifecycle/feature.md.
|
|
6
6
|
/**
|
|
7
7
|
* The postMessage type an embedded app sends its parent to request a fresh launch code.
|
|
8
8
|
* A frozen protocol constant: it deliberately does not track product branding (precedent:
|
|
@@ -13,6 +13,64 @@ export const AUTH_REQUIRED_INTENT_TYPE = "hosty:auth-required";
|
|
|
13
13
|
export function createAuthRequiredIntent(appId) {
|
|
14
14
|
return { type: AUTH_REQUIRED_INTENT_TYPE, appId };
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The postMessage types of the delegated-token handshake: an embedded app page asks its embedder
|
|
18
|
+
* for a short-TTL token scoped to itself, and the embedder answers with one it minted from Core.
|
|
19
|
+
* Same shape of contract as the launch-code recovery above — the app cannot self-serve, because
|
|
20
|
+
* minting needs the user's Core session in a first-party context, which only the embedder has.
|
|
21
|
+
*
|
|
22
|
+
* The request carries no secret, so it is safe to broadcast; the answer does, so an embedder must
|
|
23
|
+
* post it to the frame's own origin and never to `*`. Answering is a decision, not a reflex: the
|
|
24
|
+
* token is a user-scoped credential, so an embedder answers only for apps it deliberately grants
|
|
25
|
+
* one (Hosty Shell: the assistant gateway, which it already mints tokens for).
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* An embedded app asking its embedder to put text in the assistant's draft.
|
|
29
|
+
*
|
|
30
|
+
* The embedder reveals its assistant surface and forwards the text there; the assistant fills the
|
|
31
|
+
* **draft** and stops. Nothing is sent. That is the contract, not a UX choice: app-provided text
|
|
32
|
+
* entering a model that holds host shell is the same trust boundary that keeps MCP providers off by
|
|
33
|
+
* default, and an error message is exactly the shape a prompt injection arrives in. The operator
|
|
34
|
+
* reads it, with its source shown, and decides.
|
|
35
|
+
*
|
|
36
|
+
* Plain text only, and no reply. A structured payload would invite apps to smuggle shape the
|
|
37
|
+
* operator cannot read at a glance, and an answer would turn a hand-off into a channel.
|
|
38
|
+
*/
|
|
39
|
+
export const ASK_ASSISTANT_TYPE = "hosty:ask-assistant";
|
|
40
|
+
/** Longer than a prompt fragment is a page dumping itself into someone's draft. */
|
|
41
|
+
export const ASK_ASSISTANT_MAX_CHARS = 4000;
|
|
42
|
+
/**
|
|
43
|
+
* Asks the embedder to put `text` in the operator's assistant draft.
|
|
44
|
+
*
|
|
45
|
+
* Safe to call unconditionally: standalone there is no embedder to hear it, and an embedder that
|
|
46
|
+
* does not offer an assistant simply ignores it. Returns whether the message could be posted at
|
|
47
|
+
* all — never whether anyone acted on it, which is the operator's business and not the app's.
|
|
48
|
+
*/
|
|
49
|
+
export function askAssistant(text) {
|
|
50
|
+
if (typeof window === "undefined" || window.parent === window) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
// Broadcast: the message carries no secret, and the embedder verifies the sender against its own
|
|
58
|
+
// DOM rather than trusting anything claimed here.
|
|
59
|
+
//
|
|
60
|
+
// Capped on the way out too. The parser remains where the contract is enforced — an app is not
|
|
61
|
+
// trusted to have done this — but there is no reason to structure-clone a megabyte across the
|
|
62
|
+
// boundary just to have the embedder discard it.
|
|
63
|
+
const capped = trimmed.slice(0, ASK_ASSISTANT_MAX_CHARS);
|
|
64
|
+
window.parent.postMessage({ type: ASK_ASSISTANT_TYPE, text: capped }, "*");
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
export const DELEGATED_TOKEN_REQUEST_TYPE = "hosty:request-delegated-token";
|
|
68
|
+
export const DELEGATED_TOKEN_TYPE = "hosty:delegated-token";
|
|
69
|
+
export function createDelegatedTokenRequest(options = {}) {
|
|
70
|
+
return options.refresh
|
|
71
|
+
? { type: DELEGATED_TOKEN_REQUEST_TYPE, refresh: true }
|
|
72
|
+
: { type: DELEGATED_TOKEN_REQUEST_TYPE };
|
|
73
|
+
}
|
|
16
74
|
/**
|
|
17
75
|
* Maps a Core revalidation HTTP status onto the session classification. Classification is
|
|
18
76
|
* by status code only — never by error-code strings — so new Core codes cannot break an
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@ 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;
|
|
8
|
-
//
|
|
8
|
+
// Per docs/features/hosty-app-sdk/feature.md: positive revalidations may be cached for 30s, clamped to
|
|
9
9
|
// the grant's expiry; negative results are never cached, so a stuck-unauthenticated state
|
|
10
10
|
// is impossible. Bounded so an attacker spraying tokens cannot grow the map unboundedly.
|
|
11
11
|
const REVALIDATION_CACHE_TTL_MS = 30_000;
|
package/package.json
CHANGED