@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,143 @@
1
+ import { type NormalizedDatabaseSchemaV0, type SchemaPlanStepV0 } from './schema-plan.js';
2
+ import type { ApplicationSchemaIdentityV0, FieldDefinition, TableDefinition } from './types.js';
3
+ import { type EncodedValue, type RuntimeValue } from './value.js';
4
+ /**
5
+ * Portable dump of one application's stored records.
6
+ *
7
+ * The document is deliberately a logical dump rather than SQLite file bytes: a Durable Object
8
+ * offers no way to hand out its database file (its only first-class recovery surface is
9
+ * point-in-time recovery, which restores an object in place and cannot leave the platform), and a
10
+ * file would carry physical indexes and storage-layout details that mean nothing to a reader.
11
+ *
12
+ * Every document is self-describing: it carries the accepted application schema that produced the
13
+ * rows — the normalized table and field definitions plus their identity hash — so a human or an
14
+ * agent reading the file alone can tell what every value in `data` means, and `xeer import` can
15
+ * decide whether the rows still fit the schema it is importing into.
16
+ */
17
+ export declare const STATE_EXPORT_PROTOCOL: "xeer.state-export.v0";
18
+ /** Result protocol for a completed import; distinct from the document protocol on purpose. */
19
+ export declare const STATE_IMPORT_PROTOCOL: "xeer.state-import.v0";
20
+ /**
21
+ * One ceiling every hop agrees on: the runtime refuses to produce a larger export, the control
22
+ * plane refuses to proxy one, and `xeer import` refuses to send one. Exceeding it is a structured
23
+ * failure with a repair hint, never a truncated document.
24
+ */
25
+ export declare const MAX_STATE_TRANSFER_BYTES: number;
26
+ /** A single stored record: the runtime-owned identity and timestamps, plus its encoded fields. */
27
+ export interface StateExportRecordV0 {
28
+ readonly id: string;
29
+ readonly createdAt: string;
30
+ readonly updatedAt: string;
31
+ /** The record's fields in the `xeer.value.v0` encoding, exactly as the runtime stores them. */
32
+ readonly data: EncodedValue;
33
+ }
34
+ export interface StateExportTableV0 {
35
+ readonly name: string;
36
+ readonly records: number;
37
+ readonly rows: readonly StateExportRecordV0[];
38
+ }
39
+ export interface StateExportSchemaV0 {
40
+ readonly applicationSchemaVersion: number;
41
+ readonly schemaHash: string;
42
+ /** The full normalized schema, so the document explains its own rows without the application. */
43
+ readonly database: NormalizedDatabaseSchemaV0;
44
+ }
45
+ export interface StateExportV0 {
46
+ readonly protocol: typeof STATE_EXPORT_PROTOCOL;
47
+ readonly exportedAt: string;
48
+ readonly application: string;
49
+ /** Present when the state was read from an app with a control-plane identity. */
50
+ readonly appId?: string;
51
+ readonly source: {
52
+ readonly artifactId: string;
53
+ readonly generation: number;
54
+ };
55
+ /** Version of the runtime's storage layout the rows were read from. */
56
+ readonly storage: {
57
+ readonly schemaVersion: number;
58
+ };
59
+ readonly schema: StateExportSchemaV0;
60
+ readonly tables: readonly StateExportTableV0[];
61
+ readonly counts: {
62
+ readonly tables: number;
63
+ readonly records: number;
64
+ };
65
+ }
66
+ export interface StateImportResultV0 {
67
+ readonly protocol: typeof STATE_IMPORT_PROTOCOL;
68
+ readonly application: string;
69
+ readonly schema: ApplicationSchemaIdentityV0;
70
+ /** `identical` when the export came from this exact schema; `compatible` when it fits forward. */
71
+ readonly classification: 'identical' | 'compatible';
72
+ readonly steps: readonly SchemaPlanStepV0[];
73
+ readonly importedTables: readonly {
74
+ readonly name: string;
75
+ readonly records: number;
76
+ }[];
77
+ readonly importedRecords: number;
78
+ readonly removedRecords: number;
79
+ }
80
+ export type StateTransferErrorCode = 'state_export_malformed' | 'state_export_protocol_unsupported' | 'state_export_too_large' | 'state_import_application_mismatch' | 'state_import_schema_mismatch' | 'state_import_record_invalid';
81
+ /**
82
+ * Machine-readable refusal, carried verbatim from the runtime through to the CLI diagnostic. */
83
+ export interface StateTransferDiagnostic {
84
+ readonly code: StateTransferErrorCode;
85
+ readonly message: string;
86
+ readonly hint?: string;
87
+ /** The mismatch itself, as plain JSON: expected and found identities, offending row, plan steps. */
88
+ readonly detail: Record<string, unknown>;
89
+ }
90
+ /**
91
+ * A refusal a caller can act on: a stable code, the mismatch itself in `detail`, and a repair hint.
92
+ * Import failures are reported this way rather than as a bare message so an agent can decide what
93
+ * to do without parsing prose.
94
+ */
95
+ export declare class StateTransferError extends Error {
96
+ readonly code: StateTransferErrorCode;
97
+ readonly hint?: string | undefined;
98
+ readonly detail: Record<string, unknown>;
99
+ constructor(code: StateTransferErrorCode, message: string, hint?: string | undefined, detail?: Record<string, unknown>);
100
+ /** JSON shape shared by the runtime response, the control-plane envelope, and CLI diagnostics. */
101
+ toDiagnostic(): StateTransferDiagnostic;
102
+ }
103
+ /**
104
+ * Whether a decoded value satisfies a declared field. Shared with the runtime so an imported record
105
+ * is held to exactly the same rule a handler-written record is.
106
+ */
107
+ export declare function fieldValueValid(field: FieldDefinition, value: RuntimeValue): boolean;
108
+ /**
109
+ * Structural validation of an export document. It proves the file is a well-formed
110
+ * `xeer.state-export.v0` document that describes itself consistently; whether its rows fit a
111
+ * particular application is {@link planStateImport}'s question.
112
+ */
113
+ export declare function parseStateExport(value: unknown): StateExportV0;
114
+ export interface StateImportPlanV0 {
115
+ readonly classification: 'identical' | 'compatible';
116
+ readonly steps: readonly SchemaPlanStepV0[];
117
+ readonly tables: readonly {
118
+ readonly name: string;
119
+ readonly records: number;
120
+ }[];
121
+ readonly records: number;
122
+ }
123
+ /**
124
+ * Decides whether an export may be written into the schema currently in force, and validates every
125
+ * row against it.
126
+ *
127
+ * An export is accepted when it came from this exact schema, or from a schema the current one is a
128
+ * *compatible* successor of — the same classification the runtime already uses to accept a schema
129
+ * change without a reset, so an import can never introduce a row the running application could not
130
+ * have written itself. Anything else is refused with the mismatch in `detail`; there is no override,
131
+ * because the alternative is state the application's own type contract does not describe.
132
+ */
133
+ export declare function planStateImport(input: {
134
+ readonly document: StateExportV0;
135
+ readonly application: string;
136
+ readonly schema: NormalizedDatabaseSchemaV0;
137
+ readonly identity: ApplicationSchemaIdentityV0;
138
+ }): StateImportPlanV0;
139
+ /** Holds one imported record to the declared field contract of the table it is being written into. */
140
+ export declare function validateExportedRecord(tableName: string, table: TableDefinition, row: StateExportRecordV0): Record<string, RuntimeValue>;
141
+ /** Serialized size of a document, used to hold every hop to {@link MAX_STATE_TRANSFER_BYTES}. */
142
+ export declare function stateExportBytes(document: unknown): number;
143
+ export declare function assertStateTransferSize(document: unknown, what: 'export' | 'import'): void;
@@ -0,0 +1,341 @@
1
+ import { planSchemaChangeWithIdentities, SchemaPlanningError, } from './schema-plan.js';
2
+ import { normalizedDatabaseSchema } from './schema.js';
3
+ import { decodeValue } from './value.js';
4
+ // Deliberately no dependency on `./canonical.js` or `./schema-lifecycle.js`: this module is bundled
5
+ // into every deployed application Worker, and those pull in `node:crypto`.
6
+ /**
7
+ * Portable dump of one application's stored records.
8
+ *
9
+ * The document is deliberately a logical dump rather than SQLite file bytes: a Durable Object
10
+ * offers no way to hand out its database file (its only first-class recovery surface is
11
+ * point-in-time recovery, which restores an object in place and cannot leave the platform), and a
12
+ * file would carry physical indexes and storage-layout details that mean nothing to a reader.
13
+ *
14
+ * Every document is self-describing: it carries the accepted application schema that produced the
15
+ * rows — the normalized table and field definitions plus their identity hash — so a human or an
16
+ * agent reading the file alone can tell what every value in `data` means, and `xeer import` can
17
+ * decide whether the rows still fit the schema it is importing into.
18
+ */
19
+ export const STATE_EXPORT_PROTOCOL = 'xeer.state-export.v0';
20
+ /** Result protocol for a completed import; distinct from the document protocol on purpose. */
21
+ export const STATE_IMPORT_PROTOCOL = 'xeer.state-import.v0';
22
+ /**
23
+ * One ceiling every hop agrees on: the runtime refuses to produce a larger export, the control
24
+ * plane refuses to proxy one, and `xeer import` refuses to send one. Exceeding it is a structured
25
+ * failure with a repair hint, never a truncated document.
26
+ */
27
+ export const MAX_STATE_TRANSFER_BYTES = 8 * 1024 * 1024;
28
+ /**
29
+ * A refusal a caller can act on: a stable code, the mismatch itself in `detail`, and a repair hint.
30
+ * Import failures are reported this way rather than as a bare message so an agent can decide what
31
+ * to do without parsing prose.
32
+ */
33
+ export class StateTransferError extends Error {
34
+ code;
35
+ hint;
36
+ detail;
37
+ constructor(code, message, hint, detail = {}) {
38
+ super(message);
39
+ this.code = code;
40
+ this.hint = hint;
41
+ this.detail = detail;
42
+ this.name = 'StateTransferError';
43
+ }
44
+ /** JSON shape shared by the runtime response, the control-plane envelope, and CLI diagnostics. */
45
+ toDiagnostic() {
46
+ return { code: this.code, message: this.message, ...(this.hint ? { hint: this.hint } : {}), detail: this.detail };
47
+ }
48
+ }
49
+ function malformed(message, detail = {}) {
50
+ return new StateTransferError('state_export_malformed', message, 'Import the unmodified file `xeer export` produced; a hand-edited document must keep the '
51
+ + 'protocol, schema, tables, and counts fields consistent.', detail);
52
+ }
53
+ function plainObject(value) {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
55
+ ? value : null;
56
+ }
57
+ function isoTimestamp(value) {
58
+ return typeof value === 'string' && value.length > 0 && !Number.isNaN(Date.parse(value));
59
+ }
60
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/u;
61
+ /**
62
+ * Structural validation of the value encoding before anything decodes it: `decodeValue` trusts its
63
+ * input, so an imported document must be proven well-formed first.
64
+ */
65
+ function validEncodedValue(value, path) {
66
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
67
+ return;
68
+ if (typeof value === 'number') {
69
+ if (!Number.isFinite(value))
70
+ throw malformed(`Encoded value at ${path} is not a finite number.`);
71
+ return;
72
+ }
73
+ const record = plainObject(value);
74
+ if (!record)
75
+ throw malformed(`Encoded value at ${path} is not a valid xeer.value.v0 node.`);
76
+ const keys = Object.keys(record).sort();
77
+ const tag = record.$xeer;
78
+ if (tag === 'undefined') {
79
+ if (keys.length !== 1)
80
+ throw malformed(`Encoded undefined at ${path} carries unexpected keys.`);
81
+ return;
82
+ }
83
+ if (keys.length !== 2 || keys[0] !== '$xeer' || keys[1] !== 'value') {
84
+ throw malformed(`Encoded value at ${path} must carry exactly $xeer and value.`);
85
+ }
86
+ if (tag === 'date') {
87
+ if (!isoTimestamp(record.value))
88
+ throw malformed(`Encoded date at ${path} is not an ISO timestamp.`);
89
+ return;
90
+ }
91
+ if (tag === 'bytes') {
92
+ if (typeof record.value !== 'string' || !BASE64.test(record.value)) {
93
+ throw malformed(`Encoded bytes at ${path} are not base64.`);
94
+ }
95
+ return;
96
+ }
97
+ if (tag === 'array') {
98
+ if (!Array.isArray(record.value))
99
+ throw malformed(`Encoded array at ${path} has no array value.`);
100
+ record.value.forEach((item, index) => validEncodedValue(item, `${path}[${index}]`));
101
+ return;
102
+ }
103
+ if (tag === 'object') {
104
+ const nested = plainObject(record.value);
105
+ if (!nested)
106
+ throw malformed(`Encoded object at ${path} has no object value.`);
107
+ for (const [key, item] of Object.entries(nested))
108
+ validEncodedValue(item, `${path}.${key}`);
109
+ return;
110
+ }
111
+ throw malformed(`Unknown encoded value tag at ${path}.`, { tag: typeof tag === 'string' ? tag : null });
112
+ }
113
+ /**
114
+ * Whether a decoded value satisfies a declared field. Shared with the runtime so an imported record
115
+ * is held to exactly the same rule a handler-written record is.
116
+ */
117
+ export function fieldValueValid(field, value) {
118
+ // `null` and `undefined` are one state, because the column they land in has one. A nullable column
119
+ // cannot tell "omitted" from "written as null", so accepting one spelling and refusing the other
120
+ // would be a rule about the manifest rather than about the data. It also closes a hole: a required
121
+ // `json` field used to accept `null` here and then fail against its own NOT NULL, turning a
122
+ // repairable validation message into a raw SQLite error.
123
+ if (value === undefined || value === null)
124
+ return field.optional === true;
125
+ switch (field.type) {
126
+ case 'string': return typeof value === 'string' && (field.maxLength === undefined || value.length <= field.maxLength)
127
+ && (field.enum === undefined || field.enum.includes(value));
128
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
129
+ case 'boolean': return typeof value === 'boolean';
130
+ case 'datetime': return value instanceof Date && !Number.isNaN(value.getTime());
131
+ case 'bytes': return value instanceof Uint8Array && (field.maxLength === undefined || value.byteLength <= field.maxLength);
132
+ case 'json': return true;
133
+ // A ref holds the id of a row in another table. Whether that row exists is the FOREIGN KEY's
134
+ // job, checked by SQLite on write; all this can say is that the value has an id's shape.
135
+ case 'ref': return typeof value === 'string' && value.length > 0;
136
+ }
137
+ }
138
+ /**
139
+ * Validates the schema the document embeds. The rows are always held to the *live* schema of the
140
+ * application being imported into, so this check exists to keep a malformed document from reaching
141
+ * the schema planner as anything other than a structured refusal.
142
+ *
143
+ * It defers to the schema that validates a manifest's database block, because it is validating the
144
+ * same thing: a document written by one version of the vocabulary and read by another is exactly the
145
+ * case where a second, hand-rolled copy of the rules would quietly disagree with the first.
146
+ */
147
+ function parseEmbeddedSchema(value) {
148
+ const parsed = normalizedDatabaseSchema.safeParse(value);
149
+ if (!parsed.success) {
150
+ const issue = parsed.error.issues[0];
151
+ throw malformed(`schema.database is not a normalized database schema: ${['database', ...issue.path].join('.')} ${issue.message}.`);
152
+ }
153
+ return parsed.data;
154
+ }
155
+ function parseRecord(table, index, value) {
156
+ const row = plainObject(value);
157
+ if (!row)
158
+ throw malformed(`Row ${index} of table ${table} is not an object.`);
159
+ if (typeof row.id !== 'string' || row.id.length === 0 || row.id.length > 256) {
160
+ throw malformed(`Row ${index} of table ${table} has no usable id.`);
161
+ }
162
+ if (!isoTimestamp(row.createdAt) || !isoTimestamp(row.updatedAt)) {
163
+ throw malformed(`Row ${row.id} of table ${table} has non-ISO timestamps.`);
164
+ }
165
+ if (!Object.hasOwn(row, 'data'))
166
+ throw malformed(`Row ${row.id} of table ${table} carries no data.`);
167
+ const data = plainObject(row.data);
168
+ if (!data || data.$xeer !== 'object') {
169
+ throw malformed(`Row ${row.id} of table ${table} must encode its fields as a xeer.value.v0 object.`);
170
+ }
171
+ validEncodedValue(row.data, `${table}[${index}].data`);
172
+ return { id: row.id, createdAt: row.createdAt, updatedAt: row.updatedAt,
173
+ data: row.data };
174
+ }
175
+ /**
176
+ * Structural validation of an export document. It proves the file is a well-formed
177
+ * `xeer.state-export.v0` document that describes itself consistently; whether its rows fit a
178
+ * particular application is {@link planStateImport}'s question.
179
+ */
180
+ export function parseStateExport(value) {
181
+ const source = plainObject(value);
182
+ if (!source)
183
+ throw malformed('A state export must be a JSON object.');
184
+ if (source.protocol !== STATE_EXPORT_PROTOCOL) {
185
+ throw new StateTransferError('state_export_protocol_unsupported', `Unsupported state export protocol ${JSON.stringify(source.protocol ?? null)}; this build reads ${STATE_EXPORT_PROTOCOL}.`, 'Re-export the state with a matching `xeer` version, or upgrade the CLI to one that reads this protocol.', { expected: STATE_EXPORT_PROTOCOL, found: source.protocol ?? null });
186
+ }
187
+ if (!isoTimestamp(source.exportedAt))
188
+ throw malformed('exportedAt must be an ISO timestamp.');
189
+ if (typeof source.application !== 'string' || source.application.length === 0) {
190
+ throw malformed('application must be the exporting application name.');
191
+ }
192
+ if (source.appId !== undefined && (typeof source.appId !== 'string' || source.appId.length === 0)) {
193
+ throw malformed('appId must be a non-empty string when present.');
194
+ }
195
+ const origin = plainObject(source.source);
196
+ if (!origin || typeof origin.artifactId !== 'string' || !Number.isSafeInteger(origin.generation)) {
197
+ throw malformed('source must carry the exporting artifactId and generation.');
198
+ }
199
+ const storage = plainObject(source.storage);
200
+ if (!storage || !Number.isSafeInteger(storage.schemaVersion)) {
201
+ throw malformed('storage.schemaVersion must be an integer.');
202
+ }
203
+ const schema = plainObject(source.schema);
204
+ if (!schema || !Number.isSafeInteger(schema.applicationSchemaVersion)
205
+ || typeof schema.schemaHash !== 'string' || !/^sha256:[0-9a-f]{64}$/u.test(schema.schemaHash)) {
206
+ throw malformed('schema must carry applicationSchemaVersion, schemaHash, and the normalized database schema.');
207
+ }
208
+ const database = parseEmbeddedSchema(schema.database);
209
+ if (Number(schema.applicationSchemaVersion) !== database.version) {
210
+ throw malformed('schema.applicationSchemaVersion does not match the embedded database.version.', { applicationSchemaVersion: schema.applicationSchemaVersion, databaseVersion: database.version });
211
+ }
212
+ if (!Array.isArray(source.tables))
213
+ throw malformed('tables must be an array.');
214
+ const names = new Set();
215
+ const tables = source.tables.map((entry) => {
216
+ const table = plainObject(entry);
217
+ if (!table || typeof table.name !== 'string' || table.name.length === 0) {
218
+ throw malformed('Every table must carry a name.');
219
+ }
220
+ if (names.has(table.name))
221
+ throw malformed(`Table ${table.name} appears twice.`);
222
+ names.add(table.name);
223
+ if (!Array.isArray(table.rows))
224
+ throw malformed(`Table ${table.name} must carry a rows array.`);
225
+ if (table.records !== table.rows.length) {
226
+ throw malformed(`Table ${table.name} declares ${String(table.records)} records but carries ${table.rows.length}.`, { table: table.name, declared: table.records, rows: table.rows.length });
227
+ }
228
+ const rows = table.rows.map((row, index) => parseRecord(table.name, index, row));
229
+ const ids = new Set(rows.map((row) => row.id));
230
+ if (ids.size !== rows.length)
231
+ throw malformed(`Table ${table.name} repeats a record id.`, { table: table.name });
232
+ return { name: table.name, records: rows.length, rows };
233
+ });
234
+ const counts = plainObject(source.counts);
235
+ const records = tables.reduce((total, table) => total + table.records, 0);
236
+ if (!counts || counts.tables !== tables.length || counts.records !== records) {
237
+ throw malformed('counts must match the exported tables and rows.', { tables: tables.length, records, declared: counts ?? null });
238
+ }
239
+ return {
240
+ protocol: STATE_EXPORT_PROTOCOL,
241
+ exportedAt: source.exportedAt,
242
+ application: source.application,
243
+ ...(source.appId === undefined ? {} : { appId: source.appId }),
244
+ source: { artifactId: origin.artifactId, generation: Number(origin.generation) },
245
+ storage: { schemaVersion: Number(storage.schemaVersion) },
246
+ schema: {
247
+ applicationSchemaVersion: Number(schema.applicationSchemaVersion),
248
+ schemaHash: schema.schemaHash,
249
+ database,
250
+ },
251
+ tables,
252
+ counts: { tables: tables.length, records },
253
+ };
254
+ }
255
+ function schemaMismatch(message, document, identity, detail = {}) {
256
+ return new StateTransferError('state_import_schema_mismatch', message, `Check out the application schema this export was taken from (database.version `
257
+ + `${document.schema.applicationSchemaVersion}, ${document.schema.schemaHash}), import there, and let the `
258
+ + 'normal compatible-change path carry the data forward — or re-export from the current schema. '
259
+ + 'There is no forced import: writing rows a schema does not describe would corrupt the state '
260
+ + 'the application is allowed to assume.', { expected: identity, found: { applicationSchemaVersion: document.schema.applicationSchemaVersion,
261
+ schemaHash: document.schema.schemaHash }, ...detail });
262
+ }
263
+ /**
264
+ * Decides whether an export may be written into the schema currently in force, and validates every
265
+ * row against it.
266
+ *
267
+ * An export is accepted when it came from this exact schema, or from a schema the current one is a
268
+ * *compatible* successor of — the same classification the runtime already uses to accept a schema
269
+ * change without a reset, so an import can never introduce a row the running application could not
270
+ * have written itself. Anything else is refused with the mismatch in `detail`; there is no override,
271
+ * because the alternative is state the application's own type contract does not describe.
272
+ */
273
+ export function planStateImport(input) {
274
+ const { document, application, schema, identity } = input;
275
+ if (document.application !== application) {
276
+ throw new StateTransferError('state_import_application_mismatch', `This export belongs to application ${JSON.stringify(document.application)}, not ${JSON.stringify(application)}.`, 'Import it into the application it was exported from. Application state is not portable between '
277
+ + 'applications: table and field meaning is defined per application.', { expected: application, found: document.application });
278
+ }
279
+ const identical = document.schema.schemaHash === identity.schemaHash
280
+ && document.schema.applicationSchemaVersion === identity.applicationSchemaVersion;
281
+ let steps = [];
282
+ if (!identical) {
283
+ let plan;
284
+ try {
285
+ plan = planSchemaChangeWithIdentities(application, document.schema.database, { applicationSchemaVersion: document.schema.applicationSchemaVersion,
286
+ schemaHash: document.schema.schemaHash }, schema, identity);
287
+ }
288
+ catch (error) {
289
+ throw schemaMismatch(error instanceof SchemaPlanningError
290
+ ? `This export cannot be replayed into the current schema: ${error.message}`
291
+ : 'This export cannot be replayed into the current schema.', document, identity, { planningError: error instanceof SchemaPlanningError ? error.code : 'schema_plan_failed' });
292
+ }
293
+ if (plan.classification === 'incompatible') {
294
+ throw schemaMismatch(`This export was taken from application schema version ${document.schema.applicationSchemaVersion}, which is `
295
+ + `incompatible with the current version ${identity.applicationSchemaVersion}.`, document, identity, { steps: plan.steps });
296
+ }
297
+ steps = plan.steps;
298
+ }
299
+ const tables = [];
300
+ for (const table of document.tables) {
301
+ const definition = schema.tables[table.name];
302
+ if (!definition) {
303
+ throw schemaMismatch(`The current schema has no table ${JSON.stringify(table.name)}.`, document, identity, { table: table.name, currentTables: Object.keys(schema.tables).sort() });
304
+ }
305
+ for (const row of table.rows)
306
+ validateExportedRecord(table.name, definition, row);
307
+ tables.push({ name: table.name, records: table.records });
308
+ }
309
+ return { classification: identical ? 'identical' : 'compatible', steps, tables, records: document.counts.records };
310
+ }
311
+ /** Holds one imported record to the declared field contract of the table it is being written into. */
312
+ export function validateExportedRecord(tableName, table, row) {
313
+ const decoded = decodeValue(row.data);
314
+ const invalid = (message, detail = {}) => new StateTransferError('state_import_record_invalid', message, 'Re-export the state from an application whose schema matches this data, or repair the record in '
315
+ + 'the export file so every field matches its declared type.', { table: tableName, record: row.id, ...detail });
316
+ for (const key of Object.keys(decoded)) {
317
+ if (key === 'id' || key === 'createdAt' || key === 'updatedAt') {
318
+ throw invalid(`Record ${row.id} of ${tableName} stores runtime-owned field ${key} in its data.`, { field: key });
319
+ }
320
+ if (!(key in table.fields)) {
321
+ throw invalid(`Record ${row.id} of ${tableName} carries unknown field ${key}.`, { field: key, fields: Object.keys(table.fields).sort() });
322
+ }
323
+ }
324
+ for (const [name, field] of Object.entries(table.fields)) {
325
+ if (!fieldValueValid(field, decoded[name])) {
326
+ throw invalid(`Record ${row.id} of ${tableName} has an invalid value for ${name}.`, { field: name, expected: field.type, optional: field.optional === true });
327
+ }
328
+ }
329
+ return decoded;
330
+ }
331
+ /** Serialized size of a document, used to hold every hop to {@link MAX_STATE_TRANSFER_BYTES}. */
332
+ export function stateExportBytes(document) {
333
+ return new TextEncoder().encode(JSON.stringify(document)).byteLength;
334
+ }
335
+ export function assertStateTransferSize(document, what) {
336
+ const bytes = stateExportBytes(document);
337
+ if (bytes <= MAX_STATE_TRANSFER_BYTES)
338
+ return;
339
+ throw new StateTransferError('state_export_too_large', `This state ${what} is ${bytes} bytes, above the ${MAX_STATE_TRANSFER_BYTES}-byte transfer ceiling.`, 'Xeer v0 transfers state in one document. Reduce the stored data, or read it in place with '
340
+ + '`xeer state` until chunked transfer lands.', { bytes, limit: MAX_STATE_TRANSFER_BYTES });
341
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The `storage` capability's shared vocabulary: the platform ceilings a manifest is capped against,
3
+ * the object-key contract, and the binding name a provisioned bucket is attached under.
4
+ *
5
+ * This module is deliberately dependency-free and lives in `spec` rather than in the runtime, because
6
+ * three separate layers have to agree on exactly the same rules and none of them may re-derive them:
7
+ *
8
+ * the compiler normalizes and caps the manifest's declared limits (`schema.ts`);
9
+ * the runtime validates every key and enforces every byte budget before touching R2;
10
+ * the control plane derives the bucket name and never lets it reach the artifact.
11
+ *
12
+ * A key rule that differed between the compiler and the runtime would mean an application that
13
+ * builds and then fails at call time, which is the one failure mode a declared contract exists to
14
+ * remove.
15
+ */
16
+ /**
17
+ * The Worker binding name a provisioned per-app bucket is attached under. Platform-owned and never
18
+ * user-chosen, like `XEER_STATE`: the `XEER_` prefix is reserved everywhere a name can be supplied
19
+ * (the control plane's environment store refuses it, `packages/control/src/env.ts`), so an
20
+ * application value can never shadow it and `ctx.env` can never reach it — `selectAppEnv` projects
21
+ * only `XEER_ENV_*`, and an R2 bucket is not a string.
22
+ */
23
+ export declare const STORAGE_BINDING_NAME: "XEER_STORAGE";
24
+ /**
25
+ * The platform ceiling for one stored object, and the default when a manifest declares the
26
+ * capability without naming a limit.
27
+ *
28
+ * 25 MiB matches the artifact's own per-asset ceiling, which is the largest single payload anything
29
+ * else in v0 accepts, so an application cannot store an object it could never have shipped. It is
30
+ * well under R2's own 5 GiB single-put limit: the binding constraint is not R2 but the Worker request
31
+ * body a `put` is fed from.
32
+ */
33
+ export declare const STORAGE_MAX_OBJECT_BYTES: number;
34
+ /**
35
+ * Per-handler read and write byte budgets, in the same spirit as `queryRows` and `mutationWrites`:
36
+ * one handler may not stream unbounded bytes through the platform on a single request. Defaults are
37
+ * generous enough for a handler that moves one maximum-sized object and small enough that a runaway
38
+ * loop is refused rather than billed.
39
+ */
40
+ export declare const STORAGE_DEFAULT_READ_BYTES: number;
41
+ export declare const STORAGE_DEFAULT_WRITE_BYTES: number;
42
+ /** Ceilings for the two budgets above; a manifest may lower them, never raise them past these. */
43
+ export declare const STORAGE_MAX_READ_BYTES: number;
44
+ export declare const STORAGE_MAX_WRITE_BYTES: number;
45
+ /**
46
+ * `list` is bounded exactly the way `find` is: a default page a caller does not have to think about,
47
+ * and a hard ceiling it cannot ask past. Not manifest-configurable — a listing page size is a
48
+ * transport detail, not an application contract, and R2's own list ceiling is 1,000.
49
+ */
50
+ export declare const STORAGE_LIST_DEFAULT_LIMIT = 100;
51
+ export declare const STORAGE_LIST_MAX_LIMIT = 1000;
52
+ /** Longest object key. R2 accepts 1,024 UTF-8 bytes; half of that is ample and leaves headroom. */
53
+ export declare const STORAGE_MAX_KEY_BYTES = 512;
54
+ export type StorageKeyRefusal = 'storage_key_empty' | 'storage_key_too_long' | 'storage_key_invalid';
55
+ /** Why a key is refused, or `null` when it is acceptable. One implementation, three consumers. */
56
+ export declare function storageKeyRefusal(key: unknown): StorageKeyRefusal | null;
57
+ /** The same question for a list prefix, which may end in a slash or mid-segment. */
58
+ export declare function storagePrefixRefusal(prefix: unknown): StorageKeyRefusal | null;
59
+ export declare function storageKeyValid(key: unknown): key is string;
60
+ /** The human wording for each refusal, shared so the runtime and the CLI say the same thing. */
61
+ export declare function storageKeyMessage(refusal: StorageKeyRefusal, key: unknown, kind?: string): string;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The `storage` capability's shared vocabulary: the platform ceilings a manifest is capped against,
3
+ * the object-key contract, and the binding name a provisioned bucket is attached under.
4
+ *
5
+ * This module is deliberately dependency-free and lives in `spec` rather than in the runtime, because
6
+ * three separate layers have to agree on exactly the same rules and none of them may re-derive them:
7
+ *
8
+ * the compiler normalizes and caps the manifest's declared limits (`schema.ts`);
9
+ * the runtime validates every key and enforces every byte budget before touching R2;
10
+ * the control plane derives the bucket name and never lets it reach the artifact.
11
+ *
12
+ * A key rule that differed between the compiler and the runtime would mean an application that
13
+ * builds and then fails at call time, which is the one failure mode a declared contract exists to
14
+ * remove.
15
+ */
16
+ /**
17
+ * The Worker binding name a provisioned per-app bucket is attached under. Platform-owned and never
18
+ * user-chosen, like `XEER_STATE`: the `XEER_` prefix is reserved everywhere a name can be supplied
19
+ * (the control plane's environment store refuses it, `packages/control/src/env.ts`), so an
20
+ * application value can never shadow it and `ctx.env` can never reach it — `selectAppEnv` projects
21
+ * only `XEER_ENV_*`, and an R2 bucket is not a string.
22
+ */
23
+ export const STORAGE_BINDING_NAME = 'XEER_STORAGE';
24
+ /**
25
+ * The platform ceiling for one stored object, and the default when a manifest declares the
26
+ * capability without naming a limit.
27
+ *
28
+ * 25 MiB matches the artifact's own per-asset ceiling, which is the largest single payload anything
29
+ * else in v0 accepts, so an application cannot store an object it could never have shipped. It is
30
+ * well under R2's own 5 GiB single-put limit: the binding constraint is not R2 but the Worker request
31
+ * body a `put` is fed from.
32
+ */
33
+ export const STORAGE_MAX_OBJECT_BYTES = 25 * 1024 * 1024;
34
+ /**
35
+ * Per-handler read and write byte budgets, in the same spirit as `queryRows` and `mutationWrites`:
36
+ * one handler may not stream unbounded bytes through the platform on a single request. Defaults are
37
+ * generous enough for a handler that moves one maximum-sized object and small enough that a runaway
38
+ * loop is refused rather than billed.
39
+ */
40
+ export const STORAGE_DEFAULT_READ_BYTES = 32 * 1024 * 1024;
41
+ export const STORAGE_DEFAULT_WRITE_BYTES = 32 * 1024 * 1024;
42
+ /** Ceilings for the two budgets above; a manifest may lower them, never raise them past these. */
43
+ export const STORAGE_MAX_READ_BYTES = 128 * 1024 * 1024;
44
+ export const STORAGE_MAX_WRITE_BYTES = 128 * 1024 * 1024;
45
+ /**
46
+ * `list` is bounded exactly the way `find` is: a default page a caller does not have to think about,
47
+ * and a hard ceiling it cannot ask past. Not manifest-configurable — a listing page size is a
48
+ * transport detail, not an application contract, and R2's own list ceiling is 1,000.
49
+ */
50
+ export const STORAGE_LIST_DEFAULT_LIMIT = 100;
51
+ export const STORAGE_LIST_MAX_LIMIT = 1_000;
52
+ /** Longest object key. R2 accepts 1,024 UTF-8 bytes; half of that is ample and leaves headroom. */
53
+ export const STORAGE_MAX_KEY_BYTES = 512;
54
+ /**
55
+ * Object keys are a constrained, printable, path-like vocabulary rather than "any string R2 takes".
56
+ *
57
+ * The reasons are all about keys being *quoted* somewhere later — a URL path, a log line, a signed
58
+ * token, an operator's shell — and none of them are hypothetical:
59
+ *
60
+ * - control characters and whitespace would corrupt any of those quotings;
61
+ * - `..` and a leading `/` make a key look like a traversal to whatever reads it next, and a key
62
+ * that reads as a traversal invites code that treats it as one;
63
+ * - `//` and a trailing `/` produce two keys that look like one directory to a human and are not.
64
+ *
65
+ * The result is that a key is safe to put in a path segment, a log field, and an audit row without
66
+ * any escaping decision being made per call site.
67
+ */
68
+ const SEGMENT = String.raw `[A-Za-z0-9!._~()'*\- ]+`;
69
+ const STORAGE_KEY = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`, 'u');
70
+ /**
71
+ * A list prefix is **not** a key, and conflating the two would break the most ordinary listing there is.
72
+ *
73
+ * R2's keyspace is flat, so `list({ prefix: 'users/' })` is how an application asks for what a human
74
+ * would call a folder — and a trailing slash is exactly what a key may not have. A prefix is also allowed
75
+ * to end mid-segment (`'users/4'` matches `users/42`), which is what makes it a prefix rather than a
76
+ * path. So the rules that carry over are the ones about *safety* — the charset, no dot segments, no
77
+ * leading slash, no empty interior segments — and the ones about a key being a complete path do not.
78
+ */
79
+ const STORAGE_PREFIX = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*/?$`, 'u');
80
+ /** Segments that make a key or prefix *read* as a traversal, whatever handles it next. */
81
+ function hasDotSegment(value) {
82
+ return value.split('/').some((segment) => segment === '.' || segment === '..');
83
+ }
84
+ /** Why a key is refused, or `null` when it is acceptable. One implementation, three consumers. */
85
+ export function storageKeyRefusal(key) {
86
+ if (typeof key !== 'string' || key.length === 0)
87
+ return 'storage_key_empty';
88
+ if (new TextEncoder().encode(key).byteLength > STORAGE_MAX_KEY_BYTES)
89
+ return 'storage_key_too_long';
90
+ // Checked before the pattern so the dot-segment refusal is reported as such rather than as a
91
+ // generic charset failure: `.` and `..` are legal characters, they are only illegal as segments.
92
+ if (hasDotSegment(key))
93
+ return 'storage_key_invalid';
94
+ return STORAGE_KEY.test(key) ? null : 'storage_key_invalid';
95
+ }
96
+ /** The same question for a list prefix, which may end in a slash or mid-segment. */
97
+ export function storagePrefixRefusal(prefix) {
98
+ if (typeof prefix !== 'string' || prefix.length === 0)
99
+ return 'storage_key_empty';
100
+ if (new TextEncoder().encode(prefix).byteLength > STORAGE_MAX_KEY_BYTES)
101
+ return 'storage_key_too_long';
102
+ if (hasDotSegment(prefix))
103
+ return 'storage_key_invalid';
104
+ return STORAGE_PREFIX.test(prefix) ? null : 'storage_key_invalid';
105
+ }
106
+ export function storageKeyValid(key) {
107
+ return storageKeyRefusal(key) === null;
108
+ }
109
+ /** The human wording for each refusal, shared so the runtime and the CLI say the same thing. */
110
+ export function storageKeyMessage(refusal, key, kind = 'key') {
111
+ const quoted = typeof key === 'string' ? JSON.stringify(key.slice(0, 80)) : String(key);
112
+ switch (refusal) {
113
+ case 'storage_key_empty':
114
+ return `A storage ${kind} must be a non-empty string.`;
115
+ case 'storage_key_too_long':
116
+ return `A storage ${kind} may not exceed ${STORAGE_MAX_KEY_BYTES} bytes.`;
117
+ default:
118
+ return `A storage ${kind} must be slash-separated printable segments without dot segments: ${quoted}`;
119
+ }
120
+ }