@kedge/server 0.0.0-stage → 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/LICENSE +21 -0
- package/package.json +29 -3
- package/src/access.ts +472 -0
- package/src/admin-routes.ts +737 -0
- package/src/admin-shell.ts +72 -0
- package/src/app.ts +798 -0
- package/src/auth-routes.ts +381 -0
- package/src/capacity-routes.ts +440 -0
- package/src/entity-routes.ts +323 -0
- package/src/events.ts +84 -0
- package/src/http.ts +16 -0
- package/src/index.ts +2 -0
- package/src/meta-routes.ts +277 -0
- package/src/plugin-routes.ts +40 -0
- package/src/relay-routes.ts +192 -0
- package/src/track-routes.ts +102 -0
- package/src/types.ts +809 -0
- package/src/units/store.order.view.ts +9 -0
- package/src/units/store.product.view.ts +9 -0
- package/src/webhook-routes.ts +362 -0
- package/README.md +0 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The admin's half of the gate (docs/proposals/p0.9-admin-shell.md §2.4, §4.1,
|
|
2
|
+
// §6.3, §9): the Worker's own origin as an admin origin, the two headers that
|
|
3
|
+
// say which session and which authority answered, and the two handshakes that
|
|
4
|
+
// refuse a stale admin or a stale view before any route runs.
|
|
5
|
+
import { type AccessContext, KedgeError } from "@kedge/core";
|
|
6
|
+
import type { Context } from "hono";
|
|
7
|
+
import type { AuthDeps } from "./types";
|
|
8
|
+
|
|
9
|
+
/** Header names, one place. */
|
|
10
|
+
export const ADMIN_BUILD_HEADER = "x-kedge-admin-build";
|
|
11
|
+
export const AUTHZ_EXPECT_HEADER = "x-kedge-authz-expect";
|
|
12
|
+
export const SESSION_HEADER = "X-Kedge-Session";
|
|
13
|
+
export const AUTHZ_HEADER = "X-Kedge-Authz";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `ADMIN_ORIGINS` plus the request's own origin (§4.1). That origin serves only
|
|
17
|
+
* this Worker's API and its own admin assets, so a page on it is the admin: a
|
|
18
|
+
* fresh store, a preview whose URL is known only after its config is written,
|
|
19
|
+
* and `kedge dev` all sign in with no configuration.
|
|
20
|
+
*/
|
|
21
|
+
export function adminOriginsFor(c: Context, auth: AuthDeps): readonly string[] {
|
|
22
|
+
return [...auth.adminOrigins, new URL(c.req.url).origin];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
const sha256 = async (text: string) => hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What one request was authorized as (§9): the principal, the session's method,
|
|
30
|
+
* the effective grants with their row scopes, the member's locations, and the
|
|
31
|
+
* deployment's access contract. A change in any of them changes what an
|
|
32
|
+
* answer may show, so the admin drops what it rendered under the old one.
|
|
33
|
+
* The session's id is left out on purpose: a re-sign-in as the same principal
|
|
34
|
+
* keeps a pending command's view (§6.3).
|
|
35
|
+
*/
|
|
36
|
+
export async function authzFingerprint(ctx: AccessContext, method: string | null, contract: string): Promise<string> {
|
|
37
|
+
const grants = [...ctx.grants.entries()]
|
|
38
|
+
.map(([permission, scopes]) => [permission, [...scopes].sort()] as const)
|
|
39
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
40
|
+
const principal = "id" in ctx.principal ? `${ctx.principal.kind}:${ctx.principal.id}` : ctx.principal.kind;
|
|
41
|
+
return sha256(JSON.stringify({ v: 1, principal, method, grants, locations: [...ctx.locations].sort(), contract }));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The build handshake (§2.4): a request naming another admin build is refused
|
|
46
|
+
* before any route runs, reads included. A Worker with no admin build (no
|
|
47
|
+
* admin bundled, or a test) refuses every admin-build header, since no build
|
|
48
|
+
* it could serve matches. Clients that send no header are untouched.
|
|
49
|
+
*/
|
|
50
|
+
export function checkAdminBuild(c: Context, build: string | null): void {
|
|
51
|
+
const sent = c.req.header(ADMIN_BUILD_HEADER);
|
|
52
|
+
if (sent === undefined) return;
|
|
53
|
+
if (build === null || sent !== build)
|
|
54
|
+
throw new KedgeError("ADMIN_OUTDATED", "The admin was updated; load it again", { build });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The view handshake (§6.3): a mutation made from a view this request no
|
|
59
|
+
* longer has (another principal, narrowed grants, a deploy that changed the
|
|
60
|
+
* access contract) is refused before admission or execution.
|
|
61
|
+
*/
|
|
62
|
+
export function checkView(c: Context, fingerprint: string): void {
|
|
63
|
+
const sent = c.req.header(AUTHZ_EXPECT_HEADER);
|
|
64
|
+
if (sent !== undefined && sent !== fingerprint)
|
|
65
|
+
throw new KedgeError("VIEW_CHANGED", "What you may see or do changed since this form loaded; reload it");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Names the session and the authority on every staff answer, errors included (§9). */
|
|
69
|
+
export function nameAnswer(c: Context, sessionId: string, fingerprint: string): void {
|
|
70
|
+
c.header(SESSION_HEADER, sessionId);
|
|
71
|
+
c.header(AUTHZ_HEADER, fingerprint);
|
|
72
|
+
}
|