@impetik/xeer-mcp 0.2.5 → 0.2.6

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.
Files changed (54) hide show
  1. package/README.md +4 -1
  2. package/dist/dev-session.d.ts +1 -1
  3. package/dist/network-policy.js +1 -1
  4. package/dist/server.d.ts +1 -1
  5. package/dist/server.js +4 -4
  6. package/dist/test-run.d.ts +1 -1
  7. package/dist/xeer-cli.d.ts +1 -1
  8. package/package.json +8 -5
  9. package/vendor/spec/actions.d.ts +1250 -0
  10. package/vendor/spec/actions.js +805 -0
  11. package/vendor/spec/admin-sql.d.ts +59 -0
  12. package/vendor/spec/admin-sql.js +147 -0
  13. package/vendor/spec/admin.d.ts +110 -0
  14. package/vendor/spec/admin.js +58 -0
  15. package/vendor/spec/canonical.d.ts +3 -0
  16. package/vendor/spec/canonical.js +36 -0
  17. package/vendor/spec/diagnostics.d.ts +49 -0
  18. package/vendor/spec/diagnostics.js +500 -0
  19. package/vendor/spec/docs.d.ts +21 -0
  20. package/vendor/spec/docs.js +57 -0
  21. package/vendor/spec/events.d.ts +8 -0
  22. package/vendor/spec/events.js +21 -0
  23. package/vendor/spec/identity-keys.d.ts +36 -0
  24. package/vendor/spec/identity-keys.js +72 -0
  25. package/vendor/spec/index.d.ts +20 -0
  26. package/vendor/spec/index.js +20 -0
  27. package/vendor/spec/local-identity.d.ts +69 -0
  28. package/vendor/spec/local-identity.js +132 -0
  29. package/vendor/spec/network-policy.d.ts +16 -0
  30. package/vendor/spec/network-policy.js +50 -0
  31. package/vendor/spec/public-assets.d.ts +153 -0
  32. package/vendor/spec/public-assets.js +166 -0
  33. package/vendor/spec/review.d.ts +82 -0
  34. package/vendor/spec/review.js +175 -0
  35. package/vendor/spec/route.d.ts +43 -0
  36. package/vendor/spec/route.js +87 -0
  37. package/vendor/spec/schema-lifecycle.d.ts +6 -0
  38. package/vendor/spec/schema-lifecycle.js +59 -0
  39. package/vendor/spec/schema-plan.d.ts +98 -0
  40. package/vendor/spec/schema-plan.js +194 -0
  41. package/vendor/spec/schema.d.ts +166 -0
  42. package/vendor/spec/schema.js +409 -0
  43. package/vendor/spec/sql-expression.d.ts +91 -0
  44. package/vendor/spec/sql-expression.js +650 -0
  45. package/vendor/spec/state-export.d.ts +143 -0
  46. package/vendor/spec/state-export.js +341 -0
  47. package/vendor/spec/storage.d.ts +61 -0
  48. package/vendor/spec/storage.js +120 -0
  49. package/vendor/spec/table-ddl.d.ts +162 -0
  50. package/vendor/spec/table-ddl.js +508 -0
  51. package/vendor/spec/types.d.ts +275 -0
  52. package/vendor/spec/types.js +11 -0
  53. package/vendor/spec/value.d.ts +22 -0
  54. package/vendor/spec/value.js +72 -0
@@ -0,0 +1,21 @@
1
+ import { DEV_PROTOCOL } from './types.js';
2
+ export class DevEventWriter {
3
+ writeLine;
4
+ now;
5
+ #sequence = 0;
6
+ constructor(writeLine, now = () => new Date()) {
7
+ this.writeLine = writeLine;
8
+ this.now = now;
9
+ }
10
+ emit(type, data) {
11
+ const event = {
12
+ protocol: DEV_PROTOCOL,
13
+ seq: ++this.#sequence,
14
+ time: this.now().toISOString(),
15
+ type,
16
+ data,
17
+ };
18
+ this.writeLine(JSON.stringify(event));
19
+ return event;
20
+ }
21
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Identity-assertion verification key set: the wire format of the `XEER_IDENTITY_PUBLIC_KEY`
3
+ * binding shared by the control plane (which attaches it to every user Worker) and the runtime
4
+ * verifier (which enforces it as a `kid` allowlist).
5
+ *
6
+ * Two shapes are accepted:
7
+ *
8
+ * `<publicKeyRaw>` single unversioned key; accepts any signing key id
9
+ * `<keyId>:<publicKeyRaw>[,<keyId>:<publicKeyRaw>]…` versioned key set; strict allowlist
10
+ *
11
+ * Rotation keeps the outgoing and incoming key ids in the set for an overlap window no shorter
12
+ * than the maximum assertion lifetime plus clock skew (docs/AUTH_THREAT_MODEL.md, signing-key
13
+ * compromise row), then removes the outgoing key id.
14
+ */
15
+ export declare const IDENTITY_KEY_SET_FORMAT: "xeer.identity-key-set.v0";
16
+ export interface IdentityVerificationKey {
17
+ /** `null` only for the legacy single-key configuration, which accepts any signing key id. */
18
+ readonly keyId: string | null;
19
+ /** base64url-encoded raw 32-byte Ed25519 public key. Verification-only material. */
20
+ readonly publicKeyRaw: string;
21
+ }
22
+ export declare class IdentityKeySetError extends Error {
23
+ readonly code = "invalid_identity_key_set";
24
+ constructor(message: string);
25
+ }
26
+ /** Parses the binding value, failing closed on an absent, malformed, or ambiguous key set. */
27
+ export declare function parseIdentityKeySet(configured: string | null | undefined): readonly IdentityVerificationKey[];
28
+ /**
29
+ * Resolves the key a signing key id is allowed to use. A versioned set is a strict allowlist:
30
+ * an unknown key id resolves to `null` so the caller fails closed.
31
+ */
32
+ export declare function selectIdentityVerificationKey(keys: readonly IdentityVerificationKey[], keyId: string | null | undefined): IdentityVerificationKey | null;
33
+ /** Non-secret key ids, safe for readiness diagnostics; `*` marks the unversioned legacy key. */
34
+ export declare function identityKeySetIds(keys: readonly IdentityVerificationKey[]): readonly string[];
35
+ /** Canonical binding text: comma-separated, key id qualified whenever the input was. */
36
+ export declare function formatIdentityKeySet(keys: readonly IdentityVerificationKey[]): string;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Identity-assertion verification key set: the wire format of the `XEER_IDENTITY_PUBLIC_KEY`
3
+ * binding shared by the control plane (which attaches it to every user Worker) and the runtime
4
+ * verifier (which enforces it as a `kid` allowlist).
5
+ *
6
+ * Two shapes are accepted:
7
+ *
8
+ * `<publicKeyRaw>` single unversioned key; accepts any signing key id
9
+ * `<keyId>:<publicKeyRaw>[,<keyId>:<publicKeyRaw>]…` versioned key set; strict allowlist
10
+ *
11
+ * Rotation keeps the outgoing and incoming key ids in the set for an overlap window no shorter
12
+ * than the maximum assertion lifetime plus clock skew (docs/AUTH_THREAT_MODEL.md, signing-key
13
+ * compromise row), then removes the outgoing key id.
14
+ */
15
+ export const IDENTITY_KEY_SET_FORMAT = 'xeer.identity-key-set.v0';
16
+ /** Ed25519 raw public keys are 32 bytes, i.e. exactly 43 unpadded base64url characters. */
17
+ const RAW_PUBLIC_KEY = /^[A-Za-z0-9_-]{43}$/u;
18
+ const KEY_ID = /^[A-Za-z0-9_-]{1,64}$/u;
19
+ const UNVERSIONED_KEY_LABEL = '*';
20
+ export class IdentityKeySetError extends Error {
21
+ code = 'invalid_identity_key_set';
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'IdentityKeySetError';
25
+ }
26
+ }
27
+ /** Parses the binding value, failing closed on an absent, malformed, or ambiguous key set. */
28
+ export function parseIdentityKeySet(configured) {
29
+ const entries = String(configured ?? '').trim().split(/[\s,;]+/u).filter((entry) => entry.length > 0);
30
+ if (entries.length === 0) {
31
+ throw new IdentityKeySetError('No identity-assertion verification key is configured.');
32
+ }
33
+ const keys = [];
34
+ const seen = new Set();
35
+ for (const entry of entries) {
36
+ const separator = entry.indexOf(':');
37
+ const keyId = separator < 0 ? null : entry.slice(0, separator);
38
+ const publicKeyRaw = separator < 0 ? entry : entry.slice(separator + 1);
39
+ if (keyId === null && entries.length > 1) {
40
+ throw new IdentityKeySetError('Every key in a multi-key identity key set must carry its key id.');
41
+ }
42
+ if (keyId !== null && !KEY_ID.test(keyId)) {
43
+ throw new IdentityKeySetError(`Identity key id is invalid: ${JSON.stringify(keyId)}`);
44
+ }
45
+ if (!RAW_PUBLIC_KEY.test(publicKeyRaw)) {
46
+ throw new IdentityKeySetError(`Identity verification key ${keyId ?? UNVERSIONED_KEY_LABEL} must be a base64url raw 32-byte Ed25519 public key.`);
47
+ }
48
+ if (seen.has(keyId ?? UNVERSIONED_KEY_LABEL)) {
49
+ throw new IdentityKeySetError(`Identity key id is duplicated: ${keyId ?? UNVERSIONED_KEY_LABEL}`);
50
+ }
51
+ seen.add(keyId ?? UNVERSIONED_KEY_LABEL);
52
+ keys.push(Object.freeze({ keyId, publicKeyRaw }));
53
+ }
54
+ return Object.freeze(keys);
55
+ }
56
+ /**
57
+ * Resolves the key a signing key id is allowed to use. A versioned set is a strict allowlist:
58
+ * an unknown key id resolves to `null` so the caller fails closed.
59
+ */
60
+ export function selectIdentityVerificationKey(keys, keyId) {
61
+ return keys.find((key) => key.keyId !== null && key.keyId === keyId)
62
+ ?? keys.find((key) => key.keyId === null)
63
+ ?? null;
64
+ }
65
+ /** Non-secret key ids, safe for readiness diagnostics; `*` marks the unversioned legacy key. */
66
+ export function identityKeySetIds(keys) {
67
+ return Object.freeze(keys.map((key) => key.keyId ?? UNVERSIONED_KEY_LABEL));
68
+ }
69
+ /** Canonical binding text: comma-separated, key id qualified whenever the input was. */
70
+ export function formatIdentityKeySet(keys) {
71
+ return keys.map((key) => (key.keyId === null ? key.publicKeyRaw : `${key.keyId}:${key.publicKeyRaw}`)).join(',');
72
+ }
@@ -0,0 +1,20 @@
1
+ export * from './types.js';
2
+ export * from './diagnostics.js';
3
+ export * from './schema.js';
4
+ export * from './storage.js';
5
+ export * from './canonical.js';
6
+ export * from './public-assets.js';
7
+ export * from './events.js';
8
+ export * from './value.js';
9
+ export * from './route.js';
10
+ export * from './identity-keys.js';
11
+ export * from './local-identity.js';
12
+ export * from './schema-lifecycle.js';
13
+ export * from './sql-expression.js';
14
+ export * from './table-ddl.js';
15
+ export * from './state-export.js';
16
+ export * from './actions.js';
17
+ export * from './admin.js';
18
+ export * from './admin-sql.js';
19
+ export * from './network-policy.js';
20
+ export * from './review.js';
@@ -0,0 +1,20 @@
1
+ export * from './types.js';
2
+ export * from './diagnostics.js';
3
+ export * from './schema.js';
4
+ export * from './storage.js';
5
+ export * from './canonical.js';
6
+ export * from './public-assets.js';
7
+ export * from './events.js';
8
+ export * from './value.js';
9
+ export * from './route.js';
10
+ export * from './identity-keys.js';
11
+ export * from './local-identity.js';
12
+ export * from './schema-lifecycle.js';
13
+ export * from './sql-expression.js';
14
+ export * from './table-ddl.js';
15
+ export * from './state-export.js';
16
+ export * from './actions.js';
17
+ export * from './admin.js';
18
+ export * from './admin-sql.js';
19
+ export * from './network-policy.js';
20
+ export * from './review.js';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The local persona model shared by `xeer dev`, `xeer preview`, and `xeer test`.
3
+ *
4
+ * A local persona is a deterministic stand-in for an identity the platform would otherwise mint in
5
+ * a signed assertion. It carries the same two facts production membership carries — who the caller
6
+ * is, and which workspaces they belong to — so `workspaceTable()` policies and `memberOf()` guards
7
+ * are exercisable before anything is deployed.
8
+ *
9
+ * Everything here is a pure value contract, deliberately free of platform and Node dependencies:
10
+ * the runtime parses these cookies, the CLI writes them, and the test host serializes them, so the
11
+ * three must agree on one definition or local identity silently diverges between them.
12
+ *
13
+ * Workspace ids are the labels the builder declares. There is no hidden derivation: the same label
14
+ * is the same workspace on every machine, in `xeer dev` and in `xeer test`, which is what makes a
15
+ * membership assertion reproducible. The accepted shape is the *production* assertion charset (see
16
+ * `workspaceIds` in the identity assertion payload), so an id that works locally is an id the
17
+ * hosted auth service could also issue.
18
+ */
19
+ export declare const LOCAL_PERSONA_COOKIE = "xeer_local_persona";
20
+ export declare const LOCAL_WORKSPACE_COOKIE = "xeer_local_workspaces";
21
+ /** Deterministic local identities. Each persona is a distinct application user. */
22
+ export type LocalPersonaName = 'alice' | 'bob' | 'guest';
23
+ export declare const LOCAL_PERSONA_NAMES: readonly LocalPersonaName[];
24
+ /**
25
+ * Personas the local sign-in route may select. `guest` is deliberately absent: it is the unclaimed
26
+ * visitor a test uses to prove that neither alice's nor bob's rows leak, not a session to log into.
27
+ */
28
+ export declare const LOCAL_SIGN_IN_PERSONA_NAMES: readonly LocalPersonaName[];
29
+ /** The identity-assertion charset for a workspace id, so local ids stay production-shaped. */
30
+ export declare const LOCAL_WORKSPACE_ID_PATTERN: RegExp;
31
+ /**
32
+ * Local membership is a development fixture, not a directory. The cap keeps the cookie well inside
33
+ * every browser's per-cookie budget while staying far above what a local isolation test needs.
34
+ */
35
+ export declare const MAXIMUM_LOCAL_WORKSPACES = 16;
36
+ /** An unusable local membership specification. Always a builder input error, never a denial. */
37
+ export declare class LocalWorkspaceMembershipError extends Error {
38
+ readonly code: "invalid_local_workspace";
39
+ constructor(message: string);
40
+ }
41
+ export declare function isLocalPersonaName(value: unknown): value is LocalPersonaName;
42
+ /**
43
+ * Validates and de-duplicates a declared membership set, preserving declaration order so the
44
+ * single-workspace insert default (`preparePolicyInsert`) is predictable.
45
+ */
46
+ export declare function normalizeLocalWorkspaceIds(values: readonly unknown[]): readonly string[];
47
+ /**
48
+ * The one place the persona/membership pairing rule lives: only a provisioned identity can hold
49
+ * membership, and `guest` is by definition unprovisioned. A local persona that holds membership is
50
+ * therefore minted as `kind: "user"` — see `localPersonaAssertion` in the runtime.
51
+ */
52
+ export declare function normalizeLocalMembership(persona: LocalPersonaName, workspaceIds: readonly unknown[]): readonly string[];
53
+ /** True when a persona holding this membership is a provisioned user rather than a guest. */
54
+ export declare function localPersonaKind(workspaceIds: readonly string[]): 'guest' | 'user';
55
+ /** Parses one comma-separated membership list, as written in a cookie or a query parameter. */
56
+ export declare function parseLocalWorkspaceList(value: string | null | undefined): readonly string[];
57
+ /** The persona a session chose, or null when it made no choice and the baked default applies. */
58
+ export declare function readLocalPersonaCookie(cookie: string | null | undefined): LocalPersonaName | null;
59
+ /**
60
+ * The membership a session chose. An absent, empty, or malformed cookie is no membership: local
61
+ * identity fails closed, exactly as an assertion with an empty `workspaceIds` does.
62
+ */
63
+ export declare function readLocalWorkspaceCookie(cookie: string | null | undefined): readonly string[];
64
+ export declare function localWorkspaceCookieValue(workspaceIds: readonly string[]): string;
65
+ /**
66
+ * The complete local identity selection as a single `cookie` request header. `xeer test` sends this
67
+ * for every call, so a test persona reaches the runtime through the same seam a browser uses.
68
+ */
69
+ export declare function localIdentityCookieHeader(persona: LocalPersonaName, workspaceIds?: readonly string[]): string;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The local persona model shared by `xeer dev`, `xeer preview`, and `xeer test`.
3
+ *
4
+ * A local persona is a deterministic stand-in for an identity the platform would otherwise mint in
5
+ * a signed assertion. It carries the same two facts production membership carries — who the caller
6
+ * is, and which workspaces they belong to — so `workspaceTable()` policies and `memberOf()` guards
7
+ * are exercisable before anything is deployed.
8
+ *
9
+ * Everything here is a pure value contract, deliberately free of platform and Node dependencies:
10
+ * the runtime parses these cookies, the CLI writes them, and the test host serializes them, so the
11
+ * three must agree on one definition or local identity silently diverges between them.
12
+ *
13
+ * Workspace ids are the labels the builder declares. There is no hidden derivation: the same label
14
+ * is the same workspace on every machine, in `xeer dev` and in `xeer test`, which is what makes a
15
+ * membership assertion reproducible. The accepted shape is the *production* assertion charset (see
16
+ * `workspaceIds` in the identity assertion payload), so an id that works locally is an id the
17
+ * hosted auth service could also issue.
18
+ */
19
+ export const LOCAL_PERSONA_COOKIE = 'xeer_local_persona';
20
+ export const LOCAL_WORKSPACE_COOKIE = 'xeer_local_workspaces';
21
+ export const LOCAL_PERSONA_NAMES = Object.freeze(['alice', 'bob', 'guest']);
22
+ /**
23
+ * Personas the local sign-in route may select. `guest` is deliberately absent: it is the unclaimed
24
+ * visitor a test uses to prove that neither alice's nor bob's rows leak, not a session to log into.
25
+ */
26
+ export const LOCAL_SIGN_IN_PERSONA_NAMES = Object.freeze(['alice', 'bob']);
27
+ /** The identity-assertion charset for a workspace id, so local ids stay production-shaped. */
28
+ export const LOCAL_WORKSPACE_ID_PATTERN = /^[A-Za-z0-9:_-]{1,96}$/u;
29
+ /**
30
+ * Local membership is a development fixture, not a directory. The cap keeps the cookie well inside
31
+ * every browser's per-cookie budget while staying far above what a local isolation test needs.
32
+ */
33
+ export const MAXIMUM_LOCAL_WORKSPACES = 16;
34
+ /** An unusable local membership specification. Always a builder input error, never a denial. */
35
+ export class LocalWorkspaceMembershipError extends Error {
36
+ code = 'invalid_local_workspace';
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = 'LocalWorkspaceMembershipError';
40
+ }
41
+ }
42
+ export function isLocalPersonaName(value) {
43
+ return typeof value === 'string' && LOCAL_PERSONA_NAMES.includes(value);
44
+ }
45
+ /**
46
+ * Validates and de-duplicates a declared membership set, preserving declaration order so the
47
+ * single-workspace insert default (`preparePolicyInsert`) is predictable.
48
+ */
49
+ export function normalizeLocalWorkspaceIds(values) {
50
+ const normalized = [];
51
+ for (const value of values) {
52
+ if (typeof value !== 'string' || !LOCAL_WORKSPACE_ID_PATTERN.test(value)) {
53
+ throw new LocalWorkspaceMembershipError(`Invalid workspace id ${JSON.stringify(String(value))}: use 1-96 characters from A-Z, a-z, 0-9, ':', '_', and '-'.`);
54
+ }
55
+ if (!normalized.includes(value))
56
+ normalized.push(value);
57
+ }
58
+ if (normalized.length > MAXIMUM_LOCAL_WORKSPACES) {
59
+ throw new LocalWorkspaceMembershipError(`A local persona may hold at most ${MAXIMUM_LOCAL_WORKSPACES} workspaces; ${normalized.length} were declared.`);
60
+ }
61
+ return Object.freeze(normalized);
62
+ }
63
+ /**
64
+ * The one place the persona/membership pairing rule lives: only a provisioned identity can hold
65
+ * membership, and `guest` is by definition unprovisioned. A local persona that holds membership is
66
+ * therefore minted as `kind: "user"` — see `localPersonaAssertion` in the runtime.
67
+ */
68
+ export function normalizeLocalMembership(persona, workspaceIds) {
69
+ const normalized = normalizeLocalWorkspaceIds(workspaceIds);
70
+ if (persona === 'guest' && normalized.length > 0) {
71
+ throw new LocalWorkspaceMembershipError('The guest persona cannot carry workspace membership: it is the unclaimed visitor, and '
72
+ + 'membership is only ever held by a provisioned identity. Use alice or bob.');
73
+ }
74
+ return normalized;
75
+ }
76
+ /** True when a persona holding this membership is a provisioned user rather than a guest. */
77
+ export function localPersonaKind(workspaceIds) {
78
+ return workspaceIds.length > 0 ? 'user' : 'guest';
79
+ }
80
+ /** Parses one comma-separated membership list, as written in a cookie or a query parameter. */
81
+ export function parseLocalWorkspaceList(value) {
82
+ if (value === null || value === undefined)
83
+ return Object.freeze([]);
84
+ const trimmed = value.trim();
85
+ if (trimmed === '')
86
+ return Object.freeze([]);
87
+ return normalizeLocalWorkspaceIds(trimmed.split(',').map((entry) => entry.trim()));
88
+ }
89
+ function cookieValue(cookie, name) {
90
+ if (!cookie)
91
+ return null;
92
+ for (const part of cookie.split(';')) {
93
+ const separator = part.indexOf('=');
94
+ if (separator < 0)
95
+ continue;
96
+ if (part.slice(0, separator).trim() !== name)
97
+ continue;
98
+ return part.slice(separator + 1).trim();
99
+ }
100
+ return null;
101
+ }
102
+ /** The persona a session chose, or null when it made no choice and the baked default applies. */
103
+ export function readLocalPersonaCookie(cookie) {
104
+ const value = cookieValue(cookie, LOCAL_PERSONA_COOKIE);
105
+ return isLocalPersonaName(value) ? value : null;
106
+ }
107
+ /**
108
+ * The membership a session chose. An absent, empty, or malformed cookie is no membership: local
109
+ * identity fails closed, exactly as an assertion with an empty `workspaceIds` does.
110
+ */
111
+ export function readLocalWorkspaceCookie(cookie) {
112
+ try {
113
+ return parseLocalWorkspaceList(cookieValue(cookie, LOCAL_WORKSPACE_COOKIE));
114
+ }
115
+ catch {
116
+ return Object.freeze([]);
117
+ }
118
+ }
119
+ export function localWorkspaceCookieValue(workspaceIds) {
120
+ return normalizeLocalWorkspaceIds(workspaceIds).join(',');
121
+ }
122
+ /**
123
+ * The complete local identity selection as a single `cookie` request header. `xeer test` sends this
124
+ * for every call, so a test persona reaches the runtime through the same seam a browser uses.
125
+ */
126
+ export function localIdentityCookieHeader(persona, workspaceIds = []) {
127
+ const membership = normalizeLocalMembership(persona, workspaceIds);
128
+ const pairs = [`${LOCAL_PERSONA_COOKIE}=${persona}`];
129
+ if (membership.length > 0)
130
+ pairs.push(`${LOCAL_WORKSPACE_COOKIE}=${membership.join(',')}`);
131
+ return pairs.join('; ');
132
+ }
@@ -0,0 +1,16 @@
1
+ /** The hosted control plane that ordinary CLI and MCP calls use. */
2
+ export declare const DEFAULT_XEER_CONTROL_ORIGIN: "https://control.xeer.run";
3
+ /** True only for a literal loopback hostname after URL canonicalization. */
4
+ export declare function isLoopbackHostname(value: string): boolean;
5
+ /**
6
+ * Canonical HTTP(S) origin, or undefined when the value carries a path, credentials, query, fragment,
7
+ * or another scheme. This is the boundary used before a bearer credential may be sent anywhere.
8
+ */
9
+ export declare function exactHttpOrigin(value: string): string | undefined;
10
+ /** A direct local inspector target. HTTPS and non-loopback hosts are never treated as local. */
11
+ export declare function isLocalInspectorTarget(value: string): boolean;
12
+ /**
13
+ * An exact hosted Xeer application origin. It is safe as an application reference: the CLI sends it
14
+ * to the control plane for owner resolution and never fetches the application URL directly.
15
+ */
16
+ export declare function isHostedXeerApplicationTarget(value: string): boolean;
@@ -0,0 +1,50 @@
1
+ /** The hosted control plane that ordinary CLI and MCP calls use. */
2
+ export const DEFAULT_XEER_CONTROL_ORIGIN = 'https://control.xeer.run';
3
+ function parsedUrl(value) {
4
+ if (/[\u0000-\u001f\u007f]/u.test(value))
5
+ return undefined;
6
+ try {
7
+ return new URL(value.trim());
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
13
+ /** True only for a literal loopback hostname after URL canonicalization. */
14
+ export function isLoopbackHostname(value) {
15
+ const hostname = value.replace(/^\[(.*)\]$/u, '$1').toLowerCase();
16
+ if (hostname === 'localhost' || hostname === '::1')
17
+ return true;
18
+ const octets = hostname.split('.');
19
+ return octets.length === 4 && octets[0] === '127'
20
+ && octets.every((octet) => /^\d{1,3}$/u.test(octet) && Number(octet) <= 255);
21
+ }
22
+ /**
23
+ * Canonical HTTP(S) origin, or undefined when the value carries a path, credentials, query, fragment,
24
+ * or another scheme. This is the boundary used before a bearer credential may be sent anywhere.
25
+ */
26
+ export function exactHttpOrigin(value) {
27
+ const url = parsedUrl(value);
28
+ if (!url || (url.protocol !== 'http:' && url.protocol !== 'https:')
29
+ || url.pathname !== '/' || url.search || url.hash || url.username || url.password)
30
+ return undefined;
31
+ return url.origin;
32
+ }
33
+ /** A direct local inspector target. HTTPS and non-loopback hosts are never treated as local. */
34
+ export function isLocalInspectorTarget(value) {
35
+ const url = parsedUrl(value);
36
+ return url !== undefined && url.protocol === 'http:' && !url.username && !url.password
37
+ && isLoopbackHostname(url.hostname);
38
+ }
39
+ /**
40
+ * An exact hosted Xeer application origin. It is safe as an application reference: the CLI sends it
41
+ * to the control plane for owner resolution and never fetches the application URL directly.
42
+ */
43
+ export function isHostedXeerApplicationTarget(value) {
44
+ const url = parsedUrl(value);
45
+ if (!url || url.protocol !== 'https:' || url.port || url.pathname !== '/'
46
+ || url.search || url.hash || url.username || url.password)
47
+ return false;
48
+ const hostname = url.hostname.toLowerCase();
49
+ return hostname.length > '.xeer.run'.length && hostname.endsWith('.xeer.run');
50
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Deliberately free of imports. The control plane and the dispatcher are Workers, and this module is
3
+ * the one piece of the contract they both need; pulling in `canonical.ts` would drag `node:crypto`
4
+ * across that boundary for the sake of a helper neither of them calls.
5
+ */
6
+ export declare const PUBLIC_ASSETS_FORMAT: "xeer.assets.v0";
7
+ /**
8
+ * The one prefix under which a URL *is* a content address, and which no application may occupy.
9
+ *
10
+ * It does **not** bypass the dispatcher — every request to a deployed application is routed through
11
+ * it, so a cold asset request still pays origin resolution and authorization. What a dedicated
12
+ * prefix buys is narrower and worth stating exactly: it stays clear of `/_xeer/*` (dispatcher) and
13
+ * `/__xeer/*` (runtime), both of which are claimed before the assets are consulted; it gives the
14
+ * `_headers` rule an exclusive namespace, so the immutable directive can never reach a mutable path;
15
+ * and it gives the deployment somewhere to route a *miss*, which is what turns a stale reference into
16
+ * a 404 instead of the single-page-application fallback.
17
+ */
18
+ export declare const IMMUTABLE_ASSET_PREFIX: "/_xa/";
19
+ /**
20
+ * What a hashed path is served with, and the reason the naming scheme exists at all.
21
+ *
22
+ * `immutable` (RFC 8246) tells a browser not to revalidate on an ordinary reload; a force reload
23
+ * still revalidates, so this does not cost anyone a debugging tool. It is only ever safe because the
24
+ * URL is a digest of the bytes — the same URL can never legitimately answer with anything else — and
25
+ * that is why {@link immutableAssetPath} is the only sanctioned way to mint one.
26
+ */
27
+ export declare const IMMUTABLE_ASSET_CACHE_CONTROL: "public, max-age=31536000, immutable";
28
+ /**
29
+ * How much of the digest goes into the URL: all of it.
30
+ *
31
+ * A truncated digest was the first design and it was wrong. The argument for it — that collisions are
32
+ * inconceivable across one application's three generated files — answers the wrong question. The
33
+ * threat model includes attacker-influenced bytes, and against a *constructed* pair the bound is the
34
+ * birthday bound over the retained prefix, not the second-preimage bound over the whole digest. At
35
+ * 128 bits that is 2^64 work to mint two files sharing a URL, one of which then inherits a one-year
36
+ * `immutable` cache entry that no later deployment can withdraw. Thirty-two more characters of URL is
37
+ * not a price worth arguing about against that.
38
+ *
39
+ * It also makes a property the rest of the system leans on actually true: equal paths imply equal
40
+ * bytes.
41
+ */
42
+ export declare const IMMUTABLE_ASSET_HASH_LENGTH = 64;
43
+ /**
44
+ * Upper bound on {@link PublicAssetsV0.retainedGenerations}.
45
+ *
46
+ * One, because one is what the control plane implements. A wider range was declared first and that
47
+ * was a promise the code did not keep: an artifact could ask for four generations and silently get
48
+ * one. A contract that can express a value nothing honours is worse than a narrow contract.
49
+ */
50
+ export declare const MAX_RETAINED_ASSET_GENERATIONS = 1;
51
+ /**
52
+ * What a declared entry is an alias *of*. Kept as a string rather than a structured pair because its
53
+ * only consumers are humans reading an artifact and a diagnostic naming the file that produced a
54
+ * collision — nothing branches on it.
55
+ */
56
+ export type PublicAssetOrigin = `module:${string}` | `asset:${string}` | 'document';
57
+ export interface ImmutableAssetV0 {
58
+ /**
59
+ * `${immutablePrefix}${hash.slice(7)}/${basename}`, and {@link parsePublicAssets} recomputes it
60
+ * rather than trusting it.
61
+ *
62
+ * That check alone is not sufficient and must never be mistaken for sufficient: it proves the path
63
+ * agrees with the *declared* digest, not that the declared digest describes the bytes being served.
64
+ * Only a party holding the bytes can prove the second half, and it has to be proved before an
65
+ * immutable directive is attached — see `verifyDeclaredAssetBytes` in the control plane.
66
+ */
67
+ path: string;
68
+ hash: `sha256:${string}`;
69
+ contentType: string;
70
+ size: number;
71
+ origin: PublicAssetOrigin;
72
+ }
73
+ export interface RevalidatedAssetV0 {
74
+ path: string;
75
+ contentType: string;
76
+ origin: PublicAssetOrigin;
77
+ }
78
+ /**
79
+ * The compiler's statement about which of an application's public URLs are content addresses and
80
+ * which are stable contracts. It is part of the artifact, so it is covered by `artifactId`, and it is
81
+ * the *only* thing downstream is allowed to classify an asset by: never the extension, never the
82
+ * content type, never the status code. An application endpoint may legitimately serve personalised
83
+ * bytes from a path that ends in `.js`, so inference from the shape of a URL is not a classification
84
+ * — it is a guess that is wrong the first time it matters.
85
+ */
86
+ export interface PublicAssetsV0 {
87
+ format: typeof PUBLIC_ASSETS_FORMAT;
88
+ /**
89
+ * Published so every reader names the prefix from the artifact rather than from a literal of its
90
+ * own. Moving it is still a coordinated change — {@link parsePublicAssets} pins it to the compiled
91
+ * constant, deliberately, so a bundle cannot nominate its own reserved namespace — but there is
92
+ * exactly one place that knows the string.
93
+ */
94
+ immutablePrefix: typeof IMMUTABLE_ASSET_PREFIX;
95
+ /**
96
+ * How many *previous* deployments' immutable assets keep serving alongside this one.
97
+ *
98
+ * The value is `0` or `1` and nothing else — see {@link MAX_RETAINED_ASSET_GENERATIONS}. `1` means
99
+ * the deployment this artifact replaces keeps answering for its content-addressed paths; `0` means
100
+ * it does not, and a client holding the older document gets a 404.
101
+ *
102
+ * Worth recording honestly: this is the *incoming* artifact selecting an obligation that is owed to
103
+ * documents emitted by the *outgoing* one, and deployment cadence is a control-plane concern rather
104
+ * than a compiler fact. It lives here because it is covered by `artifactId` and therefore visible in
105
+ * a retained bundle, which is what a rollback replays. If retention ever needs an age component —
106
+ * Rails Sprockets keeps the last two versions *or* anything younger than an hour — that policy
107
+ * belongs to the control plane and this field should become a floor rather than the whole rule.
108
+ */
109
+ retainedGenerations: number;
110
+ /** Sorted by path. Cacheable forever, because the path is the digest. */
111
+ immutable: ImmutableAssetV0[];
112
+ /**
113
+ * Sorted by path. Declared public and deliberately *not* cacheable: the URL is a contract, the
114
+ * bytes are not.
115
+ *
116
+ * The compiler emits this exhaustively — the document plus every `public/**` file — and the control
117
+ * plane enforces that exhaustiveness where it matters, by requiring the uploaded asset set to be
118
+ * exactly `immutable ∪ revalidate`. Nothing here can therefore be a public URL the declaration
119
+ * failed to mention.
120
+ */
121
+ revalidate: RevalidatedAssetV0[];
122
+ }
123
+ /** The URL a blob of bytes is served at, and the only sanctioned way to mint one. */
124
+ export declare function immutableAssetPath(hash: `sha256:${string}`, basename: string): string;
125
+ /**
126
+ * A cheap structural pre-filter, and never the authority. Membership of
127
+ * {@link PublicAssetsV0.immutable} is what makes a path immutable; this only rejects the
128
+ * overwhelming majority of traffic before anything more expensive is consulted.
129
+ */
130
+ export declare function isImmutableAssetPath(path: string): boolean;
131
+ /**
132
+ * The `_headers` document Cloudflare's static asset layer applies to a deployment.
133
+ *
134
+ * One rule, scoped to the prefix. Nothing else may appear here: an `immutable` directive that leaked
135
+ * onto the HTML document would pin stale markup in every browser that saw it for a year, and no
136
+ * server-side action reaches a browser that has already stored one.
137
+ */
138
+ export declare function immutableAssetHeaders(prefix?: string): string;
139
+ /**
140
+ * Validates a declaration rather than trusting one. Every rule below has a failure it exists to
141
+ * prevent, and the two that matter most are the last two:
142
+ *
143
+ * - **path–hash binding.** If a hashed path could ever name bytes other than its own digest, the
144
+ * immutable cache directive becomes unsafe and there is no way to withdraw it.
145
+ * - **prefix exclusivity.** If anything mutable could answer under the prefix, the single `_headers`
146
+ * rule would make it permanently cacheable too.
147
+ */
148
+ export declare function parsePublicAssets(value: unknown): PublicAssetsV0;
149
+ /**
150
+ * Every path that must answer for this deployment, immutable first. The union a control plane uploads
151
+ * is this set for the current artifact plus the immutable half of the retained generations.
152
+ */
153
+ export declare function declaredAssetPaths(declaration: PublicAssetsV0): readonly string[];