@t3b/hub-guard 0.8.0 → 0.9.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 +9 -2
- package/dist/guard.d.ts +6 -0
- package/dist/guard.js +29 -5
- package/dist/hub-client.d.ts +7 -0
- package/dist/hub-client.js +17 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/pages.d.ts +1 -0
- package/dist/pages.js +3 -0
- package/dist/session.d.ts +8 -0
- package/dist/session.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -271,14 +271,21 @@ const lines = guard.appMenu(session);
|
|
|
271
271
|
|
|
272
272
|
## What it enforces
|
|
273
273
|
|
|
274
|
-
- `/hub/enter?ticket=…`: an EdDSA ticket signed by the Hub's published key, for this module, from this environment's Hub, 60 seconds, single use → sets the module session (HttpOnly, Secure, SameSite=Strict) and answers a short "Opening …" page that moves on to the page (0.4.0: a SameSite=Strict cookie set on a redirect ending a navigation from another site is not sent on it). `appName` names the app on that page; `landing(path)` maps the pass's page (e.g. `"/"` → `"/app"`), never off the module's own address.
|
|
275
|
-
- Every other request: no session → 403 (HTML for browsers, JSON for APIs), on **every host** the module answers on. Idle 180 minutes (0.5.0), absolute 8 hours. Every 15 minutes the Hub is asked (with the app's Vercel ID) whether the person's session still stands. A Hub blip keeps the session until the next check.
|
|
274
|
+
- `/hub/enter?ticket=…`: an EdDSA ticket signed by the Hub's published key, for this module, from this environment's Hub, 60 seconds, single use - spent at the Hub, so once across all servers (0.9.0) → sets the module session (HttpOnly, Secure, SameSite=Strict) and answers a short "Opening …" page that moves on to the page (0.4.0: a SameSite=Strict cookie set on a redirect ending a navigation from another site is not sent on it). `appName` names the app on that page; `landing(path)` maps the pass's page (e.g. `"/"` → `"/app"`), never off the module's own address.
|
|
275
|
+
- Every other request: no session → 403 (HTML for browsers, JSON for APIs), on **every host** the module answers on. Idle 180 minutes (0.5.0), absolute 8 hours. Every 15 minutes the Hub is asked (with the app's Vercel ID) whether the person's session still stands, and refreshes their pages, apps and stores (0.9.0). A Hub blip keeps the session until the next check; after **180 minutes** without an answer (`maxStaleMs`) the module still opens and reads, but every change waits - a POST/PUT/PATCH/DELETE answers 503 `hub_unreachable` and `allows(…, "write")` / `pageAllowed` say no (0.9.0).
|
|
276
276
|
- Open paths (`/hub/enter`, `/api/hub/manifest`, `/api/hub/health`, `/api/hub/activity` and what you add) never need a session — they carry their own locks (the Hub's pass, a cron secret).
|
|
277
277
|
- Cloudflare Access proof checked first when the team and audience are set.
|
|
278
278
|
- `verifyRelay`, `manifestResponse`, `healthResponse`, `activityResponse`, `verifyStationCall`: the Hub's pass, for this module, for this purpose, from this environment's Hub; a relay and a station pack also for these exact bytes; a relay at most 5 minutes old; a call at most 60 seconds old.
|
|
279
279
|
|
|
280
280
|
## Changes
|
|
281
281
|
|
|
282
|
+
- **0.9.0** (the Hub audit of 2026-10-04, H07-H09): (1) the 15-minute check refreshes `session.stores` - a store taken
|
|
283
|
+
away leaves the open app within 15 minutes; (2) after `maxStaleMs` (default 180 minutes, `MAX_STALE_MS`) without the
|
|
284
|
+
Hub's answer the session is `stale`: reads go on, changes are refused (503 `hub_unreachable`, `pageAllowed(…, "write")`
|
|
285
|
+
false - the owner too) until the Hub answers again; (3) every entry ticket is spent at the Hub
|
|
286
|
+
(`POST /api/hub/ticket-used`, `spendTicket`) - a ticket works once across every server; if the Hub does not confirm
|
|
287
|
+
it, the entry is refused (open the app again from the Hub). **The Hub must be on its 2026-10-05 release or later before
|
|
288
|
+
an app takes 0.9.0** (the ticket door). `sharedReplay: false` turns (3) off (tests only).
|
|
282
289
|
- **0.8.0** (WORKFLOWS §18): the Apps button - the pass's and the 15-minute check's `apps` are kept in the session
|
|
283
290
|
(`session.apps`), `guard.appMenu(session)` / `appMenu()` give the lines, `readApps` reads a list defensively. Nothing
|
|
284
291
|
else changes; a module on 0.7.0 keeps working (its sessions simply carry no list).
|
package/dist/guard.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type GuardConfig = {
|
|
|
27
27
|
audience?: string;
|
|
28
28
|
};
|
|
29
29
|
absoluteMs?: number;
|
|
30
|
+
/** H08 (0.9.0): how long changes stay allowed while the Hub does not answer; default 180 minutes */
|
|
31
|
+
maxStaleMs?: number;
|
|
32
|
+
/** H09 (0.9.0): spend each ticket at the Hub (works once across all servers); default on - off only in tests */
|
|
33
|
+
sharedReplay?: boolean;
|
|
30
34
|
fetchImpl?: typeof fetch;
|
|
31
35
|
now?: () => Date;
|
|
32
36
|
};
|
|
@@ -60,6 +64,8 @@ export declare class Guard {
|
|
|
60
64
|
enter(req: Request): Promise<Response>;
|
|
61
65
|
/** Every other request: open path → allow; else the session must stand, be fresh, and pass the 15-minute Hub check. */
|
|
62
66
|
check(req: Request): Promise<CheckResult>;
|
|
67
|
+
/** H08: 503 while the Hub has been silent too long - the session stays (nothing is signed out), changes wait. */
|
|
68
|
+
pauseChanges(req: Request): Response;
|
|
63
69
|
/**
|
|
64
70
|
* §14: may this person read (`"read"`) or change (`"write"`) this page? The owner may do everything.
|
|
65
71
|
* Ask on the server for every page and every action, then answer `refusePage` when not.
|
package/dist/guard.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { appMenu } from "./apps.js";
|
|
2
2
|
import { checkCloudflareAccess } from "./cf-access.js";
|
|
3
|
-
import { sessionCheckPages } from "./hub-client.js";
|
|
3
|
+
import { sessionCheckPages, spendTicket } from "./hub-client.js";
|
|
4
4
|
import { pageAllowed } from "./pages.js";
|
|
5
5
|
import { hubKeys } from "./identity.js";
|
|
6
|
-
import { clearCookieHeader, cookieHeader, cookieValue, readSession, signSession, RECHECK_MS, SESSION_COOKIE } from "./session.js";
|
|
6
|
+
import { clearCookieHeader, cookieHeader, cookieValue, readSession, signSession, MAX_STALE_MS, RECHECK_MS, SESSION_COOKIE } from "./session.js";
|
|
7
7
|
import { JtiMemory, verifyTicket } from "./ticket.js";
|
|
8
8
|
import { hmacSha256, toHex } from "./webcrypto.js";
|
|
9
9
|
export class Guard {
|
|
@@ -59,6 +59,14 @@ export class Guard {
|
|
|
59
59
|
const words = { expired: "The ticket has expired (it lives 60 seconds) - open the module again from the Hub.", wrong_audience: "This ticket was made for another module.", bad_signature: "This ticket was not signed by the Hub.", replayed: "This ticket was already used.", malformed: "No valid ticket.", wrong_environment: "This ticket comes from another Hub (test or live)." };
|
|
60
60
|
return this.refuse(req, words[v.reason] ?? "No valid ticket.");
|
|
61
61
|
}
|
|
62
|
+
// H09: this server's memory stopped a replay here; the Hub stops one on any other server
|
|
63
|
+
if (this.cfg.sharedReplay !== false) {
|
|
64
|
+
const spent = await spendTicket(this.hubClient(), v.claims.jti, v.claims.exp);
|
|
65
|
+
if (spent === "used")
|
|
66
|
+
return this.refuse(req, "This ticket was already used.");
|
|
67
|
+
if (spent === "unknown")
|
|
68
|
+
return this.refuse(req, "The Hub could not confirm this ticket - open the app again from the Hub.");
|
|
69
|
+
}
|
|
62
70
|
const now = this.now().getTime();
|
|
63
71
|
const session = { personId: v.claims.sub, name: v.claims.name, role: v.claims.role, stores: v.claims.stores, sv: v.claims.sv, iat: now, lastSeen: now, checkedAt: now, pages: v.claims.pages, apps: v.claims.apps };
|
|
64
72
|
const landed = new URL(this.cfg.landing ? this.cfg.landing(v.claims.path) : v.claims.path, url.origin);
|
|
@@ -97,12 +105,28 @@ export class Guard {
|
|
|
97
105
|
const r2 = await sessionCheckPages(this.hubClient(), s.personId, s.sv);
|
|
98
106
|
if (r2.verdict === "refused")
|
|
99
107
|
return { allow: false, response: this.refuse(req, "The Hub ended this session (signed out everywhere, or access changed).") };
|
|
100
|
-
// §14: a change of ticks reaches the module here, within 15 minutes - §18: and so does the Apps button's list
|
|
108
|
+
// §14: a change of ticks reaches the module here, within 15 minutes - §18: and so does the Apps button's list -
|
|
109
|
+
// H07: and so does a store taken away (or given)
|
|
101
110
|
if (r2.verdict === "ok")
|
|
102
|
-
s = { ...s, checkedAt: nowMs, ...(r2.pages ? { pages: r2.pages } : {}), ...(r2.apps ? { apps: r2.apps } : {}) };
|
|
111
|
+
s = { ...s, checkedAt: nowMs, ...(r2.pages ? { pages: r2.pages } : {}), ...(r2.apps ? { apps: r2.apps } : {}), ...(r2.stores ? { stores: r2.stores } : {}) };
|
|
103
112
|
}
|
|
113
|
+
// H08: the Hub has not answered for longer than the limit - the module still opens and reads, changes wait
|
|
114
|
+
const stale = nowMs - s.checkedAt >= (this.cfg.maxStaleMs ?? MAX_STALE_MS);
|
|
115
|
+
if (stale && !["GET", "HEAD", "OPTIONS"].includes(req.method.toUpperCase()))
|
|
116
|
+
return { allow: false, response: this.pauseChanges(req) };
|
|
104
117
|
s = { ...s, lastSeen: nowMs };
|
|
105
|
-
|
|
118
|
+
const setCookie = cookieHeader(await signSession(s, secret), { secure: url.protocol === "https:", maxAgeS: Math.floor((this.cfg.absoluteMs ?? 8 * 3_600_000) / 1000) });
|
|
119
|
+
return { allow: true, session: stale ? { ...s, stale: true } : s, setCookie };
|
|
120
|
+
}
|
|
121
|
+
/** H08: 503 while the Hub has been silent too long - the session stays (nothing is signed out), changes wait. */
|
|
122
|
+
pauseChanges(req) {
|
|
123
|
+
const reason = "The Hub has not answered for a while, so changes are paused. You can still look; try again when the Hub is back.";
|
|
124
|
+
const accept = req.headers.get("accept") ?? "";
|
|
125
|
+
if (!accept.includes("text/html")) {
|
|
126
|
+
return new Response(JSON.stringify({ error: "hub_unreachable", reason }), { status: 503, headers: { "Content-Type": "application/json", "Cache-Control": "no-store", "Retry-After": "60" } });
|
|
127
|
+
}
|
|
128
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Changes paused</title></head><body style="font-family:system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;margin:0"><p style="max-width:440px;text-align:center">${escapeHtml(reason)}</p></body></html>`;
|
|
129
|
+
return new Response(html, { status: 503, headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Retry-After": "60" } });
|
|
106
130
|
}
|
|
107
131
|
/**
|
|
108
132
|
* §14: may this person read (`"read"`) or change (`"write"`) this page? The owner may do everything.
|
package/dist/hub-client.d.ts
CHANGED
|
@@ -19,7 +19,14 @@ export declare function sessionCheckPages(cfg: HubClientConfig, personId: string
|
|
|
19
19
|
verdict: "ok" | "refused" | "unknown";
|
|
20
20
|
pages?: Pages;
|
|
21
21
|
apps?: HubApp[];
|
|
22
|
+
stores?: string[];
|
|
22
23
|
}>;
|
|
24
|
+
/**
|
|
25
|
+
* H09 (0.9.0): spend an entry ticket at the Hub, so it works once across every server of every module (the memory of
|
|
26
|
+
* one server is not enough on serverless). "first" = this is its first use; "used" = it was used before; "unknown" =
|
|
27
|
+
* the Hub did not say (the entry is refused - the ticket is a minute old, opening again from the Hub is cheap).
|
|
28
|
+
*/
|
|
29
|
+
export declare function spendTicket(cfg: HubClientConfig, jti: string, exp: number): Promise<"first" | "used" | "unknown">;
|
|
23
30
|
export type BrokerToken = {
|
|
24
31
|
accessToken: string;
|
|
25
32
|
expiresAt: Date;
|
package/dist/hub-client.js
CHANGED
|
@@ -50,8 +50,25 @@ export async function sessionCheckPages(cfg, personId, sessionVersion) {
|
|
|
50
50
|
verdict: "ok",
|
|
51
51
|
...(r.json.pages === undefined ? {} : { pages: readPages(r.json.pages) }),
|
|
52
52
|
...(r.json.apps === undefined ? {} : { apps: readApps(r.json.apps) }),
|
|
53
|
+
// H07 (0.9.0): the person's stores here as the Hub holds them now (absent from a Hub before 0.9.0)
|
|
54
|
+
...(Array.isArray(r.json.stores) ? { stores: r.json.stores.filter((s) => typeof s === "string" && /^[a-z]{2}$/.test(s)) } : {}),
|
|
53
55
|
};
|
|
54
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* H09 (0.9.0): spend an entry ticket at the Hub, so it works once across every server of every module (the memory of
|
|
59
|
+
* one server is not enough on serverless). "first" = this is its first use; "used" = it was used before; "unknown" =
|
|
60
|
+
* the Hub did not say (the entry is refused - the ticket is a minute old, opening again from the Hub is cheap).
|
|
61
|
+
*/
|
|
62
|
+
export async function spendTicket(cfg, jti, exp) {
|
|
63
|
+
const r = await post(cfg, "/api/hub/ticket-used", { jti, exp });
|
|
64
|
+
if (r.status !== 200 || !r.json)
|
|
65
|
+
return "unknown";
|
|
66
|
+
if (r.json.first === true)
|
|
67
|
+
return "first";
|
|
68
|
+
if (r.json.first === false)
|
|
69
|
+
return "used";
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
55
72
|
/** 7.3: the store's token from the Hub, cached until an hour before it expires. */
|
|
56
73
|
export function tokenBroker(cfg) {
|
|
57
74
|
const cache = new Map();
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ export { Guard, openingPage, type GuardConfig, type CheckResult } from "./guard.
|
|
|
2
2
|
export { appMenu, readApps, APP_ID_RE, type AppMenuItem, type HubApp } from "./apps.js";
|
|
3
3
|
export { pageAllowed, pageAccess, pagesFingerprint, readPages, PAGE_KEY_RE, type PageAccess, type PageDef, type Pages } from "./pages.js";
|
|
4
4
|
export { verifyTicket, JtiMemory, TICKET_ISSUER, TICKET_TTL_S, TICKET_ALG, type TicketClaims, type VerifyResult } from "./ticket.js";
|
|
5
|
-
export { readSession, signSession, cookieHeader, clearCookieHeader, cookieValue, SESSION_COOKIE, IDLE_MS, ABSOLUTE_MS, RECHECK_MS, type ModuleSession } from "./session.js";
|
|
6
|
-
export { sessionCheck, sessionCheckPages, tokenBroker, audit, alert, verifyHubCall, manifestResponse, healthResponse, healthAnswer, activityResponse, readActivityQuery, cleanActivityLine, ACTIVITY_AT_RE, ACTIVITY_ID_RE, ACTIVITY_MAX_LIMIT, type ActivityLine, type ActivityQuery, type HubClientConfig, type HubCallCheck, type AnswerConfig, type BrokerToken, type Connection, } from "./hub-client.js";
|
|
5
|
+
export { readSession, signSession, cookieHeader, clearCookieHeader, cookieValue, SESSION_COOKIE, IDLE_MS, ABSOLUTE_MS, RECHECK_MS, MAX_STALE_MS, type ModuleSession } from "./session.js";
|
|
6
|
+
export { sessionCheck, sessionCheckPages, spendTicket, tokenBroker, audit, alert, verifyHubCall, manifestResponse, healthResponse, healthAnswer, activityResponse, readActivityQuery, cleanActivityLine, ACTIVITY_AT_RE, ACTIVITY_ID_RE, ACTIVITY_MAX_LIMIT, type ActivityLine, type ActivityQuery, type HubClientConfig, type HubCallCheck, type AnswerConfig, type BrokerToken, type Connection, } from "./hub-client.js";
|
|
7
7
|
export { checkCloudflareAccess } from "./cf-access.js";
|
|
8
8
|
export { verifyRelay, RELAY_HEADERS, RELAY_MAX_AGE_S, type RelayCheck } from "./relay.js";
|
|
9
9
|
export { verifyStationCall, stationRefusal, stationJson, STATION_ID_RE, STATION_NAME_MAX, type StationCheck, type StationAnswer, type StationOrder, type StationItem, type PackAnswer, } from "./station.js";
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@ export { Guard, openingPage } from "./guard.js";
|
|
|
2
2
|
export { appMenu, readApps, APP_ID_RE } from "./apps.js";
|
|
3
3
|
export { pageAllowed, pageAccess, pagesFingerprint, readPages, PAGE_KEY_RE } from "./pages.js";
|
|
4
4
|
export { verifyTicket, JtiMemory, TICKET_ISSUER, TICKET_TTL_S, TICKET_ALG } from "./ticket.js";
|
|
5
|
-
export { readSession, signSession, cookieHeader, clearCookieHeader, cookieValue, SESSION_COOKIE, IDLE_MS, ABSOLUTE_MS, RECHECK_MS } from "./session.js";
|
|
6
|
-
export { sessionCheck, sessionCheckPages, tokenBroker, audit, alert, verifyHubCall, manifestResponse, healthResponse, healthAnswer, activityResponse, readActivityQuery, cleanActivityLine, ACTIVITY_AT_RE, ACTIVITY_ID_RE, ACTIVITY_MAX_LIMIT, } from "./hub-client.js";
|
|
5
|
+
export { readSession, signSession, cookieHeader, clearCookieHeader, cookieValue, SESSION_COOKIE, IDLE_MS, ABSOLUTE_MS, RECHECK_MS, MAX_STALE_MS } from "./session.js";
|
|
6
|
+
export { sessionCheck, sessionCheckPages, spendTicket, tokenBroker, audit, alert, verifyHubCall, manifestResponse, healthResponse, healthAnswer, activityResponse, readActivityQuery, cleanActivityLine, ACTIVITY_AT_RE, ACTIVITY_ID_RE, ACTIVITY_MAX_LIMIT, } from "./hub-client.js";
|
|
7
7
|
export { checkCloudflareAccess } from "./cf-access.js";
|
|
8
8
|
export { verifyRelay, RELAY_HEADERS, RELAY_MAX_AGE_S } from "./relay.js";
|
|
9
9
|
export { verifyStationCall, stationRefusal, stationJson, STATION_ID_RE, STATION_NAME_MAX, } from "./station.js";
|
package/dist/pages.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare function pageAccess(session: {
|
|
|
26
26
|
export declare function pageAllowed(session: {
|
|
27
27
|
role: string;
|
|
28
28
|
pages?: Pages;
|
|
29
|
+
stale?: boolean;
|
|
29
30
|
}, key: string, need: "read" | "write"): boolean;
|
|
30
31
|
/** The fingerprint of a page list - the same one the Hub computes for the list it read (§14.5). */
|
|
31
32
|
export declare function pagesFingerprint(pages: PageDef[]): Promise<string>;
|
package/dist/pages.js
CHANGED
|
@@ -22,6 +22,9 @@ export function pageAccess(session, key) {
|
|
|
22
22
|
* change (`"write"`) this page? Ask it on the server - hiding a button is not enough (§14.3).
|
|
23
23
|
*/
|
|
24
24
|
export function pageAllowed(session, key, need) {
|
|
25
|
+
// H08 (0.9.0): while the Hub has not answered for 180 minutes nobody changes anything - the owner included
|
|
26
|
+
if (need === "write" && session.stale)
|
|
27
|
+
return false;
|
|
25
28
|
const a = pageAccess(session, key);
|
|
26
29
|
return need === "read" ? a === "r" || a === "w" : a === "w";
|
|
27
30
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ export declare const SESSION_COOKIE = "t3b_module_session";
|
|
|
4
4
|
export declare const IDLE_MS: number;
|
|
5
5
|
export declare const ABSOLUTE_MS: number;
|
|
6
6
|
export declare const RECHECK_MS: number;
|
|
7
|
+
/**
|
|
8
|
+
* H08 (0.9.0): how long a module keeps allowing changes while the Hub does not answer its check - the same 180 minutes
|
|
9
|
+
* as the idle rule. Past it the module still opens and reads, but every change waits until the Hub answers again (a
|
|
10
|
+
* person the owner removed during an outage cannot keep changing things for the session's whole 8 hours).
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_STALE_MS: number;
|
|
7
13
|
export type ModuleSession = {
|
|
8
14
|
personId: string;
|
|
9
15
|
name: string;
|
|
@@ -17,6 +23,8 @@ export type ModuleSession = {
|
|
|
17
23
|
pages?: Pages;
|
|
18
24
|
/** §18 (0.8.0): the apps this person may open (the Apps button), from the pass, refreshed by every 15-minute check */
|
|
19
25
|
apps?: HubApp[];
|
|
26
|
+
/** H08 (0.9.0): set by `check` (never stored) when the Hub has not answered for MAX_STALE_MS - no changes */
|
|
27
|
+
stale?: boolean;
|
|
20
28
|
};
|
|
21
29
|
export declare function signSession(s: ModuleSession, secret: string): Promise<string>;
|
|
22
30
|
export declare function readSession(cookie: string | undefined | null, secret: string, now?: number, opts?: {
|
package/dist/session.js
CHANGED
|
@@ -7,8 +7,16 @@ export const SESSION_COOKIE = "t3b_module_session";
|
|
|
7
7
|
export const IDLE_MS = 180 * 60_000;
|
|
8
8
|
export const ABSOLUTE_MS = 8 * 3_600_000;
|
|
9
9
|
export const RECHECK_MS = 15 * 60_000;
|
|
10
|
+
/**
|
|
11
|
+
* H08 (0.9.0): how long a module keeps allowing changes while the Hub does not answer its check - the same 180 minutes
|
|
12
|
+
* as the idle rule. Past it the module still opens and reads, but every change waits until the Hub answers again (a
|
|
13
|
+
* person the owner removed during an outage cannot keep changing things for the session's whole 8 hours).
|
|
14
|
+
*/
|
|
15
|
+
export const MAX_STALE_MS = 180 * 60_000;
|
|
10
16
|
export async function signSession(s, secret) {
|
|
11
|
-
const
|
|
17
|
+
const { stale: _stale, ...kept } = s; // computed on every request, never carried in the cookie
|
|
18
|
+
void _stale;
|
|
19
|
+
const body = b64urlEncode(utf8(JSON.stringify(kept)));
|
|
12
20
|
const mac = b64urlEncode(await hmacSha256(secret, body));
|
|
13
21
|
return `${body}.${mac}`;
|
|
14
22
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t3b/hub-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The3Beez Hub guard: a module opens only by a Hub-signed ticket and refuses everything else. No key from the Hub: towards the Hub the app shows its own Vercel ID; from the Hub it accepts only what is signed by the key the Hub publishes. Entry route, refusal page, module session, idle rule, 15-minute re-check, Cloudflare Access proof, broker and audit clients, manifest and health answers for the Hub only. Web Crypto only: runs in Node 20+ and in edge runtimes.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|