@impetik/xeer-mcp 0.2.5 → 0.2.7

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 +120 -0
  34. package/vendor/spec/review.js +226 -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 +27 -0
  38. package/vendor/spec/schema-lifecycle.js +146 -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,166 @@
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 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 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 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 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 const MAX_RETAINED_ASSET_GENERATIONS = 1;
51
+ const IMMUTABLE_PATH = /^\/_xa\/[0-9a-f]{64}\/[A-Za-z0-9](?:[A-Za-z0-9._-]{0,63})$/u;
52
+ const ASSET_HASH = /^sha256:[0-9a-f]{64}$/u;
53
+ /** The URL a blob of bytes is served at, and the only sanctioned way to mint one. */
54
+ export function immutableAssetPath(hash, basename) {
55
+ return `${IMMUTABLE_ASSET_PREFIX}${hash.slice('sha256:'.length, 'sha256:'.length + IMMUTABLE_ASSET_HASH_LENGTH)}/${basename}`;
56
+ }
57
+ /**
58
+ * A cheap structural pre-filter, and never the authority. Membership of
59
+ * {@link PublicAssetsV0.immutable} is what makes a path immutable; this only rejects the
60
+ * overwhelming majority of traffic before anything more expensive is consulted.
61
+ */
62
+ export function isImmutableAssetPath(path) {
63
+ return IMMUTABLE_PATH.test(path);
64
+ }
65
+ /**
66
+ * The `_headers` document Cloudflare's static asset layer applies to a deployment.
67
+ *
68
+ * One rule, scoped to the prefix. Nothing else may appear here: an `immutable` directive that leaked
69
+ * onto the HTML document would pin stale markup in every browser that saw it for a year, and no
70
+ * server-side action reaches a browser that has already stored one.
71
+ */
72
+ export function immutableAssetHeaders(prefix = IMMUTABLE_ASSET_PREFIX) {
73
+ return `${prefix}*\n Cache-Control: ${IMMUTABLE_ASSET_CACHE_CONTROL}\n`;
74
+ }
75
+ function fail(message) {
76
+ throw new TypeError(`publicAssets: ${message}`);
77
+ }
78
+ function object(value, label) {
79
+ if (!value || typeof value !== 'object' || Array.isArray(value))
80
+ fail(`${label} must be an object.`);
81
+ return value;
82
+ }
83
+ function origin(value, label) {
84
+ // The `:` must be followed by something. An empty `module:` names nothing, and `documentReferences`
85
+ // resolves the document's three subresources by matching on this exact string.
86
+ if (typeof value !== 'string' || value.length > 256
87
+ || !(value === 'document' || /^(?:module|asset):.+$/u.test(value))) {
88
+ fail(`${label}.origin must be "document", "module:<path>" or "asset:<path>".`);
89
+ }
90
+ return value;
91
+ }
92
+ function contentType(value, label) {
93
+ if (typeof value !== 'string' || !/^[\x20-\x7e]{1,128}$/u.test(value))
94
+ fail(`${label}.contentType is invalid.`);
95
+ return value;
96
+ }
97
+ function sorted(paths, label) {
98
+ for (let index = 1; index < paths.length; index += 1) {
99
+ // `<=` rather than `<`: a duplicate is caught by the same comparison that catches disorder,
100
+ // because both mean the list is not the canonical enumeration of a set.
101
+ if (paths[index - 1] >= paths[index])
102
+ fail(`${label} must be sorted by path and free of duplicates.`);
103
+ }
104
+ }
105
+ /**
106
+ * Validates a declaration rather than trusting one. Every rule below has a failure it exists to
107
+ * prevent, and the two that matter most are the last two:
108
+ *
109
+ * - **path–hash binding.** If a hashed path could ever name bytes other than its own digest, the
110
+ * immutable cache directive becomes unsafe and there is no way to withdraw it.
111
+ * - **prefix exclusivity.** If anything mutable could answer under the prefix, the single `_headers`
112
+ * rule would make it permanently cacheable too.
113
+ */
114
+ export function parsePublicAssets(value) {
115
+ const input = object(value, 'declaration');
116
+ if (input.format !== PUBLIC_ASSETS_FORMAT)
117
+ fail(`format must be ${PUBLIC_ASSETS_FORMAT}.`);
118
+ if (input.immutablePrefix !== IMMUTABLE_ASSET_PREFIX)
119
+ fail(`immutablePrefix must be ${IMMUTABLE_ASSET_PREFIX}.`);
120
+ const retainedGenerations = input.retainedGenerations;
121
+ if (typeof retainedGenerations !== 'number' || !Number.isInteger(retainedGenerations)
122
+ || retainedGenerations < 0 || retainedGenerations > MAX_RETAINED_ASSET_GENERATIONS) {
123
+ fail(`retainedGenerations must be an integer between 0 and ${MAX_RETAINED_ASSET_GENERATIONS}.`);
124
+ }
125
+ if (!Array.isArray(input.immutable) || !Array.isArray(input.revalidate))
126
+ fail('immutable and revalidate must be arrays.');
127
+ const immutable = input.immutable.map((item, index) => {
128
+ const entry = object(item, `immutable[${index}]`);
129
+ const hash = entry.hash;
130
+ if (typeof hash !== 'string' || !ASSET_HASH.test(hash))
131
+ fail(`immutable[${index}].hash is invalid.`);
132
+ const path = entry.path;
133
+ if (typeof path !== 'string' || !isImmutableAssetPath(path))
134
+ fail(`immutable[${index}].path is not a well-formed immutable path.`);
135
+ if (path !== immutableAssetPath(hash, path.slice(path.lastIndexOf('/') + 1))) {
136
+ fail(`immutable[${index}].path does not carry the digest of its own bytes.`);
137
+ }
138
+ const size = entry.size;
139
+ if (typeof size !== 'number' || !Number.isInteger(size) || size < 0)
140
+ fail(`immutable[${index}].size is invalid.`);
141
+ return { path, hash: hash, contentType: contentType(entry.contentType, `immutable[${index}]`),
142
+ size, origin: origin(entry.origin, `immutable[${index}]`) };
143
+ });
144
+ const revalidate = input.revalidate.map((item, index) => {
145
+ const entry = object(item, `revalidate[${index}]`);
146
+ const path = entry.path;
147
+ if (typeof path !== 'string' || !path.startsWith('/') || path.includes('//') || path.includes('..')) {
148
+ fail(`revalidate[${index}].path is invalid.`);
149
+ }
150
+ if (path.startsWith(IMMUTABLE_ASSET_PREFIX))
151
+ fail(`revalidate[${index}].path uses the reserved immutable prefix.`);
152
+ return { path, contentType: contentType(entry.contentType, `revalidate[${index}]`),
153
+ origin: origin(entry.origin, `revalidate[${index}]`) };
154
+ });
155
+ sorted(immutable.map((entry) => entry.path), 'immutable');
156
+ sorted(revalidate.map((entry) => entry.path), 'revalidate');
157
+ return { format: PUBLIC_ASSETS_FORMAT, immutablePrefix: IMMUTABLE_ASSET_PREFIX, retainedGenerations,
158
+ immutable, revalidate };
159
+ }
160
+ /**
161
+ * Every path that must answer for this deployment, immutable first. The union a control plane uploads
162
+ * is this set for the current artifact plus the immutable half of the retained generations.
163
+ */
164
+ export function declaredAssetPaths(declaration) {
165
+ return [...declaration.immutable.map((entry) => entry.path), ...declaration.revalidate.map((entry) => entry.path)];
166
+ }
@@ -0,0 +1,120 @@
1
+ import type { NormalizedDatabaseSchemaV0, SchemaPlanV0 } from './schema-plan.js';
2
+ import { type ApplicationArtifactV0, type ApplicationSchemaIdentityV0, type Capability, type SourceReceipt } from './types.js';
3
+ export declare const ARTIFACT_REVIEW_PROTOCOL: "xeer.artifact-review.v0";
4
+ export declare const TEST_EVIDENCE_PROTOCOL: "xeer.test-evidence.v0";
5
+ export declare const REVIEW_PROTOCOL: "xeer.review.v0";
6
+ export declare const REVIEW_RECEIPT_ID_SOURCE: "^review_[A-Za-z0-9_-]{12,96}$";
7
+ export declare const REVIEW_RECEIPT_ID_PATTERN: RegExp;
8
+ export interface ArtifactReviewMetadataV0 {
9
+ readonly protocol: typeof ARTIFACT_REVIEW_PROTOCOL;
10
+ readonly sourceHash: `sha256:${string}`;
11
+ readonly schema: NormalizedDatabaseSchemaV0;
12
+ readonly schemaIdentity: ApplicationSchemaIdentityV0;
13
+ }
14
+ export type TestEvidenceStatus = 'passed' | 'failed';
15
+ /** A local test runner's report. The control plane records it as reported evidence, not an attestation. */
16
+ export interface XeerTestEvidenceV0 {
17
+ readonly protocol: typeof TEST_EVIDENCE_PROTOCOL;
18
+ readonly artifactId: `sha256:${string}`;
19
+ readonly status: TestEvidenceStatus;
20
+ readonly reason: string;
21
+ readonly total: number;
22
+ readonly passed: number;
23
+ readonly failed: number;
24
+ readonly files: readonly string[];
25
+ readonly completedAt: string;
26
+ }
27
+ export interface ReviewTestEvidenceV0 {
28
+ readonly provenance: 'reported' | 'absent';
29
+ readonly status: TestEvidenceStatus | 'absent';
30
+ readonly reason: string;
31
+ readonly total: number;
32
+ readonly passed: number;
33
+ readonly failed: number;
34
+ readonly completedAt: string | null;
35
+ }
36
+ export interface ReviewSchemaChangeV0 {
37
+ readonly status: 'unknown' | 'initial' | 'unchanged' | 'planned' | 'unplannable';
38
+ readonly from: ApplicationSchemaIdentityV0 | null;
39
+ readonly to: ApplicationSchemaIdentityV0 | null;
40
+ readonly plan: SchemaPlanV0 | null;
41
+ readonly planningError: string | null;
42
+ }
43
+ export interface ReviewCapabilityChangeV0 {
44
+ readonly before: readonly Capability[];
45
+ readonly after: readonly Capability[];
46
+ readonly added: readonly Capability[];
47
+ readonly removed: readonly Capability[];
48
+ }
49
+ export interface ReviewEnvironmentNamesV0 {
50
+ readonly preview: readonly string[];
51
+ readonly production: readonly string[];
52
+ readonly previewOnly: readonly string[];
53
+ readonly productionOnly: readonly string[];
54
+ }
55
+ export interface XeerReviewReceiptV0 {
56
+ readonly protocol: typeof REVIEW_PROTOCOL;
57
+ readonly receiptId: string;
58
+ readonly appId: string;
59
+ readonly application: string;
60
+ readonly artifactId: `sha256:${string}`;
61
+ readonly sourceHash: `sha256:${string}` | null;
62
+ readonly createdAt: string;
63
+ readonly test: ReviewTestEvidenceV0;
64
+ readonly changes: {
65
+ readonly baselineArtifactId: `sha256:${string}` | null;
66
+ readonly schema: ReviewSchemaChangeV0;
67
+ readonly capabilities: ReviewCapabilityChangeV0;
68
+ readonly environmentNames: ReviewEnvironmentNamesV0;
69
+ };
70
+ readonly preview: {
71
+ readonly deploymentId: string;
72
+ readonly url: string;
73
+ };
74
+ }
75
+ export declare function artifactSourceHash(files: readonly SourceReceipt[]): `sha256:${string}`;
76
+ export declare function artifactReviewMetadata(artifact: ApplicationArtifactV0): ArtifactReviewMetadataV0;
77
+ /**
78
+ * Why review metadata was refused. The three cases look alike from the outside and are nothing alike
79
+ * to repair, which is what #205 cost an afternoon to: two of them are version skew between the build
80
+ * that produced the metadata and the build reading it, and only the third means the bundle is wrong.
81
+ */
82
+ export type ArtifactReviewRejection = {
83
+ readonly code: 'artifact_review_malformed';
84
+ /**
85
+ * The failing paths within `artifactReview`, deduplicated, in the order they were reported —
86
+ * which is the order the payload declares them, and not sorted, because sorting shows `t1, t10,
87
+ * t2` and hides everything past the eighth lexicographic name.
88
+ *
89
+ * **Empty means the value is not an object of this protocol at all**, which is reported at the
90
+ * root where there is no field to name. It has no second meaning.
91
+ */
92
+ readonly fields: readonly string[];
93
+ /** Failing paths beyond the ones listed, so a truncated list never reads as a complete one. */
94
+ readonly omitted: number;
95
+ } | {
96
+ /**
97
+ * Fields this build has never heard of. Skew, and the usual shape of it: a newer CLI emitting a
98
+ * schema key an older plane does not know reaches the strict object before any hash is compared,
99
+ * so it can only be told apart here.
100
+ */
101
+ readonly code: 'artifact_review_unknown_fields';
102
+ readonly fields: readonly string[];
103
+ readonly omitted: number;
104
+ } | {
105
+ readonly code: 'artifact_review_schema_identity_mismatch';
106
+ /** What this build hashes the declared schema to. */
107
+ readonly expected: ApplicationSchemaIdentityV0;
108
+ /** What the metadata claims the same schema hashes to. */
109
+ readonly received: ApplicationSchemaIdentityV0;
110
+ };
111
+ export declare class ArtifactReviewError extends Error {
112
+ readonly rejection: ArtifactReviewRejection;
113
+ constructor(rejection: ArtifactReviewRejection, message: string);
114
+ }
115
+ export declare function parseArtifactReviewMetadata(value: unknown): ArtifactReviewMetadataV0;
116
+ export declare function parseTestEvidence(value: unknown): XeerTestEvidenceV0;
117
+ export declare function parseReviewReceipt(value: unknown): XeerReviewReceiptV0;
118
+ export declare function isReviewReceiptId(value: string): boolean;
119
+ export declare function capabilityChange(before: readonly string[], after: readonly string[]): ReviewCapabilityChangeV0;
120
+ export declare function environmentNameChange(preview: readonly string[], production: readonly string[]): ReviewEnvironmentNamesV0;
@@ -0,0 +1,226 @@
1
+ import { z } from 'zod';
2
+ import { canonicalHash } from './canonical.js';
3
+ import { normalizedDatabaseSchema } from './schema.js';
4
+ import { applicationSchemaIdentity } from './schema-lifecycle.js';
5
+ import { CAPABILITIES, } from './types.js';
6
+ export const ARTIFACT_REVIEW_PROTOCOL = 'xeer.artifact-review.v0';
7
+ export const TEST_EVIDENCE_PROTOCOL = 'xeer.test-evidence.v0';
8
+ export const REVIEW_PROTOCOL = 'xeer.review.v0';
9
+ export const REVIEW_RECEIPT_ID_SOURCE = '^review_[A-Za-z0-9_-]{12,96}$';
10
+ export const REVIEW_RECEIPT_ID_PATTERN = new RegExp(REVIEW_RECEIPT_ID_SOURCE, 'u');
11
+ const HASH = /^sha256:[a-f0-9]{64}$/u;
12
+ const APP_ID = /^app_[A-Za-z0-9_-]{8,96}$/u;
13
+ const schemaIdentity = z.strictObject({
14
+ applicationSchemaVersion: z.number().int().positive(),
15
+ schemaHash: z.string().regex(HASH),
16
+ });
17
+ const artifactReview = z.strictObject({
18
+ protocol: z.literal(ARTIFACT_REVIEW_PROTOCOL),
19
+ sourceHash: z.string().regex(HASH),
20
+ schema: normalizedDatabaseSchema,
21
+ schemaIdentity,
22
+ });
23
+ const testEvidence = z.strictObject({
24
+ protocol: z.literal(TEST_EVIDENCE_PROTOCOL),
25
+ artifactId: z.string().regex(HASH),
26
+ status: z.enum(['passed', 'failed']),
27
+ reason: z.string().min(1).max(64),
28
+ total: z.number().int().nonnegative(),
29
+ passed: z.number().int().nonnegative(),
30
+ failed: z.number().int().nonnegative(),
31
+ files: z.array(z.string().min(1).max(512)).max(512),
32
+ completedAt: z.iso.datetime(),
33
+ }).superRefine((value, context) => {
34
+ if (value.passed + value.failed !== value.total) {
35
+ context.addIssue({ code: 'custom', path: ['total'], message: 'must equal passed + failed' });
36
+ }
37
+ if (value.status === 'passed' && (value.total === 0 || value.failed !== 0)) {
38
+ context.addIssue({ code: 'custom', path: ['status'], message: 'requires at least one test and zero failures' });
39
+ }
40
+ if (value.status === 'failed' && value.failed === 0 && value.reason === 'complete') {
41
+ context.addIssue({ code: 'custom', path: ['status'], message: 'failed evidence needs a failure reason' });
42
+ }
43
+ });
44
+ const schemaPlanStep = z.discriminatedUnion('kind', [
45
+ z.strictObject({ kind: z.literal('table.add'), table: z.string().min(1) }),
46
+ z.strictObject({ kind: z.literal('table.remove'), table: z.string().min(1) }),
47
+ z.strictObject({ kind: z.literal('field.add'), table: z.string().min(1), field: z.string().min(1),
48
+ optional: z.boolean() }),
49
+ z.strictObject({ kind: z.literal('field.remove'), table: z.string().min(1), field: z.string().min(1) }),
50
+ z.strictObject({ kind: z.literal('field.type'), table: z.string().min(1), field: z.string().min(1),
51
+ from: z.enum(['string', 'number', 'boolean', 'datetime', 'bytes', 'json']),
52
+ to: z.enum(['string', 'number', 'boolean', 'datetime', 'bytes', 'json']) }),
53
+ z.strictObject({ kind: z.literal('field.optional'), table: z.string().min(1), field: z.string().min(1),
54
+ from: z.boolean(), to: z.boolean() }),
55
+ z.strictObject({ kind: z.literal('field.maxLength'), table: z.string().min(1), field: z.string().min(1),
56
+ from: z.number().int().positive().nullable(), to: z.number().int().positive().nullable() }),
57
+ z.strictObject({ kind: z.literal('index.add'), table: z.string().min(1), index: z.string().min(1),
58
+ fields: z.array(z.string().min(1)) }),
59
+ z.strictObject({ kind: z.literal('index.remove'), table: z.string().min(1), index: z.string().min(1),
60
+ fields: z.array(z.string().min(1)) }),
61
+ z.strictObject({ kind: z.literal('index.change'), table: z.string().min(1), index: z.string().min(1),
62
+ from: z.array(z.string().min(1)), to: z.array(z.string().min(1)) }),
63
+ z.strictObject({ kind: z.literal('unknown.change'), path: z.string().min(1) }),
64
+ ]);
65
+ const schemaPlan = z.strictObject({
66
+ protocol: z.literal('xeer.schema.v0'),
67
+ application: z.string().min(1),
68
+ from: schemaIdentity,
69
+ to: schemaIdentity,
70
+ classification: z.enum(['unchanged', 'compatible', 'incompatible']),
71
+ steps: z.array(schemaPlanStep),
72
+ requiresReset: z.boolean(),
73
+ });
74
+ const reviewReceipt = z.strictObject({
75
+ protocol: z.literal(REVIEW_PROTOCOL),
76
+ receiptId: z.string().regex(REVIEW_RECEIPT_ID_PATTERN),
77
+ appId: z.string().regex(APP_ID),
78
+ application: z.string().min(1),
79
+ artifactId: z.string().regex(HASH),
80
+ sourceHash: z.string().regex(HASH).nullable(),
81
+ createdAt: z.iso.datetime(),
82
+ test: z.strictObject({
83
+ provenance: z.enum(['reported', 'absent']),
84
+ status: z.enum(['passed', 'failed', 'absent']),
85
+ reason: z.string().min(1).max(64),
86
+ total: z.number().int().nonnegative(),
87
+ passed: z.number().int().nonnegative(),
88
+ failed: z.number().int().nonnegative(),
89
+ completedAt: z.iso.datetime().nullable(),
90
+ }),
91
+ changes: z.strictObject({
92
+ baselineArtifactId: z.string().regex(HASH).nullable(),
93
+ schema: z.strictObject({
94
+ status: z.enum(['unknown', 'initial', 'unchanged', 'planned', 'unplannable']),
95
+ from: schemaIdentity.nullable(),
96
+ to: schemaIdentity.nullable(),
97
+ plan: schemaPlan.nullable(),
98
+ planningError: z.string().nullable(),
99
+ }),
100
+ capabilities: z.strictObject({
101
+ before: z.array(z.enum(CAPABILITIES)), after: z.array(z.enum(CAPABILITIES)),
102
+ added: z.array(z.enum(CAPABILITIES)), removed: z.array(z.enum(CAPABILITIES)),
103
+ }),
104
+ environmentNames: z.strictObject({
105
+ preview: z.array(z.string().min(1)), production: z.array(z.string().min(1)),
106
+ previewOnly: z.array(z.string().min(1)), productionOnly: z.array(z.string().min(1)),
107
+ }),
108
+ }),
109
+ preview: z.strictObject({ deploymentId: z.string().min(1), url: z.url() }),
110
+ }).superRefine((value, context) => {
111
+ if (value.test.passed + value.test.failed !== value.test.total) {
112
+ context.addIssue({ code: 'custom', path: ['test', 'total'], message: 'must equal passed + failed' });
113
+ }
114
+ if (value.test.provenance === 'absent'
115
+ && (value.test.status !== 'absent' || value.test.total !== 0 || value.test.completedAt !== null)) {
116
+ context.addIssue({ code: 'custom', path: ['test'], message: 'absent evidence must contain no results' });
117
+ }
118
+ if (value.test.provenance === 'reported' && value.test.completedAt === null) {
119
+ context.addIssue({ code: 'custom', path: ['test', 'completedAt'], message: 'reported evidence needs a time' });
120
+ }
121
+ if (value.test.status === 'passed' && (value.test.total === 0 || value.test.failed !== 0)) {
122
+ context.addIssue({ code: 'custom', path: ['test', 'status'], message: 'requires tests and zero failures' });
123
+ }
124
+ });
125
+ export function artifactSourceHash(files) {
126
+ return canonicalHash([...files]
127
+ .map(({ path, size, hash }) => ({ path, size, hash }))
128
+ .sort((left, right) => left.path.localeCompare(right.path)));
129
+ }
130
+ export function artifactReviewMetadata(artifact) {
131
+ return Object.freeze({
132
+ protocol: ARTIFACT_REVIEW_PROTOCOL,
133
+ sourceHash: artifactSourceHash(artifact.source.files),
134
+ schema: artifact.schema,
135
+ schemaIdentity: artifact.schemaIdentity,
136
+ });
137
+ }
138
+ export class ArtifactReviewError extends Error {
139
+ rejection;
140
+ constructor(rejection, message) {
141
+ super(message);
142
+ this.rejection = rejection;
143
+ this.name = 'ArtifactReviewError';
144
+ }
145
+ }
146
+ /** How many failing paths are worth showing a person, and how long one segment of one may be. */
147
+ const REPORTED_PATHS = 8;
148
+ const SEGMENT_LENGTH = 40;
149
+ const PLAIN_SEGMENT = /^[A-Za-z0-9_$-]+$/u;
150
+ /**
151
+ * One segment of a path, as it can safely be shown.
152
+ *
153
+ * A segment is not always ours: `tables` and `fields` are records keyed by caller-supplied names, so a
154
+ * table name arrives here verbatim. It is length-bounded because the rendered path reaches a 400 body
155
+ * and an operator's log, and quoted unless it is a plain name, because a table called `a.b` would
156
+ * otherwise read as two segments and send someone looking for a field that does not exist.
157
+ */
158
+ function segment(value) {
159
+ const text = String(value);
160
+ const shown = text.length > SEGMENT_LENGTH ? `${text.slice(0, SEGMENT_LENGTH)}…` : text;
161
+ return PLAIN_SEGMENT.test(shown) ? shown : JSON.stringify(shown);
162
+ }
163
+ function renderPath(parts) {
164
+ return parts.map(segment).join('.');
165
+ }
166
+ /** The first {@link REPORTED_PATHS} of a deduplicated path list, with the count of the rest. */
167
+ function reported(paths) {
168
+ const unique = [...new Set(paths)];
169
+ return { fields: unique.slice(0, REPORTED_PATHS), omitted: Math.max(0, unique.length - REPORTED_PATHS) };
170
+ }
171
+ /**
172
+ * The rejection a failed parse describes: unknown fields where that is the *only* complaint, and a
173
+ * malformed bundle otherwise. Requiring it to be the only complaint is deliberate — a payload that is
174
+ * both unrecognized and broken is broken, and calling it skew would point the repair at the wrong side.
175
+ */
176
+ function parseRejection(error) {
177
+ if (error.issues.every((issue) => issue.code === 'unrecognized_keys')) {
178
+ return { code: 'artifact_review_unknown_fields',
179
+ ...reported(error.issues.flatMap((issue) => issue.code === 'unrecognized_keys'
180
+ ? issue.keys.map((key) => renderPath([...issue.path, key])) : [])) };
181
+ }
182
+ return { code: 'artifact_review_malformed',
183
+ ...reported(error.issues.map((issue) => renderPath(issue.path)).filter((path) => path !== '')) };
184
+ }
185
+ export function parseArtifactReviewMetadata(value) {
186
+ const result = artifactReview.safeParse(value);
187
+ if (!result.success) {
188
+ throw new ArtifactReviewError(parseRejection(result.error), 'Artifact review metadata does not match xeer.artifact-review.v0.');
189
+ }
190
+ const parsed = result.data;
191
+ const expected = applicationSchemaIdentity(parsed.schema);
192
+ if (parsed.schemaIdentity.applicationSchemaVersion !== expected.applicationSchemaVersion
193
+ || parsed.schemaIdentity.schemaHash !== expected.schemaHash) {
194
+ throw new ArtifactReviewError({ code: 'artifact_review_schema_identity_mismatch', expected, received: parsed.schemaIdentity }, 'Artifact review schemaIdentity does not match its normalized schema.');
195
+ }
196
+ return parsed;
197
+ }
198
+ export function parseTestEvidence(value) {
199
+ return testEvidence.parse(value);
200
+ }
201
+ export function parseReviewReceipt(value) {
202
+ return reviewReceipt.parse(value);
203
+ }
204
+ export function isReviewReceiptId(value) {
205
+ return REVIEW_RECEIPT_ID_PATTERN.test(value);
206
+ }
207
+ export function capabilityChange(before, after) {
208
+ const prior = [...new Set(before)].filter((name) => CAPABILITIES.includes(name)).sort();
209
+ const next = [...new Set(after)].filter((name) => CAPABILITIES.includes(name)).sort();
210
+ return Object.freeze({
211
+ before: prior,
212
+ after: next,
213
+ added: next.filter((name) => !prior.includes(name)),
214
+ removed: prior.filter((name) => !next.includes(name)),
215
+ });
216
+ }
217
+ export function environmentNameChange(preview, production) {
218
+ const previewNames = [...new Set(preview)].sort();
219
+ const productionNames = [...new Set(production)].sort();
220
+ return Object.freeze({
221
+ preview: previewNames,
222
+ production: productionNames,
223
+ previewOnly: previewNames.filter((name) => !productionNames.includes(name)),
224
+ productionOnly: productionNames.filter((name) => !previewNames.includes(name)),
225
+ });
226
+ }
@@ -0,0 +1,43 @@
1
+ export declare const ENDPOINT_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
2
+ export type EndpointMethod = typeof ENDPOINT_METHODS[number];
3
+ export interface EndpointRoute {
4
+ readonly key: string;
5
+ readonly method: EndpointMethod;
6
+ readonly path: string;
7
+ /** Dynamic parameter names are erased so equivalent route shapes collide. */
8
+ readonly shape: string;
9
+ }
10
+ export declare function parseEndpointRoute(key: string): EndpointRoute | null;
11
+ export declare function endpointRouteIdentity(route: Pick<EndpointRoute, 'method' | 'shape'>): string;
12
+ export declare function endpointRouteMatches(route: Pick<EndpointRoute, 'method' | 'path'>, method: string, pathname: string): boolean;
13
+ export declare function normalizeEndpointRoutes(keys: Iterable<string>): EndpointRoute[];
14
+ /**
15
+ * The reserved runtime route that serves the live invalidation stream.
16
+ *
17
+ * `packages/runtime/src/live.ts` is normative for this path; this is a mirror, exactly as the SDK's
18
+ * `live.ts` already mirrors it. It is restated here rather than imported because the public
19
+ * dispatcher needs it and `xeer-auth` does not — and must not — depend on the runtime package: the
20
+ * dispatcher is a separate Worker, and an import edge would pull the whole application runtime into
21
+ * it to learn one string.
22
+ */
23
+ export declare const LIVE_STREAM_PATH = "/__xeer/events";
24
+ /**
25
+ * Every `/__xeer/` path a public visitor may reach. **A closed allowlist, and it has to stay one.**
26
+ *
27
+ * The public dispatcher refuses anything under `/__xeer/` that is not listed here. That is the
28
+ * inversion of an exact-path denylist, which was safe only for as long as nobody added a route: a
29
+ * new builder-only surface in the runtime was publicly reachable the moment it existed, and nothing
30
+ * in its own diff said so. Enumerating what is *open* means the default for a route nobody has
31
+ * written yet is closed.
32
+ *
33
+ * The obvious alternative — refusing the whole `/__xeer/` prefix — is wrong, because the application
34
+ * protocol lives under it: `rpc/query`, `rpc/mutation` and the live stream are how an application is
35
+ * used at all. `health` and `identity` are the two readiness probes, deliberately excluded from the
36
+ * runtime's own `INSPECTOR_ROUTES` so that deploys and uptime checks keep working.
37
+ *
38
+ * Adding an entry here makes a route publicly reachable on every deployed application. Nothing else
39
+ * does, which is the point: the decision is visible in one diff, in one place.
40
+ */
41
+ export declare const PUBLIC_RESERVED_ROUTES: ReadonlySet<string>;
42
+ /** Whether a path is reserved by the platform, and therefore governed by the allowlist above. */
43
+ export declare function reservedRuntimePath(pathname: string): boolean;
@@ -0,0 +1,87 @@
1
+ export const ENDPOINT_METHODS = [
2
+ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD',
3
+ ];
4
+ const METHODS = new Set(ENDPOINT_METHODS);
5
+ const STATIC_SEGMENT = /^[A-Za-z0-9_.-]+$/;
6
+ const PARAMETER_SEGMENT = /^:[A-Za-z][A-Za-z0-9_]*$/;
7
+ export function parseEndpointRoute(key) {
8
+ const separator = key.indexOf(' ');
9
+ if (separator <= 0 || key.indexOf(' ', separator + 1) !== -1)
10
+ return null;
11
+ const method = key.slice(0, separator);
12
+ const path = key.slice(separator + 1);
13
+ if (!METHODS.has(method) || (path !== '/api' && !path.startsWith('/api/')))
14
+ return null;
15
+ const segments = path.split('/').slice(2);
16
+ if (segments.some((segment) => !STATIC_SEGMENT.test(segment) && !PARAMETER_SEGMENT.test(segment)))
17
+ return null;
18
+ const shape = segments.length === 0
19
+ ? '/api'
20
+ : `/api/${segments.map((segment) => segment.startsWith(':') ? ':' : segment).join('/')}`;
21
+ return { key, method: method, path, shape };
22
+ }
23
+ export function endpointRouteIdentity(route) {
24
+ return `${route.method} ${route.shape}`;
25
+ }
26
+ export function endpointRouteMatches(route, method, pathname) {
27
+ if (route.method !== method)
28
+ return false;
29
+ const expected = route.path.split('/');
30
+ const actual = pathname.split('/');
31
+ return expected.length === actual.length && expected.every((part, index) => part.startsWith(':') ? (actual[index]?.length ?? 0) > 0 : part === actual[index]);
32
+ }
33
+ export function normalizeEndpointRoutes(keys) {
34
+ const routes = [];
35
+ const identities = new Map();
36
+ for (const key of keys) {
37
+ const route = parseEndpointRoute(key);
38
+ if (!route)
39
+ throw new Error(`Invalid endpoint route: ${key}`);
40
+ const identity = endpointRouteIdentity(route);
41
+ const existing = identities.get(identity);
42
+ if (existing && existing !== route.key) {
43
+ throw new Error(`Ambiguous endpoint routes: ${existing} and ${route.key}`);
44
+ }
45
+ identities.set(identity, route.key);
46
+ routes.push(route);
47
+ }
48
+ return routes.sort((left, right) => left.key.localeCompare(right.key));
49
+ }
50
+ /**
51
+ * The reserved runtime route that serves the live invalidation stream.
52
+ *
53
+ * `packages/runtime/src/live.ts` is normative for this path; this is a mirror, exactly as the SDK's
54
+ * `live.ts` already mirrors it. It is restated here rather than imported because the public
55
+ * dispatcher needs it and `xeer-auth` does not — and must not — depend on the runtime package: the
56
+ * dispatcher is a separate Worker, and an import edge would pull the whole application runtime into
57
+ * it to learn one string.
58
+ */
59
+ export const LIVE_STREAM_PATH = '/__xeer/events';
60
+ /**
61
+ * Every `/__xeer/` path a public visitor may reach. **A closed allowlist, and it has to stay one.**
62
+ *
63
+ * The public dispatcher refuses anything under `/__xeer/` that is not listed here. That is the
64
+ * inversion of an exact-path denylist, which was safe only for as long as nobody added a route: a
65
+ * new builder-only surface in the runtime was publicly reachable the moment it existed, and nothing
66
+ * in its own diff said so. Enumerating what is *open* means the default for a route nobody has
67
+ * written yet is closed.
68
+ *
69
+ * The obvious alternative — refusing the whole `/__xeer/` prefix — is wrong, because the application
70
+ * protocol lives under it: `rpc/query`, `rpc/mutation` and the live stream are how an application is
71
+ * used at all. `health` and `identity` are the two readiness probes, deliberately excluded from the
72
+ * runtime's own `INSPECTOR_ROUTES` so that deploys and uptime checks keep working.
73
+ *
74
+ * Adding an entry here makes a route publicly reachable on every deployed application. Nothing else
75
+ * does, which is the point: the decision is visible in one diff, in one place.
76
+ */
77
+ export const PUBLIC_RESERVED_ROUTES = new Set([
78
+ '/__xeer/health',
79
+ '/__xeer/identity',
80
+ '/__xeer/rpc/query',
81
+ '/__xeer/rpc/mutation',
82
+ LIVE_STREAM_PATH,
83
+ ]);
84
+ /** Whether a path is reserved by the platform, and therefore governed by the allowlist above. */
85
+ export function reservedRuntimePath(pathname) {
86
+ return pathname.startsWith('/__xeer/') || pathname === '/__xeer';
87
+ }