@volter/twin-planetscale 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +473 -0
  3. package/api/src/fetch.ts +50 -0
  4. package/api/src/generated/surface.gen.json +1 -0
  5. package/api/src/generated/ui.gen.json +1 -0
  6. package/api/src/index.ts +19 -0
  7. package/api/src/manifest.ts +136 -0
  8. package/api/src/screens/deploy-request.tsx +111 -0
  9. package/api/src/screens/service-tokens.tsx +141 -0
  10. package/api/src/screens/session.tsx +117 -0
  11. package/api/src/semantics/audit.ts +82 -0
  12. package/api/src/semantics/backups.ts +258 -0
  13. package/api/src/semantics/branches.ts +201 -0
  14. package/api/src/semantics/deploy-requests.ts +493 -0
  15. package/api/src/semantics/index.ts +371 -0
  16. package/api/src/semantics/shared.ts +141 -0
  17. package/api/src/semantics/time.ts +77 -0
  18. package/api/src/token-gate.ts +96 -0
  19. package/dist/api/src/fetch.d.ts +8 -0
  20. package/dist/api/src/fetch.js +51 -0
  21. package/dist/api/src/fetch.ts +50 -0
  22. package/dist/api/src/generated/surface.gen.json +1 -0
  23. package/dist/api/src/generated/ui.gen.json +1 -0
  24. package/dist/api/src/index.ts +19 -0
  25. package/dist/api/src/manifest.d.ts +2 -0
  26. package/dist/api/src/manifest.js +113 -0
  27. package/dist/api/src/manifest.ts +136 -0
  28. package/dist/api/src/screens/deploy-request.d.ts +7 -0
  29. package/dist/api/src/screens/deploy-request.js +106 -0
  30. package/dist/api/src/screens/deploy-request.tsx +111 -0
  31. package/dist/api/src/screens/service-tokens.d.ts +3 -0
  32. package/dist/api/src/screens/service-tokens.js +134 -0
  33. package/dist/api/src/screens/service-tokens.tsx +141 -0
  34. package/dist/api/src/screens/session.d.ts +11 -0
  35. package/dist/api/src/screens/session.js +108 -0
  36. package/dist/api/src/screens/session.tsx +117 -0
  37. package/dist/api/src/semantics/audit.d.ts +31 -0
  38. package/dist/api/src/semantics/audit.js +80 -0
  39. package/dist/api/src/semantics/audit.ts +82 -0
  40. package/dist/api/src/semantics/backups.d.ts +37 -0
  41. package/dist/api/src/semantics/backups.js +264 -0
  42. package/dist/api/src/semantics/backups.ts +258 -0
  43. package/dist/api/src/semantics/branches.d.ts +53 -0
  44. package/dist/api/src/semantics/branches.js +197 -0
  45. package/dist/api/src/semantics/branches.ts +201 -0
  46. package/dist/api/src/semantics/deploy-requests.d.ts +47 -0
  47. package/dist/api/src/semantics/deploy-requests.js +491 -0
  48. package/dist/api/src/semantics/deploy-requests.ts +493 -0
  49. package/dist/api/src/semantics/index.d.ts +20 -0
  50. package/dist/api/src/semantics/index.js +381 -0
  51. package/dist/api/src/semantics/index.ts +371 -0
  52. package/dist/api/src/semantics/shared.d.ts +36 -0
  53. package/dist/api/src/semantics/shared.js +132 -0
  54. package/dist/api/src/semantics/shared.ts +141 -0
  55. package/dist/api/src/semantics/time.d.ts +2 -0
  56. package/dist/api/src/semantics/time.js +81 -0
  57. package/dist/api/src/semantics/time.ts +77 -0
  58. package/dist/api/src/token-gate.d.ts +9 -0
  59. package/dist/api/src/token-gate.js +97 -0
  60. package/dist/api/src/token-gate.ts +96 -0
  61. package/dist/src/cli.d.ts +2 -0
  62. package/dist/src/cli.js +61 -0
  63. package/dist/src/generated/surface.gen.json +1 -0
  64. package/dist/src/generated/ui.gen.json +1 -0
  65. package/dist/src/index.d.ts +26 -0
  66. package/dist/src/index.js +156 -0
  67. package/dist/src/manifest.d.ts +2 -0
  68. package/dist/src/manifest.js +41 -0
  69. package/dist/src/planetscale-budget.d.ts +78 -0
  70. package/dist/src/planetscale-budget.js +305 -0
  71. package/dist/src/planetscale-capabilities.d.ts +10 -0
  72. package/dist/src/planetscale-capabilities.js +3977 -0
  73. package/dist/src/planetscale-collation-weights.gen.d.ts +4 -0
  74. package/dist/src/planetscale-collation-weights.gen.js +12 -0
  75. package/dist/src/planetscale-collation.d.ts +70 -0
  76. package/dist/src/planetscale-collation.js +391 -0
  77. package/dist/src/planetscale-conformance.d.ts +8 -0
  78. package/dist/src/planetscale-conformance.js +213 -0
  79. package/dist/src/planetscale-connector.d.ts +150 -0
  80. package/dist/src/planetscale-connector.js +532 -0
  81. package/dist/src/planetscale-deploy.d.ts +26 -0
  82. package/dist/src/planetscale-deploy.js +235 -0
  83. package/dist/src/planetscale-information-schema.d.ts +32 -0
  84. package/dist/src/planetscale-information-schema.js +299 -0
  85. package/dist/src/planetscale-mysql.d.ts +33 -0
  86. package/dist/src/planetscale-mysql.js +547 -0
  87. package/dist/src/planetscale-roles.d.ts +11 -0
  88. package/dist/src/planetscale-roles.js +60 -0
  89. package/dist/src/planetscale-row.d.ts +12 -0
  90. package/dist/src/planetscale-row.js +39 -0
  91. package/dist/src/planetscale-server.d.ts +42 -0
  92. package/dist/src/planetscale-server.js +137 -0
  93. package/dist/src/planetscale-sql.d.ts +701 -0
  94. package/dist/src/planetscale-sql.js +7167 -0
  95. package/dist/src/planetscale-store.d.ts +126 -0
  96. package/dist/src/planetscale-store.js +827 -0
  97. package/dist/src/planetscale-twin.d.ts +48 -0
  98. package/dist/src/planetscale-twin.js +290 -0
  99. package/dist/src/planetscale-values.d.ts +139 -0
  100. package/dist/src/planetscale-values.js +719 -0
  101. package/dist/src/planetscale-wire.d.ts +110 -0
  102. package/dist/src/planetscale-wire.js +188 -0
  103. package/dist/src/semantics/psdb.d.ts +18 -0
  104. package/dist/src/semantics/psdb.js +30 -0
  105. package/package.json +58 -0
  106. package/src/cli.ts +58 -0
  107. package/src/generated/surface.gen.json +1 -0
  108. package/src/generated/ui.gen.json +1 -0
  109. package/src/index.ts +267 -0
  110. package/src/manifest.ts +60 -0
  111. package/src/planetscale-budget.ts +347 -0
  112. package/src/planetscale-capabilities.ts +3862 -0
  113. package/src/planetscale-collation-weights.gen.ts +13 -0
  114. package/src/planetscale-collation.ts +378 -0
  115. package/src/planetscale-conformance.ts +237 -0
  116. package/src/planetscale-connector.ts +571 -0
  117. package/src/planetscale-deploy.ts +197 -0
  118. package/src/planetscale-information-schema.ts +322 -0
  119. package/src/planetscale-mysql.ts +339 -0
  120. package/src/planetscale-roles.ts +71 -0
  121. package/src/planetscale-row.ts +43 -0
  122. package/src/planetscale-server.ts +162 -0
  123. package/src/planetscale-sql.ts +5957 -0
  124. package/src/planetscale-store.ts +869 -0
  125. package/src/planetscale-twin.ts +338 -0
  126. package/src/planetscale-values.ts +572 -0
  127. package/src/planetscale-wire.ts +274 -0
  128. package/src/semantics/psdb.ts +57 -0
@@ -0,0 +1,237 @@
1
+ // planetscale conformance (dev-only; lazy-imported by the CLI, NEVER from index.ts/runtime — E2).
2
+ //
3
+ // This is NOT a self-referential snapshot check. Two constants asserting about each other cannot
4
+ // detect a dead handler (§9 has refuted that pattern twice in this repo), so this check ISSUES REAL
5
+ // REQUESTS against a throwaway root and grades the OUTCOME a live handler produces — a status set
6
+ // plus a predicate over the body. A twin whose handler returned `{}` fails every probe.
7
+ //
8
+ // FOUR directions are closed, not two:
9
+ // 1. every PROBE has a matching entry in `implementedEndpoints` (no probe for unclaimed surface);
10
+ // 2. every claimed endpoint has a PROBE (no claim nothing exercises);
11
+ // 3. `ROUTER_SURFACE` — a HAND-WRITTEN census of the method/path pairs the router must REFUSE —
12
+ // is exercised, and any pair answering something other than the transport-level refusal
13
+ // envelope is a violation. Probe⇄snapshot alone is blind to SERVED-BUT-UNCLAIMED surface;
14
+ // this is what catches an endpoint that quietly exists.
15
+ // 3b. `MALFORMED_SURFACE` — the branches where dispatch reaches Execute but the request never
16
+ // becomes a query. They must answer `invalid_argument`, never a query error and never a result.
17
+ // 4. auth: a credential-less Execute must answer the vendor's 401 envelope. A twin that
18
+ // authenticated everything would pass every probe above while accepting an anonymous client.
19
+ //
20
+ // §9 round one rewrote direction 3 twice over: its census used to be BUILT FROM `PSDB_METHODS` (the
21
+ // router's own method gate) behind a comment claiming it was hand-enumerated, and its "claimed"
22
+ // half could not fire for any input. Both are documented at `ROUTER_SURFACE`.
23
+ import { mkdtempSync, rmSync } from 'node:fs';
24
+ import { tmpdir } from 'node:os';
25
+ import { join } from 'node:path';
26
+ import { twinResources } from '@volter/world-core';
27
+ import { createPlanetscaleTwinFetch } from './planetscale-server.ts';
28
+ import { SERVICE } from './planetscale-store.ts';
29
+ import {
30
+ handlePlanetscaleTwinRequest,
31
+ planetscaleTwinSnapshot,
32
+ PSDB_SERVICE,
33
+ PLANETSCALE_RESOURCE_TYPES,
34
+ } from './planetscale-twin.ts';
35
+ import { unpackRow, type WireExecuteResponse } from './planetscale-wire.ts';
36
+
37
+ export type PlanetscaleConformanceReport = {
38
+ ok: boolean;
39
+ endpointsChecked: number;
40
+ routerSurfaceChecked: number;
41
+ resourceTypesChecked: number;
42
+ violations: string[];
43
+ };
44
+
45
+ const AT = '2026-01-01T00:00:00.000Z';
46
+ /** `Basic base64('twin:conformance')` — a credential the twin accepts because it is well-formed. */
47
+ const AUTH = { authorization: `Basic ${btoa('twin:conformance')}` };
48
+
49
+ type Probe = { label: string; method: string; path: string; body?: string; headers?: Record<string, string>; expect: (r: { status: number; body: unknown }) => boolean };
50
+
51
+ const exec = (query: string, session?: unknown): string => JSON.stringify({ query, ...(session !== undefined ? { session } : {}) });
52
+
53
+ /**
54
+ * The SERVED-BUT-UNCLAIMED census — every method/path pair the router must ANSWER WITH A REFUSAL.
55
+ *
56
+ * Probe⇄snapshot (directions 1 and 2) is blind in exactly one direction: surface the router quietly
57
+ * serves that no snapshot entry claims. This closes it. Anything here answering something other
58
+ * than the transport-level refusal envelope is a violation.
59
+ *
60
+ * HAND-WRITTEN LITERALS, deliberately. §9 round one, finding 8: the first version built five of its
61
+ * nine entries with `...PSDB_METHODS.map(...)` behind a comment insisting it was hand-enumerated.
62
+ * `PSDB_METHODS` IS the router's own method gate, so those five drifted in lockstep with the code
63
+ * they audit — adding a name to that array grew the census and produced no violation. Writing the
64
+ * paths out is what makes this an INDEPENDENT census; §9 still compares it against the real
65
+ * dispatch branches by reading.
66
+ *
67
+ * The CLAIMED half was deleted with it (finding 9): it could not fire. `Execute` with a `{}` body
68
+ * answers 400, which is not a refusal status, so the only entries the condition could reach were
69
+ * the two `unimplemented` ones it explicitly excluded. The PROBES above already assert each claimed
70
+ * endpoint's real OUTCOME, which is strictly stronger than "did not refuse".
71
+ */
72
+ const ROUTER_SURFACE: Array<{ method: string; path: string; body?: string }> = [
73
+ // Wrong verb: Connect unary RPCs are POST-only, and the refusal precedes authentication.
74
+ { method: 'GET', path: '/psdb.v1alpha1.Database/Execute' },
75
+ { method: 'HEAD', path: '/psdb.v1alpha1.Database/Execute' },
76
+ { method: 'PUT', path: '/psdb.v1alpha1.Database/CreateSession' },
77
+ { method: 'DELETE', path: '/psdb.v1alpha1.Database/CloseSession' },
78
+ // A method the service does not declare.
79
+ { method: 'POST', path: '/psdb.v1alpha1.Database/NoSuchMethod', body: '{}' },
80
+ { method: 'POST', path: '/psdb.v1alpha1.Database/DropDatabase', body: '{}' },
81
+ // A different service, and paths shaped like OTHER vendors' that must not be served here.
82
+ { method: 'POST', path: '/psdb.v2.Database/Execute', body: '{}' },
83
+ { method: 'POST', path: '/v1/query', body: '{}' },
84
+ { method: 'POST', path: '/query', body: '{}' },
85
+ { method: 'POST', path: '/', body: '{}' },
86
+ { method: 'GET', path: '/' },
87
+ ];
88
+
89
+ /** The malformed-input branches — see direction 3b below. */
90
+ const MALFORMED_SURFACE: Array<{ label: string; body: string }> = [
91
+ { label: 'a body that is not JSON', body: '{not json' },
92
+ { label: 'a JSON array body', body: '[1,2,3]' },
93
+ { label: 'a non-string query', body: JSON.stringify({ query: 42 }) },
94
+ { label: 'a missing query', body: JSON.stringify({ session: null }) },
95
+ ];
96
+
97
+ export async function checkPlanetscaleConformance(): Promise<PlanetscaleConformanceReport> {
98
+ const snapshot = planetscaleTwinSnapshot();
99
+ const violations: string[] = [];
100
+ const root = mkdtempSync(join(tmpdir(), 'planetscale-conf-'));
101
+ const call = (method: string, path: string, body?: string, headers: Record<string, string> = AUTH) =>
102
+ handlePlanetscaleTwinRequest({ method, path, headers, root, occurredAt: AT, ...(body !== undefined ? { body } : {}) });
103
+
104
+ const probes: Probe[] = [
105
+ {
106
+ label: 'POST /psdb.v1alpha1.Database/CreateSession',
107
+ method: 'POST', path: `/${PSDB_SERVICE}/CreateSession`, body: '{}',
108
+ expect: (r) => {
109
+ const b = r.body as { branch?: string; user?: { username?: string }; session?: { vitessSession?: { sessionUUID?: string } } };
110
+ return r.status === 200 && b.branch === 'main' && b.user?.username === 'twin'
111
+ && typeof b.session?.vitessSession?.sessionUUID === 'string' && b.session.vitessSession.sessionUUID !== '';
112
+ },
113
+ },
114
+ {
115
+ label: 'POST /psdb.v1alpha1.Database/Execute',
116
+ method: 'POST', path: `/${PSDB_SERVICE}/Execute`, body: exec('SELECT 1 AS one FROM dual'),
117
+ expect: (r) => {
118
+ const b = r.body as WireExecuteResponse;
119
+ if (r.status !== 200 || b.error !== undefined || b.result === undefined) return false;
120
+ // Grade the DECODED VALUE, not the shape: a handler that returned an empty result set, or
121
+ // packed the row wrongly, fails here.
122
+ const fields = b.result.fields ?? [];
123
+ const rows = b.result.rows ?? [];
124
+ return fields.length === 1 && fields[0]!.name === 'one' && rows.length === 1
125
+ && unpackRow(rows[0]!)[0] === '1';
126
+ },
127
+ },
128
+ {
129
+ label: 'POST /psdb.v1alpha1.Database/CloseSession',
130
+ method: 'POST', path: `/${PSDB_SERVICE}/CloseSession`,
131
+ body: JSON.stringify({ session: { vitessSession: { sessionUUID: 'tws-1-0' } } }),
132
+ expect: (r) => {
133
+ const b = r.body as { session?: { vitessSession?: { sessionUUID?: string } } };
134
+ return r.status === 200 && b.session?.vitessSession?.sessionUUID === 'tws-1-0';
135
+ },
136
+ },
137
+ {
138
+ label: 'POST /psdb.v1alpha1.Database/Prepare (vendor-faithful unimplemented)',
139
+ method: 'POST', path: `/${PSDB_SERVICE}/Prepare`, body: '{}',
140
+ expect: (r) => r.status === 501 && (r.body as { code?: string }).code === 'unimplemented',
141
+ },
142
+ {
143
+ label: 'POST /psdb.v1alpha1.Database/StreamExecute (the gap: Connect unimplemented)',
144
+ method: 'POST', path: `/${PSDB_SERVICE}/StreamExecute`, body: exec('SELECT 1'),
145
+ // this probe calls the request handler, which answers 501; through the pack's fetch it is the gap (psdbGap), 404
146
+ expect: (r) => r.status === 501 && (r.body as { code?: string }).code === 'unimplemented',
147
+ },
148
+ ];
149
+
150
+ try {
151
+ for (const probe of probes) {
152
+ if (!snapshot.implementedEndpoints.includes(probe.label)) {
153
+ violations.push(`probe '${probe.label}' has no matching entry in implementedEndpoints`);
154
+ continue;
155
+ }
156
+ const response = await call(probe.method, probe.path, probe.body, probe.headers ?? AUTH);
157
+ if (!probe.expect(response)) {
158
+ violations.push(`endpoint '${probe.label}' did not answer as declared (status ${response.status}, body ${JSON.stringify(response.body)})`);
159
+ }
160
+ }
161
+ for (const endpoint of snapshot.implementedEndpoints) {
162
+ if (!probes.some((p) => p.label === endpoint)) violations.push(`declared endpoint '${endpoint}' has no conformance probe`);
163
+ }
164
+
165
+ // Direction 3 — served-but-unclaimed surface. Every entry must answer the TRANSPORT-level
166
+ // refusal envelope (a Connect `{code, message}` at 404/405/501), never a query envelope and
167
+ // never a result.
168
+ for (const entry of ROUTER_SURFACE) {
169
+ const response = await call(entry.method, entry.path, entry.body);
170
+ const body = response.body as { code?: string; error?: unknown; result?: unknown; session?: unknown };
171
+ const refused = (response.status === 404 || response.status === 405 || response.status === 501)
172
+ && typeof body.code === 'string' && body.error === undefined && body.result === undefined && body.session === undefined;
173
+ if (!refused) {
174
+ violations.push(`router surface '${entry.method} ${entry.path}' must answer the transport refusal envelope, got ${response.status} ${JSON.stringify(response.body)} — served-but-unclaimed surface`);
175
+ }
176
+ }
177
+
178
+ // Direction 3b — the MALFORMED-INPUT branches. Dispatch reaches Execute, but the request never
179
+ // becomes a query: it must answer `invalid_argument`, never a query error and never a result.
180
+ for (const entry of MALFORMED_SURFACE) {
181
+ const response = await call('POST', `/${PSDB_SERVICE}/Execute`, entry.body);
182
+ const body = response.body as { code?: string; error?: unknown; result?: unknown };
183
+ if (response.status !== 400 || body.code !== 'invalid_argument' || body.error !== undefined || body.result !== undefined) {
184
+ violations.push(`${entry.label} must answer 400 {code:'invalid_argument'}, got ${response.status} ${JSON.stringify(response.body)}`);
185
+ }
186
+ }
187
+
188
+ // Auth is a branch too, and it must refuse — a twin that authenticated everything would pass
189
+ // every probe above while accepting a credential-less client.
190
+ const unauth = await call('POST', `/${PSDB_SERVICE}/Execute`, exec('SELECT 1'), {});
191
+ if (unauth.status !== 401 || (unauth.body as { error?: { code?: string } }).error?.code !== 'unauthenticated') {
192
+ violations.push(`a credential-less Execute must answer 401 {error:{code:'unauthenticated'}}, got ${unauth.status} ${JSON.stringify(unauth.body)}`);
193
+ }
194
+
195
+ // …and each declared type must be genuinely REACHABLE, not merely declared: DDL writes a table,
196
+ // DML writes a row, and the round-trip must be readable back through the same handler.
197
+ await call('POST', `/${PSDB_SERVICE}/Execute`, exec('CREATE TABLE conf (id BIGINT PRIMARY KEY AUTO_INCREMENT, label VARCHAR(64) NOT NULL)'));
198
+ await call('POST', `/${PSDB_SERVICE}/Execute`, exec("INSERT INTO conf (label) VALUES ('alpha')"));
199
+ const read = await call('POST', `/${PSDB_SERVICE}/Execute`, exec('SELECT id, label FROM conf'));
200
+ const result = (read.body as WireExecuteResponse).result;
201
+ const values = result?.rows?.[0] === undefined ? [] : unpackRow(result.rows[0]!);
202
+ if (values[0] !== '1' || values[1] !== 'alpha') {
203
+ violations.push(`the 'table'/'row' resource types are declared but a DDL+DML round trip did not project (got ${JSON.stringify(read.body)})`);
204
+ }
205
+ const sessions = await call('POST', `/${PSDB_SERVICE}/CreateSession`, '{}');
206
+ if (typeof (sessions.body as { session?: { vitessSession?: { sessionUUID?: string } } }).session?.vitessSession?.sessionUUID !== 'string') {
207
+ violations.push(`the 'session' resource type is declared but CreateSession did not mint one (${JSON.stringify(sessions.body)})`);
208
+ }
209
+ // a branch's backup, through the management API (the `api` lane, api/src/)
210
+ const backup = await createPlanetscaleTwinFetch({ root })(new Request('http://api.planetscale.com/v1/organizations/o/databases/d/branches/main/backups', {
211
+ method: 'POST', headers: { authorization: 'id:token', 'content-type': 'application/json' }, body: JSON.stringify({ name: 'conf' }),
212
+ }));
213
+ if (backup.status !== 201) violations.push(`the '_backup' resource type is declared but creating a backup answered ${backup.status}`);
214
+
215
+ // The inventory must match what the handlers WRITE, in BOTH directions — a missing type is a violation as much
216
+ // as an extra one. What was written is read back from the root the paths above wrote to, never listed by hand.
217
+ const written = [...new Set(twinResources(SERVICE, root).map((r) => r.type))];
218
+ for (const type of PLANETSCALE_RESOURCE_TYPES) {
219
+ if (!written.includes(type)) violations.push(`declared resource type '${type}' is not written by any handler path`);
220
+ }
221
+ for (const type of written) {
222
+ if (!(PLANETSCALE_RESOURCE_TYPES as readonly string[]).includes(type)) {
223
+ violations.push(`the handler writes subject type '${type}' but PLANETSCALE_RESOURCE_TYPES does not declare it`);
224
+ }
225
+ }
226
+ } finally {
227
+ rmSync(root, { recursive: true, force: true });
228
+ }
229
+
230
+ return {
231
+ ok: violations.length === 0,
232
+ endpointsChecked: snapshot.implementedEndpoints.length,
233
+ routerSurfaceChecked: ROUTER_SURFACE.length + MALFORMED_SURFACE.length,
234
+ resourceTypesChecked: snapshot.resourceTypes.length,
235
+ violations,
236
+ };
237
+ }