@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.
- package/README.md +4 -1
- package/dist/dev-session.d.ts +1 -1
- package/dist/network-policy.js +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +4 -4
- package/dist/test-run.d.ts +1 -1
- package/dist/xeer-cli.d.ts +1 -1
- package/package.json +8 -5
- package/vendor/spec/actions.d.ts +1250 -0
- package/vendor/spec/actions.js +805 -0
- package/vendor/spec/admin-sql.d.ts +59 -0
- package/vendor/spec/admin-sql.js +147 -0
- package/vendor/spec/admin.d.ts +110 -0
- package/vendor/spec/admin.js +58 -0
- package/vendor/spec/canonical.d.ts +3 -0
- package/vendor/spec/canonical.js +36 -0
- package/vendor/spec/diagnostics.d.ts +49 -0
- package/vendor/spec/diagnostics.js +500 -0
- package/vendor/spec/docs.d.ts +21 -0
- package/vendor/spec/docs.js +57 -0
- package/vendor/spec/events.d.ts +8 -0
- package/vendor/spec/events.js +21 -0
- package/vendor/spec/identity-keys.d.ts +36 -0
- package/vendor/spec/identity-keys.js +72 -0
- package/vendor/spec/index.d.ts +20 -0
- package/vendor/spec/index.js +20 -0
- package/vendor/spec/local-identity.d.ts +69 -0
- package/vendor/spec/local-identity.js +132 -0
- package/vendor/spec/network-policy.d.ts +16 -0
- package/vendor/spec/network-policy.js +50 -0
- package/vendor/spec/public-assets.d.ts +153 -0
- package/vendor/spec/public-assets.js +166 -0
- package/vendor/spec/review.d.ts +82 -0
- package/vendor/spec/review.js +175 -0
- package/vendor/spec/route.d.ts +43 -0
- package/vendor/spec/route.js +87 -0
- package/vendor/spec/schema-lifecycle.d.ts +6 -0
- package/vendor/spec/schema-lifecycle.js +59 -0
- package/vendor/spec/schema-plan.d.ts +98 -0
- package/vendor/spec/schema-plan.js +194 -0
- package/vendor/spec/schema.d.ts +166 -0
- package/vendor/spec/schema.js +409 -0
- package/vendor/spec/sql-expression.d.ts +91 -0
- package/vendor/spec/sql-expression.js +650 -0
- package/vendor/spec/state-export.d.ts +143 -0
- package/vendor/spec/state-export.js +341 -0
- package/vendor/spec/storage.d.ts +61 -0
- package/vendor/spec/storage.js +120 -0
- package/vendor/spec/table-ddl.d.ts +162 -0
- package/vendor/spec/table-ddl.js +508 -0
- package/vendor/spec/types.d.ts +275 -0
- package/vendor/spec/types.js +11 -0
- package/vendor/spec/value.d.ts +22 -0
- package/vendor/spec/value.js +72 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a single SQL statement may be run against an application's own database.
|
|
3
|
+
*
|
|
4
|
+
* The shape of this is not obvious, so the reasoning is here rather than in a commit message.
|
|
5
|
+
*
|
|
6
|
+
* Direct SQL is safer than it looks. The schema's `CHECK`, `NOT NULL`, `UNIQUE` and foreign-key
|
|
7
|
+
* constraints bind *every* writer, not only `ctx.db` — `UPDATE posts SET position = 'abc'` is refused
|
|
8
|
+
* by the same constraint that makes the column numeric in the first place, because SQLite's NUMERIC
|
|
9
|
+
* is an affinity and the CHECK is the actual contract. Ordinary DML therefore cannot store a value
|
|
10
|
+
* the generated types call impossible.
|
|
11
|
+
*
|
|
12
|
+
* Two things escape that, and they are what this refuses.
|
|
13
|
+
*
|
|
14
|
+
* `PRAGMA ignore_check_constraints` is writable and genuinely switches `CHECK` off. A console that
|
|
15
|
+
* could set it would write exactly the values the contract forbids, and the application would then
|
|
16
|
+
* read them back through an interface whose types say they cannot exist.
|
|
17
|
+
*
|
|
18
|
+
* DDL is worse, and it is why writes are gated rather than simply allowed. The runtime reconciles the
|
|
19
|
+
* physical schema against the DDL text it recorded, so a `DROP TABLE` from a console desynchronises
|
|
20
|
+
* bookkeeping the runtime believes it owns alone. That is not an operator changing their own data,
|
|
21
|
+
* which is their business — it is a failure that surfaces later and looks like a runtime bug.
|
|
22
|
+
*
|
|
23
|
+
* So: reads by default, writes only when asked for explicitly, and neither DDL nor a pragma that can
|
|
24
|
+
* change enforcement at all.
|
|
25
|
+
*/
|
|
26
|
+
import type { EncodedValue } from './value.js';
|
|
27
|
+
import { ADMIN_PROTOCOL } from './admin.js';
|
|
28
|
+
export type AdminStatementKind = 'read' | 'write';
|
|
29
|
+
export type AdminStatementRefusalCode = 'admin_sql_empty' | 'admin_sql_multiple_statements' | 'admin_sql_statement_not_allowed' | 'admin_sql_pragma_not_allowed' | 'admin_sql_reserved_table';
|
|
30
|
+
export interface AdminStatementRefusal {
|
|
31
|
+
readonly code: AdminStatementRefusalCode;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
}
|
|
34
|
+
export type AdminStatementClassification = {
|
|
35
|
+
readonly ok: true;
|
|
36
|
+
readonly kind: AdminStatementKind;
|
|
37
|
+
} | {
|
|
38
|
+
readonly ok: false;
|
|
39
|
+
readonly refusal: AdminStatementRefusal;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Decides whether one statement may run, and whether it counts as a write.
|
|
43
|
+
*
|
|
44
|
+
* `reservedTable` is injected rather than imported so this stays a pure decision about SQL text; the
|
|
45
|
+
* runtime passes the same reservation the schema generator uses.
|
|
46
|
+
*/
|
|
47
|
+
export declare function classifyAdminStatement(sql: string, reservedTable: (name: string) => boolean): AdminStatementClassification;
|
|
48
|
+
/** The most rows one statement returns before the rest are withheld and `truncated` is reported. */
|
|
49
|
+
export declare const ADMIN_SQL_MAX_ROWS = 200;
|
|
50
|
+
export interface AdminSqlResponseV0 {
|
|
51
|
+
protocol: typeof ADMIN_PROTOCOL;
|
|
52
|
+
/** Column names in result order, so a caller can render a table without inspecting every row. */
|
|
53
|
+
columns: readonly string[];
|
|
54
|
+
rows: readonly EncodedValue[];
|
|
55
|
+
/** Rows changed by a write. Absent for a read. */
|
|
56
|
+
rowsWritten?: number;
|
|
57
|
+
/** True when the statement produced more rows than {@link ADMIN_SQL_MAX_ROWS}. */
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a single SQL statement may be run against an application's own database.
|
|
3
|
+
*
|
|
4
|
+
* The shape of this is not obvious, so the reasoning is here rather than in a commit message.
|
|
5
|
+
*
|
|
6
|
+
* Direct SQL is safer than it looks. The schema's `CHECK`, `NOT NULL`, `UNIQUE` and foreign-key
|
|
7
|
+
* constraints bind *every* writer, not only `ctx.db` — `UPDATE posts SET position = 'abc'` is refused
|
|
8
|
+
* by the same constraint that makes the column numeric in the first place, because SQLite's NUMERIC
|
|
9
|
+
* is an affinity and the CHECK is the actual contract. Ordinary DML therefore cannot store a value
|
|
10
|
+
* the generated types call impossible.
|
|
11
|
+
*
|
|
12
|
+
* Two things escape that, and they are what this refuses.
|
|
13
|
+
*
|
|
14
|
+
* `PRAGMA ignore_check_constraints` is writable and genuinely switches `CHECK` off. A console that
|
|
15
|
+
* could set it would write exactly the values the contract forbids, and the application would then
|
|
16
|
+
* read them back through an interface whose types say they cannot exist.
|
|
17
|
+
*
|
|
18
|
+
* DDL is worse, and it is why writes are gated rather than simply allowed. The runtime reconciles the
|
|
19
|
+
* physical schema against the DDL text it recorded, so a `DROP TABLE` from a console desynchronises
|
|
20
|
+
* bookkeeping the runtime believes it owns alone. That is not an operator changing their own data,
|
|
21
|
+
* which is their business — it is a failure that surfaces later and looks like a runtime bug.
|
|
22
|
+
*
|
|
23
|
+
* So: reads by default, writes only when asked for explicitly, and neither DDL nor a pragma that can
|
|
24
|
+
* change enforcement at all.
|
|
25
|
+
*/
|
|
26
|
+
import { ADMIN_PROTOCOL } from './admin.js';
|
|
27
|
+
/** Statements that only read. `EXPLAIN` is here because it plans a statement without running it. */
|
|
28
|
+
const READ_VERBS = new Set(['SELECT', 'WITH', 'VALUES', 'EXPLAIN']);
|
|
29
|
+
const WRITE_VERBS = new Set(['INSERT', 'UPDATE', 'DELETE', 'REPLACE']);
|
|
30
|
+
/**
|
|
31
|
+
* Pragmas that only report. An allowlist rather than a denylist, because the case that must never
|
|
32
|
+
* slip through is precisely the one nobody remembered to add to a pattern.
|
|
33
|
+
*/
|
|
34
|
+
const READ_PRAGMAS = new Set([
|
|
35
|
+
'table_info', 'table_xinfo', 'table_list', 'index_list', 'index_info', 'index_xinfo',
|
|
36
|
+
'foreign_key_list', 'foreign_key_check', 'quick_check',
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* Walks the statement, skipping anything that is not code, and reports its leading keyword and
|
|
40
|
+
* whether a second statement follows.
|
|
41
|
+
*
|
|
42
|
+
* The walk exists for one reason: a `;` inside a string literal or a comment is not a separator. A
|
|
43
|
+
* rule written as a substring search would either refuse `WHERE note = 'a;b'` or accept
|
|
44
|
+
* `SELECT 1; DROP TABLE posts`, and both are wrong in the direction that matters.
|
|
45
|
+
*/
|
|
46
|
+
function scanStatement(sql) {
|
|
47
|
+
const words = [];
|
|
48
|
+
let index = 0;
|
|
49
|
+
let terminated = false;
|
|
50
|
+
while (index < sql.length) {
|
|
51
|
+
const char = sql[index];
|
|
52
|
+
if (char === '-' && sql[index + 1] === '-') {
|
|
53
|
+
const end = sql.indexOf('\n', index);
|
|
54
|
+
index = end < 0 ? sql.length : end + 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (char === '/' && sql[index + 1] === '*') {
|
|
58
|
+
const end = sql.indexOf('*/', index + 2);
|
|
59
|
+
index = end < 0 ? sql.length : end + 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (char === "'" || char === '"' || char === '`' || char === '[') {
|
|
63
|
+
const closing = char === '[' ? ']' : char;
|
|
64
|
+
let end = index + 1;
|
|
65
|
+
while (end < sql.length && sql[end] !== closing)
|
|
66
|
+
end += 1;
|
|
67
|
+
// A quoted identifier can be a statement's target table, so it counts as a word.
|
|
68
|
+
if (char !== "'" && words.length < 3)
|
|
69
|
+
words.push(sql.slice(index + 1, end));
|
|
70
|
+
index = end + 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === ';') {
|
|
74
|
+
terminated = true;
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/\s/.test(char)) {
|
|
79
|
+
index += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// Any code at all after a terminator is a second statement, whatever it happens to be.
|
|
83
|
+
if (terminated)
|
|
84
|
+
return { verb: (words[0] ?? '').toUpperCase(), words, multiple: true };
|
|
85
|
+
if (/[A-Za-z_]/.test(char)) {
|
|
86
|
+
let end = index;
|
|
87
|
+
while (end < sql.length && /[A-Za-z0-9_]/.test(sql[end]))
|
|
88
|
+
end += 1;
|
|
89
|
+
if (words.length < 3)
|
|
90
|
+
words.push(sql.slice(index, end));
|
|
91
|
+
index = end;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
return { verb: (words[0] ?? '').toUpperCase(), words, multiple: false };
|
|
97
|
+
}
|
|
98
|
+
/** The table a write targets: the word after `UPDATE`, `INSERT INTO`, `DELETE FROM`, `REPLACE INTO`. */
|
|
99
|
+
function writeTarget(verb, words) {
|
|
100
|
+
if (verb === 'UPDATE')
|
|
101
|
+
return words[1] ?? '';
|
|
102
|
+
// `INSERT`/`REPLACE`/`DELETE` are followed by `INTO`/`FROM`, then the table. `INSERT OR IGNORE`
|
|
103
|
+
// pushes it further out than the three words scanned, which is why an unrecognized shape returns
|
|
104
|
+
// nothing and is treated as unnamed rather than as a match.
|
|
105
|
+
const second = (words[1] ?? '').toUpperCase();
|
|
106
|
+
return second === 'INTO' || second === 'FROM' ? words[2] ?? '' : '';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decides whether one statement may run, and whether it counts as a write.
|
|
110
|
+
*
|
|
111
|
+
* `reservedTable` is injected rather than imported so this stays a pure decision about SQL text; the
|
|
112
|
+
* runtime passes the same reservation the schema generator uses.
|
|
113
|
+
*/
|
|
114
|
+
export function classifyAdminStatement(sql, reservedTable) {
|
|
115
|
+
const refuse = (code, message) => ({ ok: false, refusal: { code, message } });
|
|
116
|
+
const { verb, words, multiple } = scanStatement(sql);
|
|
117
|
+
if (multiple) {
|
|
118
|
+
return refuse('admin_sql_multiple_statements', 'Send one statement per call. Only the last statement of a batch may carry parameters and only '
|
|
119
|
+
+ 'its rows are returned, so a batch would silently discard results you asked for.');
|
|
120
|
+
}
|
|
121
|
+
if (verb === '')
|
|
122
|
+
return refuse('admin_sql_empty', 'No SQL statement was supplied.');
|
|
123
|
+
if (verb === 'PRAGMA') {
|
|
124
|
+
const name = (words[1] ?? '').toLowerCase();
|
|
125
|
+
if (!READ_PRAGMAS.has(name) || sql.includes('=')) {
|
|
126
|
+
return refuse('admin_sql_pragma_not_allowed', `PRAGMA ${words[1] ?? '?'} is not available here. Only introspection pragmas run, and none may `
|
|
127
|
+
+ 'be assigned to: one of them switches CHECK enforcement off, which would let this write the '
|
|
128
|
+
+ 'very values the application types say cannot exist.');
|
|
129
|
+
}
|
|
130
|
+
return { ok: true, kind: 'read' };
|
|
131
|
+
}
|
|
132
|
+
if (READ_VERBS.has(verb))
|
|
133
|
+
return { ok: true, kind: 'read' };
|
|
134
|
+
if (WRITE_VERBS.has(verb)) {
|
|
135
|
+
const target = writeTarget(verb, words);
|
|
136
|
+
if (target !== '' && reservedTable(target)) {
|
|
137
|
+
return refuse('admin_sql_reserved_table', `${target} is the runtime's own bookkeeping, not application data. Writing to it desynchronises `
|
|
138
|
+
+ 'the schema the runtime believes it applied from the one that is actually there.');
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, kind: 'write' };
|
|
141
|
+
}
|
|
142
|
+
return refuse('admin_sql_statement_not_allowed', `${verb} is not available here. Schema changes belong to the manifest: the runtime reconciles the `
|
|
143
|
+
+ 'physical schema against the DDL it recorded, so changing it from outside leaves the two '
|
|
144
|
+
+ 'disagreeing with nothing able to notice.');
|
|
145
|
+
}
|
|
146
|
+
/** The most rows one statement returns before the rest are withheld and `truncated` is reported. */
|
|
147
|
+
export const ADMIN_SQL_MAX_ROWS = 200;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin data protocol: enough of an application's own database, over HTTP, for someone to browse
|
|
3
|
+
* and edit their rows without the tool doing it being this repository's admin UI.
|
|
4
|
+
*
|
|
5
|
+
* It is versioned like `xeer.dev.v0` and `xeer.state-export.v0` because that is the point — the
|
|
6
|
+
* shapes here are a contract a third party can build against, not the private wire format of one
|
|
7
|
+
* client. Everything a UI needs to render a table is in `/tables`: field types, constraints, which
|
|
8
|
+
* columns the runtime owns, and which field a table policy scopes rows by.
|
|
9
|
+
*
|
|
10
|
+
* Two decisions are worth stating because they are the ones that would otherwise be reinvented
|
|
11
|
+
* badly.
|
|
12
|
+
*
|
|
13
|
+
* **Paging is keyset, not offset.** A cursor names the last row seen — `(created_at, id)`, which is
|
|
14
|
+
* a total order because `id` is unique — so a page is a range scan rather than a count-and-skip, and
|
|
15
|
+
* inserting a row while someone pages does not shift every later page by one. The cursor is opaque
|
|
16
|
+
* on purpose: it is base64url text and a client should round-trip it, not parse it.
|
|
17
|
+
*
|
|
18
|
+
* **The client supplies the row id on create.** That is the whole idempotency story: the insert is
|
|
19
|
+
* `INSERT OR IGNORE`, so a retried request writes the same id and changes nothing, and the response
|
|
20
|
+
* says whether this call was the one that created it. No request ledger, no dedupe window.
|
|
21
|
+
*/
|
|
22
|
+
import type { EncodedValue } from './value.js';
|
|
23
|
+
export declare const ADMIN_PROTOCOL: "xeer.admin.v0";
|
|
24
|
+
/** Columns the runtime owns on every table. Present in every row, never writable by a client. */
|
|
25
|
+
export declare const ADMIN_RUNTIME_FIELDS: readonly AdminFieldV0[];
|
|
26
|
+
/** The default and the ceiling for one page. The ceiling bounds the response, not the table. */
|
|
27
|
+
export declare const ADMIN_PAGE_DEFAULT = 50;
|
|
28
|
+
export declare const ADMIN_PAGE_MAX = 200;
|
|
29
|
+
export interface AdminFieldV0 {
|
|
30
|
+
name: string;
|
|
31
|
+
type: string;
|
|
32
|
+
optional: boolean;
|
|
33
|
+
/** True for a runtime column and for a generated column: both are readable and never writable. */
|
|
34
|
+
readOnly: boolean;
|
|
35
|
+
maxLength?: number;
|
|
36
|
+
enum?: readonly string[];
|
|
37
|
+
default?: string | number | boolean;
|
|
38
|
+
collate?: string;
|
|
39
|
+
unique?: boolean;
|
|
40
|
+
/** For a `ref` field, the table its value points at. */
|
|
41
|
+
references?: string;
|
|
42
|
+
/** For a generated column, the expression that computes it — worth showing, never editable. */
|
|
43
|
+
generated?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface AdminIndexV0 {
|
|
46
|
+
name: string;
|
|
47
|
+
/** Column terms only; an expression term has no name a filter could use. */
|
|
48
|
+
columns: readonly string[];
|
|
49
|
+
unique: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How a table policy scopes rows, when one is declared.
|
|
53
|
+
*
|
|
54
|
+
* Reported because an admin client has to *ask* for the owner or workspace on create: admin access
|
|
55
|
+
* lifts row visibility but deliberately does not stamp ownership, so a row created without one would
|
|
56
|
+
* be a row no application user could ever read.
|
|
57
|
+
*/
|
|
58
|
+
export interface AdminTablePolicyV0 {
|
|
59
|
+
kind: 'owner' | 'workspace';
|
|
60
|
+
field: string;
|
|
61
|
+
}
|
|
62
|
+
export interface AdminTableV0 {
|
|
63
|
+
name: string;
|
|
64
|
+
fields: readonly AdminFieldV0[];
|
|
65
|
+
indexes: readonly AdminIndexV0[];
|
|
66
|
+
policy: AdminTablePolicyV0 | null;
|
|
67
|
+
}
|
|
68
|
+
export interface AdminTablesResponseV0 {
|
|
69
|
+
protocol: typeof ADMIN_PROTOCOL;
|
|
70
|
+
application: string;
|
|
71
|
+
artifactId: string;
|
|
72
|
+
generation: number;
|
|
73
|
+
schema: {
|
|
74
|
+
applicationSchemaVersion: number;
|
|
75
|
+
schemaHash: string;
|
|
76
|
+
};
|
|
77
|
+
/** The same three columns on every table, described once rather than repeated per table. */
|
|
78
|
+
runtimeFields: readonly AdminFieldV0[];
|
|
79
|
+
tables: readonly AdminTableV0[];
|
|
80
|
+
}
|
|
81
|
+
export interface AdminRowsResponseV0 {
|
|
82
|
+
protocol: typeof ADMIN_PROTOCOL;
|
|
83
|
+
table: string;
|
|
84
|
+
/** Rows in `(created_at, id)` order, each encoded with the `xeer.value.v0` codec. */
|
|
85
|
+
rows: readonly EncodedValue[];
|
|
86
|
+
/** Pass back as `?cursor=` for the next page. `null` when this page is the last one. */
|
|
87
|
+
cursor: string | null;
|
|
88
|
+
}
|
|
89
|
+
export interface AdminWriteResponseV0 {
|
|
90
|
+
protocol: typeof ADMIN_PROTOCOL;
|
|
91
|
+
table: string;
|
|
92
|
+
/** The row after the write, encoded; `null` for a delete. */
|
|
93
|
+
row: EncodedValue | null;
|
|
94
|
+
/**
|
|
95
|
+
* Whether this call was the one that created the row. `false` on a retry of the same id, which is
|
|
96
|
+
* what makes `POST` safe to repeat.
|
|
97
|
+
*/
|
|
98
|
+
created?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A page cursor. Opaque to clients by contract, and deliberately not signed: it names a position in
|
|
102
|
+
* the caller's own data on a surface they already had to authenticate to reach, so there is nothing
|
|
103
|
+
* for a forged one to disclose that the next page would not have shown anyway.
|
|
104
|
+
*/
|
|
105
|
+
export declare function encodeAdminCursor(createdAt: string, id: string): string;
|
|
106
|
+
/** The inverse, or `null` when the text is not a cursor this version wrote. */
|
|
107
|
+
export declare function decodeAdminCursor(cursor: string): {
|
|
108
|
+
createdAt: string;
|
|
109
|
+
id: string;
|
|
110
|
+
} | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The admin data protocol: enough of an application's own database, over HTTP, for someone to browse
|
|
3
|
+
* and edit their rows without the tool doing it being this repository's admin UI.
|
|
4
|
+
*
|
|
5
|
+
* It is versioned like `xeer.dev.v0` and `xeer.state-export.v0` because that is the point — the
|
|
6
|
+
* shapes here are a contract a third party can build against, not the private wire format of one
|
|
7
|
+
* client. Everything a UI needs to render a table is in `/tables`: field types, constraints, which
|
|
8
|
+
* columns the runtime owns, and which field a table policy scopes rows by.
|
|
9
|
+
*
|
|
10
|
+
* Two decisions are worth stating because they are the ones that would otherwise be reinvented
|
|
11
|
+
* badly.
|
|
12
|
+
*
|
|
13
|
+
* **Paging is keyset, not offset.** A cursor names the last row seen — `(created_at, id)`, which is
|
|
14
|
+
* a total order because `id` is unique — so a page is a range scan rather than a count-and-skip, and
|
|
15
|
+
* inserting a row while someone pages does not shift every later page by one. The cursor is opaque
|
|
16
|
+
* on purpose: it is base64url text and a client should round-trip it, not parse it.
|
|
17
|
+
*
|
|
18
|
+
* **The client supplies the row id on create.** That is the whole idempotency story: the insert is
|
|
19
|
+
* `INSERT OR IGNORE`, so a retried request writes the same id and changes nothing, and the response
|
|
20
|
+
* says whether this call was the one that created it. No request ledger, no dedupe window.
|
|
21
|
+
*/
|
|
22
|
+
export const ADMIN_PROTOCOL = 'xeer.admin.v0';
|
|
23
|
+
/** Columns the runtime owns on every table. Present in every row, never writable by a client. */
|
|
24
|
+
export const ADMIN_RUNTIME_FIELDS = Object.freeze([
|
|
25
|
+
Object.freeze({ name: 'id', type: 'string', optional: false, readOnly: true }),
|
|
26
|
+
Object.freeze({ name: 'createdAt', type: 'datetime', optional: false, readOnly: true }),
|
|
27
|
+
Object.freeze({ name: 'updatedAt', type: 'datetime', optional: false, readOnly: true }),
|
|
28
|
+
]);
|
|
29
|
+
/** The default and the ceiling for one page. The ceiling bounds the response, not the table. */
|
|
30
|
+
export const ADMIN_PAGE_DEFAULT = 50;
|
|
31
|
+
export const ADMIN_PAGE_MAX = 200;
|
|
32
|
+
/**
|
|
33
|
+
* A page cursor. Opaque to clients by contract, and deliberately not signed: it names a position in
|
|
34
|
+
* the caller's own data on a surface they already had to authenticate to reach, so there is nothing
|
|
35
|
+
* for a forged one to disclose that the next page would not have shown anyway.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeAdminCursor(createdAt, id) {
|
|
38
|
+
const text = JSON.stringify([createdAt, id]);
|
|
39
|
+
return btoa(String.fromCharCode(...new TextEncoder().encode(text)))
|
|
40
|
+
.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
|
|
41
|
+
}
|
|
42
|
+
/** The inverse, or `null` when the text is not a cursor this version wrote. */
|
|
43
|
+
export function decodeAdminCursor(cursor) {
|
|
44
|
+
try {
|
|
45
|
+
const padded = cursor.replaceAll('-', '+').replaceAll('_', '/');
|
|
46
|
+
const bytes = Uint8Array.from(atob(padded + '='.repeat((4 - (padded.length % 4)) % 4)), (c) => c.charCodeAt(0));
|
|
47
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
48
|
+
if (!Array.isArray(parsed) || parsed.length !== 2)
|
|
49
|
+
return null;
|
|
50
|
+
const [createdAt, id] = parsed;
|
|
51
|
+
if (typeof createdAt !== 'string' || typeof id !== 'string')
|
|
52
|
+
return null;
|
|
53
|
+
return { createdAt, id };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
function canonicalValue(value) {
|
|
3
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
4
|
+
return value;
|
|
5
|
+
if (typeof value === 'number') {
|
|
6
|
+
if (!Number.isFinite(value))
|
|
7
|
+
throw new TypeError('Canonical JSON cannot encode a non-finite number');
|
|
8
|
+
return Object.is(value, -0) ? 0 : value;
|
|
9
|
+
}
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.map(canonicalValue);
|
|
12
|
+
if (typeof value === 'object') {
|
|
13
|
+
const prototype = Object.getPrototypeOf(value);
|
|
14
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
15
|
+
throw new TypeError('Canonical JSON only accepts plain objects');
|
|
16
|
+
}
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const key of Object.keys(value).sort()) {
|
|
19
|
+
const item = value[key];
|
|
20
|
+
if (item === undefined)
|
|
21
|
+
throw new TypeError(`Canonical JSON cannot encode undefined at ${key}`);
|
|
22
|
+
result[key] = canonicalValue(item);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
throw new TypeError(`Canonical JSON cannot encode ${typeof value}`);
|
|
27
|
+
}
|
|
28
|
+
export function canonicalJson(value) {
|
|
29
|
+
return JSON.stringify(canonicalValue(value));
|
|
30
|
+
}
|
|
31
|
+
export function sha256(bytes) {
|
|
32
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
33
|
+
}
|
|
34
|
+
export function canonicalHash(value) {
|
|
35
|
+
return sha256(canonicalJson(value));
|
|
36
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diagnostic catalogue.
|
|
3
|
+
*
|
|
4
|
+
* Xeer's primary user is a coding agent, so a diagnostic code is a stable API:
|
|
5
|
+
* an agent dispatches on `code`, not on wording. This module is the single
|
|
6
|
+
* declaration of what each code means and which edit closes it, and the agent
|
|
7
|
+
* reference shipped with the skill is rendered from it by
|
|
8
|
+
* `renderDiagnosticsReference()` rather than written by hand.
|
|
9
|
+
*
|
|
10
|
+
* `packages/spec/src/diagnostics.test.ts` scans every non-test source file in
|
|
11
|
+
* the workspace for `XE####` literals and fails when the emitted set and this
|
|
12
|
+
* catalogue disagree in either direction, so a new code cannot ship
|
|
13
|
+
* undocumented and a retired code cannot linger here.
|
|
14
|
+
*/
|
|
15
|
+
/** Where an agent observes a code, in the vocabulary of the CLI commands. */
|
|
16
|
+
export type DiagnosticSurface = 'any' | 'check' | 'build' | 'dev' | 'preview' | 'test' | 'new' | 'agent' | 'doctor' | 'inspect' | 'state' | 'auth' | 'deploy' | 'deployments' | 'promote' | 'rollback' | 'disable' | 'enable' | 'delete' | 'link' | 'env' | 'token' | 'domains' | 'export' | 'import' | 'db';
|
|
17
|
+
export interface DiagnosticFamily {
|
|
18
|
+
/** Numeric prefix the family owns, as it appears in a code. */
|
|
19
|
+
readonly prefix: string;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
/** What the whole family is about, for an agent triaging an unknown code. */
|
|
22
|
+
readonly summary: string;
|
|
23
|
+
}
|
|
24
|
+
export interface DiagnosticDefinition {
|
|
25
|
+
readonly code: string;
|
|
26
|
+
readonly prefix: string;
|
|
27
|
+
/** One line: what the platform observed. */
|
|
28
|
+
readonly means: string;
|
|
29
|
+
/** One line: the edit or action that closes it. */
|
|
30
|
+
readonly repair: string;
|
|
31
|
+
/** Commands whose JSON output can carry this code. */
|
|
32
|
+
readonly surfaces: readonly DiagnosticSurface[];
|
|
33
|
+
}
|
|
34
|
+
export declare const DIAGNOSTIC_FAMILIES: readonly DiagnosticFamily[];
|
|
35
|
+
export declare const DIAGNOSTIC_DEFINITIONS: readonly DiagnosticDefinition[];
|
|
36
|
+
export declare const DIAGNOSTICS: Readonly<Record<string, DiagnosticDefinition>>;
|
|
37
|
+
/** Every documented code, ascending. */
|
|
38
|
+
export declare const DIAGNOSTIC_CODES: readonly string[];
|
|
39
|
+
export declare function diagnosticDefinition(code: string): DiagnosticDefinition | undefined;
|
|
40
|
+
/** The family a code belongs to, matched on its numeric prefix. */
|
|
41
|
+
export declare function diagnosticFamily(code: string): DiagnosticFamily | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Renders the agent-facing diagnostics reference. The checked-in copy under
|
|
44
|
+
* `.agents/skills/xeer/references/` is this function's canonical output; a test fails when
|
|
45
|
+
* the two diverge, so the reference cannot drift from the catalogue.
|
|
46
|
+
*/
|
|
47
|
+
export declare function renderDiagnosticsReference(): string;
|
|
48
|
+
/** Markdown used by the public docs page and the installed offline docs bundle. */
|
|
49
|
+
export declare function renderDiagnosticsDocsPage(): string;
|