@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,26 @@
1
+ export { handlePlanetscaleTwinRequest, planetscaleTwinSnapshot, extractBasicCredential, PSDB_SERVICE, PSDB_METHODS, PLANETSCALE_UNAUTHENTICATED, PLANETSCALE_RESOURCE_TYPES, } from './planetscale-twin.js';
2
+ export type { PlanetscaleRequest, PlanetscaleResponse, PlanetscaleTwinSnapshot } from './planetscale-twin.js';
3
+ export { runSql, execStatement, parseStatement, tokenize, evalExpr, likeMatch, emptyDatabase, findTable, tableRows, tableKey, nextRowId, vitessTypeFor, fieldForColumn, isNumericVitessType, unsupported, parseError, SqlError, PLANETSCALE_LIMITS, VITESS_TYPES, BINARY_CHARSET, UTF8MB4_CHARSET, FLAG_NOT_NULL, FLAG_PRI_KEY, FLAG_UNIQUE_KEY, FLAG_UNSIGNED, FLAG_BINARY, FLAG_AUTO_INCREMENT, } from './planetscale-sql.js';
4
+ export type { Cell, ColumnDef, Database, Expr, Field, QueryOutcome, RowRec, Statement, TableDef, VitessType, Write } from './planetscale-sql.js';
5
+ export { packRow, unpackRow, packBytes, encodeCell, encodeField, encodeQueryResult, makeSession, sessionIdOf, } from './planetscale-wire.js';
6
+ export type { WireField, WireRow, WireQueryResult, WireError, WireSession, WireExecuteResponse, WireCreateSessionResponse } from './planetscale-wire.js';
7
+ export { SERVICE, DEFAULT_BRANCH, DEFAULT_DATABASE, loadState, readDatabase, executeSql, createSession, closeSession, mintSessionId, applyWritesToImage, } from './planetscale-store.js';
8
+ export type { ExecuteContext, ExecuteResult, SessionState, PlanetscaleResourceType } from './planetscale-store.js';
9
+ export { createPlanetscaleTwinFetch, createPlanetscaleTwinServer } from './planetscale-server.js';
10
+ export { createPlanetscaleMysqlStream, createPlanetscaleTwinStream } from './planetscale-mysql.js';
11
+ export type { PlanetscaleServerOptions, PlanetscaleTwinFetchOptions } from './planetscale-server.js';
12
+ export { mapTable, mapRow, mapDescribeRow, encodeCellValue, quoteSqlLiteral, quoteIdent, pollTimestamp, pullPlanetscaleTables, pullPlanetscaleDatabase, pullPlanetscaleSnapshot, type PlanetscalePullReport, type PlanetscaleRefreshReport, syncPlanetscaleFromReal, syncPlanetscaleFromRemote, performPlanetscaleAction, pushPlanetscaleAction, pushPlanetscaleActions, } from './planetscale-connector.js';
13
+ export type { PlanetscaleLikeClient, PlanetscaleRealTable, PlanetscaleExecutedQuery, PlanetscalePullOptions } from './planetscale-connector.js';
14
+ export { PLANETSCALE_BUDGETED_METHODS, PLANETSCALE_BUDGET_CEILING, PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S, PLANETSCALE_BUDGET_WINDOW_MS, PLANETSCALE_CALL_WEIGHTS, PLANETSCALE_RATE_BUDGET, PlanetscaleBudget, PlanetscaleBudgetError, planetscaleBudgetPath, planetscaleCallWeight, planetscaleClientBudget, guardPlanetscaleClient, } from './planetscale-budget.js';
15
+ export type { PlanetscaleBudgetErrorKind, PlanetscaleBudgetOptions, PlanetscaleBudgetReservation, PlanetscaleBudgetSnapshot, PlanetscaleBudgetedOptions, } from './planetscale-budget.js';
16
+ import { type TwinPack } from '@volter/world-core';
17
+ export declare const pack: TwinPack;
18
+ /**
19
+ * How a journey reads an answer of this pack (a step's `view`, scripts/behavior-journey.ts). `rows` decodes an Execute
20
+ * answer into what MySQL's client prints for it: `fields`, the result's column names in order; `rows`, each row as an
21
+ * array of its cells as text (MySQL's text protocol spelling: numbers as their digits, NULL as null), in the order the
22
+ * answer holds them; `rowsAffected` and `insertId` as numbers where psdb answers them (0 when it omits them, as its
23
+ * protojson does for a zero); and `error`, `{code, message}`, when the statement was refused. Cells are the packed bytes
24
+ * decoded as UTF-8.
25
+ */
26
+ export declare const answerViews: Record<string, (body: unknown) => unknown>;
@@ -0,0 +1,156 @@
1
+ // @volter/twin-planetscale — the PlanetScale psdb HTTP-API twin, built on the shared @volter/world-core
2
+ // kernel. PlanetScale's serverless driver does not speak MySQL's binary protocol: it speaks
3
+ // `psdb.v1alpha1.Database`, a Connect RPC service addressed with unary JSON POSTs
4
+ // (`/psdb.v1alpha1.Database/CreateSession`, `/Execute`, `/CloseSession`) under HTTP Basic auth,
5
+ // with results packed as protojson `QueryResult` — base64 row bytes plus per-column byte lengths,
6
+ // `-1` for NULL, and every zero-valued field OMITTED.
7
+ //
8
+ // Behind that protocol is a REAL DATABASE: a stateful MySQL-subset engine with schemas
9
+ // (CREATE/DROP/TRUNCATE TABLE, SHOW TABLES, DESCRIBE), DML (INSERT/SELECT/UPDATE/DELETE with WHERE,
10
+ // ORDER BY, LIMIT/OFFSET, COUNT(*), ON DUPLICATE KEY UPDATE), MySQL's own errno/sqlstate errors
11
+ // (1146 no-such-table, 1062 duplicate entry, 1048 not-null, 1064 parse error, 1054 unknown column),
12
+ // AUTO_INCREMENT that survives deletes, NULL three-valued logic, and REAL TRANSACTIONS — so the
13
+ // unmodified client's `conn.transaction(async (tx) => …)`, which is nothing but BEGIN/…/COMMIT over
14
+ // the same session, genuinely commits or rolls back.
15
+ //
16
+ // THE HONEST CARVE-OUTS: a twin cannot make a wall clock pass or run a storage engine. `NOW()`
17
+ // and the other clock functions read the World clock's instant for the statement;
18
+ // `RAND()` and `UUID()` are REFUSED rather than frozen to a constant
19
+ // (the serve path must be a pure function of (request, stored state)); real elapsed query `timing`
20
+ // is never emitted; real Vitess sharding/resharding and MySQL's storage-engine physics are todos.
21
+ // PlanetScale's branches are modelled: a password acts on its own branch, `main` is the World's one
22
+ // image and every other branch a scope of its own (planetscale-store.ts, "BRANCH SCOPES"), and the
23
+ // `api` lane deploys a branch's schema changes into its base (api/src/semantics/deploy-requests.ts).
24
+ // The twin's limit: every database's `main` shares the one image. See README ## Coverage.
25
+ //
26
+ // State lives ENTIRELY in the kernel action log (no side-store): every table and every row is one
27
+ // kernel subject, so a restart against the same root answers the same `SELECT`.
28
+ export { handlePlanetscaleTwinRequest, planetscaleTwinSnapshot, extractBasicCredential, PSDB_SERVICE, PSDB_METHODS, PLANETSCALE_UNAUTHENTICATED, PLANETSCALE_RESOURCE_TYPES, } from "./planetscale-twin.js";
29
+ // The SQL core — exported so a caller (or a reviewer) can drive MySQL semantics in-process,
30
+ // without HTTP. `runSql` over an image is the whole engine; nothing about the protocol is required
31
+ // to exercise it.
32
+ export { runSql, execStatement, parseStatement, tokenize, evalExpr, likeMatch, emptyDatabase, findTable, tableRows, tableKey, nextRowId, vitessTypeFor, fieldForColumn, isNumericVitessType, unsupported, parseError, SqlError, PLANETSCALE_LIMITS, VITESS_TYPES, BINARY_CHARSET, UTF8MB4_CHARSET, FLAG_NOT_NULL, FLAG_PRI_KEY, FLAG_UNIQUE_KEY, FLAG_UNSIGNED, FLAG_BINARY, FLAG_AUTO_INCREMENT, } from "./planetscale-sql.js";
33
+ // The wire codec. Exported because faithful row packing is a headline claim of this pack and a
34
+ // consumer (or a reviewer) must be able to decode a served response without the SDK.
35
+ export { packRow, unpackRow, packBytes, encodeCell, encodeField, encodeQueryResult, makeSession, sessionIdOf, } from "./planetscale-wire.js";
36
+ // The kernel binding — the projection and the session/transaction book.
37
+ export { SERVICE, DEFAULT_BRANCH, DEFAULT_DATABASE, loadState, readDatabase, executeSql, createSession, closeSession, mintSessionId, applyWritesToImage, } from "./planetscale-store.js";
38
+ export { createPlanetscaleTwinFetch, createPlanetscaleTwinServer } from "./planetscale-server.js";
39
+ export { createPlanetscaleMysqlStream, createPlanetscaleTwinStream } from "./planetscale-mysql.js";
40
+ export { mapTable, mapRow, mapDescribeRow, encodeCellValue, quoteSqlLiteral, quoteIdent, pollTimestamp, pullPlanetscaleTables, pullPlanetscaleDatabase, pullPlanetscaleSnapshot, syncPlanetscaleFromReal, syncPlanetscaleFromRemote, performPlanetscaleAction, pushPlanetscaleAction, pushPlanetscaleActions, } from "./planetscale-connector.js";
41
+ // The client-side rate budget — the fail-closed backstop every live call goes through. The
42
+ // MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives here is this vendor's
43
+ // DECLARATION plus `guardPlanetscaleClient`, the choke point the connector entrypoints apply
44
+ // unconditionally. There is deliberately no export that disables the guard.
45
+ export { PLANETSCALE_BUDGETED_METHODS, PLANETSCALE_BUDGET_CEILING, PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S, PLANETSCALE_BUDGET_WINDOW_MS, PLANETSCALE_CALL_WEIGHTS, PLANETSCALE_RATE_BUDGET, PlanetscaleBudget, PlanetscaleBudgetError, planetscaleBudgetPath, planetscaleCallWeight, planetscaleClientBudget, guardPlanetscaleClient, } from "./planetscale-budget.js";
46
+ // Registry descriptor: the pack self-describes so tooling can discover it.
47
+ import { registerPack } from '@volter/world-core';
48
+ import { performPlanetscaleAction, syncPlanetscaleFromRemote } from "./planetscale-connector.js";
49
+ import { PLANETSCALE_RATE_BUDGET as RATE_BUDGET } from "./planetscale-budget.js";
50
+ export const pack = {
51
+ protocol: '2',
52
+ stateSystem: { perform: performPlanetscaleAction, refresh: syncPlanetscaleFromRemote },
53
+ refresh: { onDemand: { atMost: '60s' } },
54
+ roundTrip: [
55
+ { method: 'POST', path: '/psdb.v1alpha1.Database/Execute', headers: { authorization: 'Basic dHdpbjp0d2lu' }, body: { query: 'CREATE TABLE IF NOT EXISTS round_trip (id BIGINT PRIMARY KEY AUTO_INCREMENT, body VARCHAR(255))' } },
56
+ { method: 'POST', path: '/psdb.v1alpha1.Database/Execute', headers: { authorization: 'Basic dHdpbjp0d2lu' }, body: { query: "INSERT INTO round_trip (body) VALUES ('round trip')" } },
57
+ ],
58
+ parityOrigin: 'http://twin',
59
+ // The SAME object planetscale-budget.ts declares at module load — one source of truth, so
60
+ // registering the pack and importing the connector can never arm two different ceilings.
61
+ rateBudget: RATE_BUDGET,
62
+ vendor: 'planetscale',
63
+ transport: 'rest',
64
+ nativeTransport: { protocol: 'mysql', flag: '--mysql', upstreamEnv: 'PLANETSCALE_TWIN_URL' },
65
+ endpointEnv: {
66
+ name: 'PLANETSCALE_TWIN_URL',
67
+ templates: { PLANETSCALE_DATABASE_URL: 'http://twin:twin@${host}:${port}' },
68
+ note: 'the @planetscale/database client reads PLANETSCALE_DATABASE_URL and honours http:; the injector also redirects *.psdb.cloud.',
69
+ },
70
+ // PRISMA — dub's `new PrismaClient({ omit })` names no adapter; on Prisma's client-engine
71
+ // build (the tab's, and the edge client's) that refuses to construct. The injector supplies
72
+ // this adapter, from the application's own `@prisma/adapter-planetscale`, over the endpoint
73
+ // template's PLANETSCALE_DATABASE_URL, which `@planetscale/database` honours as http:.
74
+ prismaAdapter: { adapter: '@prisma/adapter-planetscale', export: 'PrismaPlanetScale', urlEnv: 'PLANETSCALE_DATABASE_URL' },
75
+ archetype: 'crud',
76
+ bin: 'world-planetscale',
77
+ resources: ['table', 'row', '_session', '_backup'],
78
+ // ADOPTION — declared HERE, not in world-runtime's central SDK_TWINS / ENV_STEM_VENDORS tables
79
+ // (adding-a-twin.md §3; declaring a fact in both homes THROWS). `@planetscale/database` is the
80
+ // only official client of this API surface. `PLANETSCALE` is the credential-env stem: dub reads
81
+ // `PLANETSCALE_DATABASE_URL` (apps/web/lib/planetscale/connection.ts) — the exact var whose
82
+ // DATABASE_URL shape made `volter-world init` bin this vendor as generic infra and report the
83
+ // repo fully covered while a real call escaped (volter-ai/twin#255).
84
+ adoption: {
85
+ // No Python client for the psdb HTTP data API - `@planetscale/database` is JS-only, and Python
86
+ // callers reach PlanetScale over the MySQL wire protocol (a generic driver, not a vendor client).
87
+ pypi: [],
88
+ sdks: ['@planetscale/database'],
89
+ envStems: ['PLANETSCALE'],
90
+ },
91
+ // INTERCEPTION — the SDK addresses ONE host: whatever `config.host` resolves to from the
92
+ // connection URL (dist/index.js's constructor takes `url.hostname`, then `buildURL` rebuilds the
93
+ // request against it). PlanetScale's psdb data plane lives on per-region subdomains of
94
+ // `psdb.cloud` — `aws.connect.psdb.cloud`, `gcp.connect.psdb.cloud`, `<region>.connect.psdb.cloud`
95
+ // — so a SUFFIX matcher is required; an exact host would miss every region but one.
96
+ //
97
+ // `api.planetscale.com` is the MANAGEMENT API, the pack's `api` lane (api/src/, derived from PlanetScale's own
98
+ // Swagger document): the pack claims its /v1/organizations tree, where the lane serves databases, a branch's
99
+ // passwords and backups, and service tokens, and answers PlanetScale's 404 for every other operation; the host's
100
+ // other paths are unclaimed.
101
+ // app.planetscale.com's sign-in and service tokens page are screens the lane serves (api/src/screens/).
102
+ hosts: [{ suffix: '.psdb.cloud' }, { host: 'api.planetscale.com', pathPattern: '^/v1/organizations(/|$)' }, { host: 'app.planetscale.com', pathPattern: '^/(sign-in|[^/]+/settings/service-tokens)(/|$)' }],
103
+ // No endpoint env, and none INVENTED. `@planetscale/database` reads no environment variable of
104
+ // its own — `connect({url})` takes the URL from the application, which chooses the var name
105
+ // (dub's is PLANETSCALE_DATABASE_URL, Vercel's template uses DATABASE_URL). Interception here is
106
+ // the injector's, via the `hosts` suffix above, so an app-read endpoint var is not needed;
107
+ // emitting one anyway would let `covers` report coverage for an app that reads a different name.
108
+ pullPosture: 'on-demand',
109
+ pullPostureReason: 'PlanetScale meters ROWS READ rather than requests, so a scheduled full-table pull is unbounded in cost by '
110
+ + 'construction: one SELECT can read millions of billable rows. Pull it explicitly when someone asks, '
111
+ + 'through the guarded connector and its mandatory row limit.',
112
+ specSource: 'the installed @planetscale/database@1.20.1 package source (npm pack: dist/index.js, dist/cast.js, '
113
+ + 'dist/sanitization.js, dist/text.js, dist/index.d.ts) as the authoritative wire contract + its own test '
114
+ + 'fixtures for the error envelopes + github.com/mattrobenolt/ps-http-sim (the simulator upstream dub devs '
115
+ + 'run) and the psdb.v1alpha1 Connect service it implements + '
116
+ + 'planetscale.com/docs/reference/planetscale-system-limits for the published scalar limits; see '
117
+ + 'spec-sources.json.',
118
+ description: "PlanetScale psdb HTTP-API twin — a real stateful MySQL-subset database (DDL, INSERT/SELECT/UPDATE/DELETE, "
119
+ + 'WHERE/ORDER BY/LIMIT, COUNT(*), ON DUPLICATE KEY UPDATE, AUTO_INCREMENT, NULL three-valued logic, real '
120
+ + "BEGIN/COMMIT/ROLLBACK transactions, MySQL errno/sqlstate errors) served over PlanetScale's Connect-JSON "
121
+ + 'wire protocol: POST /psdb.v1alpha1.Database/{CreateSession,Execute,CloseSession}, HTTP Basic auth, '
122
+ + 'protojson QueryResult with base64 row packing and -1 NULL lengths, query errors at HTTP 200 in the '
123
+ + '{error:{code,message}} envelope and auth failures at 401 in the same one. Non-deterministic SQL (NOW, '
124
+ + 'RAND, UUID) is refused, never frozen. Kernel-backed.',
125
+ browserRouting: { apiPathPrefix: '/psdb.v1alpha1.Database/', loaderHost: 'https://aws.connect.psdb.cloud' },
126
+ };
127
+ registerPack(pack);
128
+ /**
129
+ * How a journey reads an answer of this pack (a step's `view`, scripts/behavior-journey.ts). `rows` decodes an Execute
130
+ * answer into what MySQL's client prints for it: `fields`, the result's column names in order; `rows`, each row as an
131
+ * array of its cells as text (MySQL's text protocol spelling: numbers as their digits, NULL as null), in the order the
132
+ * answer holds them; `rowsAffected` and `insertId` as numbers where psdb answers them (0 when it omits them, as its
133
+ * protojson does for a zero); and `error`, `{code, message}`, when the statement was refused. Cells are the packed bytes
134
+ * decoded as UTF-8.
135
+ */
136
+ export const answerViews = {
137
+ rows: (body) => {
138
+ const b = (body ?? {});
139
+ if (b.error)
140
+ return { error: b.error };
141
+ const r = b.result ?? {};
142
+ const rows = (r.rows ?? []).map((row) => {
143
+ const bytes = Uint8Array.from(atob(row.values ?? ''), (c) => c.charCodeAt(0));
144
+ let at = 0;
145
+ return (row.lengths ?? []).map((size) => {
146
+ const width = Number(size);
147
+ if (width < 0)
148
+ return null;
149
+ const cell = new TextDecoder().decode(bytes.subarray(at, at + width));
150
+ at += width;
151
+ return cell;
152
+ });
153
+ });
154
+ return { fields: (r.fields ?? []).map((f) => f.name ?? ''), rows, rowsAffected: Number(r.rowsAffected ?? 0), insertId: Number(r.insertId ?? 0) };
155
+ },
156
+ };
@@ -0,0 +1,2 @@
1
+ import type { DerivedManifest } from '@volter/world-core';
2
+ export declare const manifest: DerivedManifest;
@@ -0,0 +1,41 @@
1
+ // ps-http-sim, the simulator PlanetScale's driver authors publish for local development: its CloseSession handler
2
+ // closes the session's connection when the request names one (`closeConn(mysqlConnKey{…})`) and answers the session
3
+ // reset, whatever the connection's state.
4
+ const PS_HTTP_SIM = 'https://github.com/mattrobenolt/ps-http-sim/blob/main/main.go';
5
+ /** A session's `closed`. CreateSession opens one (the initial state); CloseSession closes an open one, discarding any
6
+ * uncommitted transaction as a dropped connection does. Closing one already closed moves nothing and answers 200, as
7
+ * ps-http-sim answers every close. Not moves of a session, and the twin's decisions where nothing shows the vendor's:
8
+ * a CloseSession naming a session the twin never issued answers 200 and writes nothing; an Execute naming a closed
9
+ * session runs in a new session the twin opens for it (planetscale-store.ts, `executeSqlUnlocked`), as an Execute
10
+ * naming none does (ps-http-sim: "if !clientSession { sess = session.New(…) }"). */
11
+ const sessionClosed = {
12
+ initial: false,
13
+ transitions: [
14
+ { operation: 'CloseSession', from: ['false'], to: 'true', source: PS_HTTP_SIM },
15
+ { operation: 'CloseSession', from: ['true'], source: PS_HTTP_SIM },
16
+ ],
17
+ };
18
+ export const manifest = {
19
+ vendor: 'planetscale',
20
+ service: 'planetscale',
21
+ // the driver always sends JSON (`Content-Type: application/json`, @planetscale/database dist/index.js `postJSON`)
22
+ body: { json: 'always' },
23
+ // the store mints a session `tws-<n>-<the World instant in ms>` (planetscale-store.ts, `mintSessionId`), stored as
24
+ // subject `session:<id>`; the core mints none (it owns no operation), so the template names the counted part only
25
+ ids: { template: '{prefix}-{n}' },
26
+ time: 'iso',
27
+ // Connect's transport-level error body: a lowercase code and a message, no wrapper
28
+ error: { code: '{code}', message: '{message}' },
29
+ // a whole-operation refusal of a write to a read-only twin; the psdb handlers refuse per statement (errno 1290 in a
30
+ // 200 `{session, error}`) and never reach this
31
+ readOnly: { status: 403, code: 'permission_denied', message: 'twin is read-only; omit readOnly to accept writes' },
32
+ malformedBody: { status: 400, code: 'invalid_argument', message: 'malformed JSON request body' },
33
+ // Connect's not_found; psdb addresses nothing by id in a path, so no operation answers it
34
+ notFound: { status: 404, code: 'not_found', message: 'not found' },
35
+ // psdb has no list operation: an Execute's rows are its QueryResult. Required by the type; nothing reads it.
36
+ list: { style: 'envelope', envelope: { data: '{data}' }, limit: { param: 'limit', default: 100, max: 100 } },
37
+ deleted: {},
38
+ resources: {
39
+ Session: { storedAs: '_session', idPrefix: 'tws', state: { closed: sessionClosed } },
40
+ },
41
+ };
@@ -0,0 +1,78 @@
1
+ import { RateBudget, type RateBudgetDeclaration, type RateBudgetOptions, type RateBudgetReservation, type RateBudgetSnapshot } from '@volter/world-core';
2
+ import type { PlanetscaleLikeClient } from './planetscale-connector.js';
3
+ /** Rolling window, in ms. Spend older than this is pruned. */
4
+ export declare const PLANETSCALE_BUDGET_WINDOW_MS = 60000;
5
+ /** Weighted units allowed inside one window. See the header for where this number comes from. */
6
+ export declare const PLANETSCALE_BUDGET_CEILING = 60;
7
+ /** Seconds. A `Retry-After` above this means the credential is throttled hard — fail loudly. */
8
+ export declare const PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S = 300;
9
+ /** Per-call cost, keyed by the client method the guard is about to invoke. See the header. */
10
+ export declare const PLANETSCALE_CALL_WEIGHTS: {
11
+ /** `transaction` — ONE guarded call that runs an unbounded number of statements behind it. */
12
+ readonly transaction: 8;
13
+ /** `execute` / `refresh` / anything unclassified. */
14
+ readonly other: 2;
15
+ };
16
+ /**
17
+ * The client methods this pack PRICES BY NAME. `@planetscale/database`'s `Client` and `Connection`
18
+ * expose exactly these three (dist/index.d.ts), and the connector calls only `execute`.
19
+ *
20
+ * NOT a closed list: a method absent from it is still priced at `defaultWeight` through the
21
+ * passthrough proxy. An unmodeled call must never be free — free would also make it invisible.
22
+ */
23
+ export declare const PLANETSCALE_BUDGETED_METHODS: readonly ["execute", "transaction", "refresh"];
24
+ /** THE PACK'S DECLARATION — pure data, the only PlanetScale-specific thing in the whole budget. */
25
+ export declare const PLANETSCALE_RATE_BUDGET: RateBudgetDeclaration;
26
+ /** Price one PlanetScale call by its client method name (`execute`, `transaction`, …). */
27
+ export declare function planetscaleCallWeight(method: string): number;
28
+ /** Where PlanetScale's ledger lives. Token-keyed and cwd-independent by default (the vendor meters
29
+ * per credential, so a cwd-scoped ledger would hand the same credential a fresh allowance in every
30
+ * checkout, worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
31
+ export declare function planetscaleBudgetPath(opts?: {
32
+ root?: string;
33
+ token?: string;
34
+ } | string): string;
35
+ /** Construction options for PlanetScale's budget. The vendor is fixed; everything else may only TIGHTEN. */
36
+ export type PlanetscaleBudgetOptions = Omit<RateBudgetOptions, 'vendor'>;
37
+ /**
38
+ * PlanetScale's budget — the shared kernel guard bound to this vendor's declaration. A real
39
+ * subclass, not an alias, so `budget instanceof PlanetscaleBudget` means "a budget that accounts
40
+ * against this vendor's ledger under this vendor's ceiling".
41
+ */
42
+ export declare class PlanetscaleBudget extends RateBudget {
43
+ constructor(opts?: PlanetscaleBudgetOptions);
44
+ }
45
+ export type { RateBudgetErrorKind as PlanetscaleBudgetErrorKind } from '@volter/world-core';
46
+ export { RateBudgetError as PlanetscaleBudgetError } from '@volter/world-core';
47
+ export type PlanetscaleBudgetReservation = RateBudgetReservation;
48
+ export type PlanetscaleBudgetSnapshot = RateBudgetSnapshot;
49
+ /** What every budgeted connector entrypoint accepts. There is deliberately no option that turns the
50
+ * guard OFF — only ones that say WHICH ledger and clock to account against. */
51
+ export type PlanetscaleBudgetedOptions = {
52
+ /** An existing budget to share across calls. Omit and one is constructed. Cannot be null. */
53
+ budget?: PlanetscaleBudget;
54
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
55
+ budgetOptions?: PlanetscaleBudgetOptions;
56
+ };
57
+ /** Pull the budget wiring out of a caller's opts bag, so an entrypoint can forward it verbatim. */
58
+ export declare function planetscaleBudgetOf(opts: PlanetscaleBudgetedOptions): PlanetscaleBudgetedOptions;
59
+ /** Is this client already behind a budget? Returns the budget it is behind, if so. */
60
+ export declare function planetscaleClientBudget(client: unknown): RateBudget | undefined;
61
+ /** Reserve every known row call plus session, BEGIN, COMMIT and possible ROLLBACK before IO. */
62
+ export declare function runPlanetscaleTransaction<T>(client: PlanetscaleLikeClient, statementCount: number, run: (tx: unknown) => Promise<T>, opts?: PlanetscaleBudgetedOptions): Promise<T>;
63
+ /**
64
+ * Wrap an INJECTED PlanetScale client so EVERY call it makes is charged against the shared budget
65
+ * BEFORE the request goes out. This pack never constructs the transport itself (the consumer injects
66
+ * something satisfying `PlanetscaleLikeClient` — a real `Client` or `Connection` is assignable
67
+ * as-is), so the guard is a DECORATOR rather than a factory, which is exactly why every connector
68
+ * entrypoint applies it UNCONDITIONALLY instead of trusting the caller.
69
+ *
70
+ * IDEMPOTENT: wrapping an already-guarded client returns it unchanged.
71
+ *
72
+ * A method that THROWS is still inspected: `@planetscale/database` RAISES `DatabaseError` /
73
+ * `UnknownError` on a non-2xx rather than returning it, and that error's status and headers are
74
+ * exactly the signal that must become a persisted cooldown. The original error is always re-raised
75
+ * afterwards — EXCEPT when the back-off is beyond the cap, where the budget's own louder "stop
76
+ * calling" error takes precedence.
77
+ */
78
+ export declare function guardPlanetscaleClient(client: PlanetscaleLikeClient, opts?: PlanetscaleBudgetedOptions): PlanetscaleLikeClient;
@@ -0,0 +1,305 @@
1
+ // PLANETSCALE'S CLIENT-SIDE RATE BUDGET — the pack's DECLARATION (the numbers) plus
2
+ // `guardPlanetscaleClient`, the choke point every live PlanetScale call goes through. The MECHANISM
3
+ // — the durable token-keyed ledger, the rolling window, reserve-under-lock, the `Retry-After`/429
4
+ // cooldown, fail-CLOSED on a corrupt ledger — lives ONCE in the vendor-agnostic kernel
5
+ // (`@volter/world-core` → `rateBudget.ts`). Read that module's header for the full rationale AND for the
6
+ // honest list of what the guard does NOT guarantee. This module is modeled on
7
+ // `notion-budget.ts` / `upstash-budget.ts`, the reference injected-client decorators.
8
+ //
9
+ // ── THE NUMBER, AND WHY IT IS THE KERNEL FALLBACK RATHER THAN A TRANSCRIBED LIMIT ─────────────
10
+ // PlanetScale publishes system limits and this build LIVE-READ them
11
+ // (planetscale.com/docs/reference/planetscale-system-limits, 2026-08-31). Every published figure is
12
+ // a PER-QUERY or PER-SCHEMA bound, not a rate:
13
+ // • per-query rows returned/updated/deleted: 100k • per-query result set: 64 MiB
14
+ // • autocommit timeout: 900s • transaction timeout: 20s
15
+ // • tables per schema: 2048 • columns per table: 1017
16
+ // There is NO published requests-per-second or requests-per-minute figure for the psdb HTTP API,
17
+ // and this build did not find one. Per ADDING_A_TWIN's rule for exactly that case, the reason says
18
+ // so plainly and the ceiling stays AT the kernel's austere fallback — 60 units / 60s at the default
19
+ // weight of 2 = 30 calls per minute — rather than dressing a guess up as a vendor fact. Nothing
20
+ // here is more permissive than `DEFAULT_RATE_BUDGET`, so no `VENDOR_BURST_ANCHOR` figure is owed.
21
+ //
22
+ // ── WHAT THE RISK ACTUALLY IS ─────────────────────────────────────────────────────────────────
23
+ // PlanetScale meters ROWS READ, not requests. One `SELECT` against a large table is a single call
24
+ // that can read millions of rows, so a request-count ceiling bounds the SHAPE of a runaway loop
25
+ // without bounding its cost. That is a real limitation of a request-count guard against this vendor
26
+ // and it is stated rather than papered over: the connector's own defence is that every pull takes a
27
+ // hard result `limit` (not a storage rows-examined guarantee), and `transaction` is priced at 4x because ONE guarded call fans out into an
28
+ // unbounded number of statements behind it (`Connection.transaction` in dist/index.js runs the
29
+ // caller's whole callback).
30
+ import { declareRateBudget, rateBudgetPath, rateBudgetWeight, RateBudget, RateBudgetError, } from '@volter/world-core';
31
+ const VENDOR = 'planetscale';
32
+ /** Rolling window, in ms. Spend older than this is pruned. */
33
+ export const PLANETSCALE_BUDGET_WINDOW_MS = 60_000;
34
+ /** Weighted units allowed inside one window. See the header for where this number comes from. */
35
+ export const PLANETSCALE_BUDGET_CEILING = 60;
36
+ /** Seconds. A `Retry-After` above this means the credential is throttled hard — fail loudly. */
37
+ export const PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S = 300;
38
+ /** Per-call cost, keyed by the client method the guard is about to invoke. See the header. */
39
+ export const PLANETSCALE_CALL_WEIGHTS = {
40
+ /** `transaction` — ONE guarded call that runs an unbounded number of statements behind it. */
41
+ transaction: 8,
42
+ /** `execute` / `refresh` / anything unclassified. */
43
+ other: 2,
44
+ };
45
+ /**
46
+ * The client methods this pack PRICES BY NAME. `@planetscale/database`'s `Client` and `Connection`
47
+ * expose exactly these three (dist/index.d.ts), and the connector calls only `execute`.
48
+ *
49
+ * NOT a closed list: a method absent from it is still priced at `defaultWeight` through the
50
+ * passthrough proxy. An unmodeled call must never be free — free would also make it invisible.
51
+ */
52
+ export const PLANETSCALE_BUDGETED_METHODS = ['execute', 'transaction', 'refresh'];
53
+ /** THE PACK'S DECLARATION — pure data, the only PlanetScale-specific thing in the whole budget. */
54
+ export const PLANETSCALE_RATE_BUDGET = {
55
+ windowMs: PLANETSCALE_BUDGET_WINDOW_MS,
56
+ ceiling: PLANETSCALE_BUDGET_CEILING,
57
+ defaultWeight: PLANETSCALE_CALL_WEIGHTS.other,
58
+ maxRetryAfterSeconds: PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S,
59
+ // Ordered: the kernel prices FIRST-MATCH-WINS, so the most expensive tier is listed first.
60
+ rules: [
61
+ { match: '(^|\\.)transaction$', weight: PLANETSCALE_CALL_WEIGHTS.transaction },
62
+ ],
63
+ reason: 'PlanetScale publishes NO requests-per-second or requests-per-minute limit for the psdb HTTP API. Its '
64
+ + 'system-limits page (https://planetscale.com/docs/reference/planetscale-system-limits, live-read '
65
+ + '2026-08-31) publishes only PER-QUERY and PER-SCHEMA bounds: 100k rows returned/updated/deleted per '
66
+ + 'query, a 64 MiB per-query result set, a 900s autocommit timeout, a 20s transaction timeout, 2048 tables '
67
+ + 'per schema and 1017 columns per table. None of those is a call rate, so — per the ADDING_A_TWIN rule for '
68
+ + 'a vendor that publishes no scalar rate — this declaration states that plainly and stays AT the kernel '
69
+ + "fallback (60 units / 60s at weight 2 = 30 calls per minute) instead of inventing a number. It is "
70
+ + 'therefore never more permissive than DEFAULT_RATE_BUDGET and owes no VENDOR_BURST_ANCHOR figure. The '
71
+ + 'real cost driver is metered ROWS READ rather than request count, which a request-count ceiling can only '
72
+ + 'bound crudely; that limitation is disclosed rather than hidden, and the connector answers it separately '
73
+ + 'by bounding returned pages on every pull; SQL LIMIT does not cap rows examined or billed. `transaction` is priced at 8 because ONE guarded call runs '
74
+ + "the caller's entire callback — an unbounded number of statements — behind it "
75
+ + '(@planetscale/database@1.20.1 dist/index.js, `Connection.transaction`).',
76
+ };
77
+ // Declared at module load, so merely importing this module (which `planetscale-connector.ts` does)
78
+ // is enough to arm the real ceiling.
79
+ declareRateBudget(VENDOR, PLANETSCALE_RATE_BUDGET);
80
+ /** Price one PlanetScale call by its client method name (`execute`, `transaction`, …). */
81
+ export function planetscaleCallWeight(method) {
82
+ return rateBudgetWeight(VENDOR, method);
83
+ }
84
+ /** Where PlanetScale's ledger lives. Token-keyed and cwd-independent by default (the vendor meters
85
+ * per credential, so a cwd-scoped ledger would hand the same credential a fresh allowance in every
86
+ * checkout, worktree and CI matrix leg); pass `root` to opt into world-scoped accounting. */
87
+ export function planetscaleBudgetPath(opts = {}) {
88
+ const o = typeof opts === 'string' ? { root: opts } : opts;
89
+ // VENDOR spread LAST: a loosely-typed `{ vendor: 'other', … }` slipping through (TypeScript's
90
+ // excess-property check only catches object literals) must not redirect this pack's ledger.
91
+ return rateBudgetPath({ ...o, vendor: VENDOR });
92
+ }
93
+ /**
94
+ * PlanetScale's budget — the shared kernel guard bound to this vendor's declaration. A real
95
+ * subclass, not an alias, so `budget instanceof PlanetscaleBudget` means "a budget that accounts
96
+ * against this vendor's ledger under this vendor's ceiling".
97
+ */
98
+ export class PlanetscaleBudget extends RateBudget {
99
+ constructor(opts = {}) {
100
+ super({ ...opts, vendor: VENDOR });
101
+ }
102
+ }
103
+ export { RateBudgetError as PlanetscaleBudgetError } from '@volter/world-core';
104
+ /** Pull the budget wiring out of a caller's opts bag, so an entrypoint can forward it verbatim. */
105
+ export function planetscaleBudgetOf(opts) {
106
+ return {
107
+ ...(opts.budget !== undefined ? { budget: opts.budget } : {}),
108
+ ...(opts.budgetOptions !== undefined ? { budgetOptions: opts.budgetOptions } : {}),
109
+ };
110
+ }
111
+ // ── the choke point ─────────────────────────────────────────────────────────────────────────
112
+ /**
113
+ * Marks a client this module has already wrapped, so guarding twice cannot charge twice.
114
+ *
115
+ * A MODULE-PRIVATE `Symbol()`, deliberately not `Symbol.for()`: a global-registry symbol is
116
+ * reachable BY NAME, so any caller could stamp the brand on a RAW client and the guard would hand it
117
+ * straight back UNGUARDED — a one-line bypass of the whole budget.
118
+ */
119
+ const GUARDED = Symbol('@volter/twin-planetscale.budget.guarded');
120
+ /** Is this client already behind a budget? Returns the budget it is behind, if so. */
121
+ export function planetscaleClientBudget(client) {
122
+ const mark = client?.[GUARDED];
123
+ return mark instanceof RateBudget ? mark : undefined;
124
+ }
125
+ const NEVER_WRAP = new Set(['then', 'catch', 'finally', 'constructor', 'prototype', 'toJSON', 'toString', 'valueOf', 'inspect']);
126
+ /** The HTTP status a value carries, if it looks like one (an SDK error, or a raw response). */
127
+ function responseStatus(v) {
128
+ const s = v?.status;
129
+ return typeof s === 'number' && Number.isFinite(s) ? s : undefined;
130
+ }
131
+ /**
132
+ * Response/error headers as a plain lower-cased record, or `undefined` when there are none. Accepts
133
+ * a `Headers` instance, a `Map`, or a plain object — `DatabaseError`/`UnknownError` carry a
134
+ * `context.headers` record (dist/index.js), and a raw `Response` carries a `Headers`.
135
+ */
136
+ function responseHeaders(v) {
137
+ const direct = v?.headers;
138
+ const nested = v?.context?.headers;
139
+ const h = direct ?? nested;
140
+ if (!h || typeof h !== 'object')
141
+ return undefined;
142
+ const out = {};
143
+ if (typeof h.forEach === 'function') {
144
+ h.forEach((value, key) => { out[String(key).toLowerCase()] = String(value); });
145
+ }
146
+ else {
147
+ for (const [k, value] of Object.entries(h))
148
+ out[k.toLowerCase()] = String(value);
149
+ }
150
+ return Object.keys(out).length > 0 ? out : undefined;
151
+ }
152
+ // Guard ownership stays module-private; known deployment batches can reserve once without
153
+ // exposing an unguarded client to callers or interrupting rollback with a second admission.
154
+ const rawClients = new WeakMap();
155
+ function settleBudget(budget, weight, reservation, value) {
156
+ if (value instanceof AggregateError) {
157
+ for (const error of value.errors)
158
+ settleBudget(budget, weight, reservation, error);
159
+ return;
160
+ }
161
+ budget.recordCall(weight, responseHeaders(value), { status: responseStatus(value), reservation });
162
+ }
163
+ /** Settle once the call RESOLVED: PlanetScale has answered. recordCall arms any cooldown before it
164
+ * throws (a back-off beyond the cap), so that refusal is swallowed and the result kept — a write
165
+ * that committed is never reported failed and run again on retry. A non-2xx status still throws. */
166
+ function settleAnswered(budget, weight, reservation, value) {
167
+ try {
168
+ settleBudget(budget, weight, reservation, value);
169
+ }
170
+ catch (error) {
171
+ const status = responseStatus(value);
172
+ if (!(error instanceof RateBudgetError) || (status !== undefined && (status < 200 || status >= 300)))
173
+ throw error;
174
+ }
175
+ }
176
+ async function charged(budget, weight, invoke) {
177
+ const reservation = budget.checkBudget(weight);
178
+ try {
179
+ const result = await invoke();
180
+ settleAnswered(budget, weight, reservation, result);
181
+ return result;
182
+ }
183
+ catch (error) {
184
+ settleBudget(budget, weight, reservation, error);
185
+ throw error;
186
+ }
187
+ }
188
+ /** Reserve every known row call plus session, BEGIN, COMMIT and possible ROLLBACK before IO. */
189
+ export async function runPlanetscaleTransaction(client, statementCount, run, opts = {}) {
190
+ if (!Number.isSafeInteger(statementCount) || statementCount < 0)
191
+ throw new Error('Invalid PlanetScale statement count');
192
+ const guarded = guardPlanetscaleClient(client, opts);
193
+ const raw = rawClients.get(guarded);
194
+ if (!raw?.transaction)
195
+ throw new Error('PlanetScale deployment requires a transactional client');
196
+ const budget = planetscaleClientBudget(guarded);
197
+ return charged(budget, Math.max(budget.weightFor('transaction'), (statementCount + 4) * budget.weightFor('execute')), () => raw.transaction(run));
198
+ }
199
+ /**
200
+ * Wrap an INJECTED PlanetScale client so EVERY call it makes is charged against the shared budget
201
+ * BEFORE the request goes out. This pack never constructs the transport itself (the consumer injects
202
+ * something satisfying `PlanetscaleLikeClient` — a real `Client` or `Connection` is assignable
203
+ * as-is), so the guard is a DECORATOR rather than a factory, which is exactly why every connector
204
+ * entrypoint applies it UNCONDITIONALLY instead of trusting the caller.
205
+ *
206
+ * IDEMPOTENT: wrapping an already-guarded client returns it unchanged.
207
+ *
208
+ * A method that THROWS is still inspected: `@planetscale/database` RAISES `DatabaseError` /
209
+ * `UnknownError` on a non-2xx rather than returning it, and that error's status and headers are
210
+ * exactly the signal that must become a persisted cooldown. The original error is always re-raised
211
+ * afterwards — EXCEPT when the back-off is beyond the cap, where the budget's own louder "stop
212
+ * calling" error takes precedence.
213
+ */
214
+ export function guardPlanetscaleClient(client, opts = {}) {
215
+ // There is no value a caller can pass to end up with an UNGUARDED client. Validated BEFORE the
216
+ // already-guarded early return, so `guard(alreadyGuarded, { budget: impostor })` is refused too.
217
+ if (opts.budget !== undefined && opts.budget !== null && !(opts.budget instanceof PlanetscaleBudget)) {
218
+ throw new Error('guardPlanetscaleClient: `budget` must be a PlanetscaleBudget — refusing to guard a PlanetScale client with an unverified rate guard');
219
+ }
220
+ if (planetscaleClientBudget(client))
221
+ return client;
222
+ const budget = opts.budget instanceof PlanetscaleBudget
223
+ ? opts.budget
224
+ : new PlanetscaleBudget({
225
+ // The default ledger is keyed by a hash of the credential — the vendor meters per credential,
226
+ // so a cwd-scoped ledger would hand it a fresh allowance per worktree/CI leg.
227
+ //
228
+ // HONESTLY: this pack does NOT hold the credential — the consumer's injected client does — so
229
+ // `PLANETSCALE_DATABASE_URL` is a BEST-EFFORT stand-in, not the real thing. If it names a
230
+ // different database than the injected client, spend is booked against the wrong ledger; if it
231
+ // is unset, every unattributed PlanetScale credential on the machine shares one (over-tight,
232
+ // the safe direction). A caller who knows the credential says so with `budgetOptions: { token }`,
233
+ // which is spread last and therefore wins.
234
+ ...(process.env.PLANETSCALE_DATABASE_URL !== undefined ? { token: process.env.PLANETSCALE_DATABASE_URL } : {}),
235
+ ...(opts.budgetOptions ?? {}),
236
+ });
237
+ /** Settle a reservation from whatever the call produced. May THROW (a back-off past the cap). */
238
+ const settle = (weight, reservation, value) => settleBudget(budget, weight, reservation, value);
239
+ /**
240
+ * Charge, call, settle. Refusing REJECTS rather than throwing synchronously, so
241
+ * `client.execute(...).catch(…)` behaves exactly as it does on an unguarded client.
242
+ * `checkBudget` RESERVES under lock, so nothing after its line runs when the budget refuses:
243
+ * the request is never made.
244
+ */
245
+ const chargeAsync = (method, invoke) => async (...args) => charged(budget, budget.weightFor(method), async () => invoke(...args));
246
+ const guarded = {};
247
+ for (const name of PLANETSCALE_BUDGETED_METHODS) {
248
+ const raw = client[name];
249
+ if (typeof raw !== 'function')
250
+ continue;
251
+ // Resolved at CALL time, not here, so a client whose method is swapped later is still charged.
252
+ guarded[name] = chargeAsync(name, (...args) => client[name].apply(client, args));
253
+ }
254
+ Object.defineProperty(guarded, GUARDED, { value: budget, enumerable: false });
255
+ // A real client has MORE than the three methods above (`connection()`, `config`, …) and a consumer
256
+ // who needs one must not be forced to keep the RAW client alongside — every call through that
257
+ // would be unbudgeted. So the guarded object is a Proxy: modeled members come from the map above,
258
+ // anything else is taken from the real client and PRICED at `defaultWeight`.
259
+ const proxy = new Proxy(guarded, {
260
+ get(target, prop, receiver) {
261
+ if (typeof prop === 'symbol') {
262
+ const own = Reflect.get(target, prop, receiver);
263
+ return own !== undefined ? own : client[prop];
264
+ }
265
+ const name = String(prop);
266
+ if (NEVER_WRAP.has(name))
267
+ return Reflect.get(target, prop, receiver);
268
+ const own = Reflect.get(target, prop, receiver);
269
+ if (own !== undefined)
270
+ return own;
271
+ const from = client[name];
272
+ // `connection()` returns a NEW Connection whose `execute` would otherwise escape the budget
273
+ // entirely, so a function member is charged AND its result is re-guarded when it looks like a
274
+ // client. Anything else passes through.
275
+ if (typeof from === 'function') {
276
+ return (...args) => {
277
+ const weight = budget.weightFor(name);
278
+ const reservation = budget.checkBudget(weight);
279
+ let out;
280
+ try {
281
+ out = from.apply(client, args);
282
+ }
283
+ catch (e) {
284
+ settle(weight, reservation, e);
285
+ throw e;
286
+ }
287
+ if (out !== null && typeof out === 'object' && typeof out.then === 'function') {
288
+ return out.then((res) => { settleAnswered(budget, weight, reservation, res); return res; }, (e) => { settle(weight, reservation, e); throw e; });
289
+ }
290
+ settle(weight, reservation, undefined);
291
+ if (out !== null && typeof out === 'object' && typeof out.execute === 'function') {
292
+ return guardPlanetscaleClient(out, { budget });
293
+ }
294
+ return out;
295
+ };
296
+ }
297
+ return from;
298
+ },
299
+ has(target, prop) {
300
+ return Reflect.has(target, prop) || Reflect.has(client, prop);
301
+ },
302
+ });
303
+ rawClients.set(proxy, client);
304
+ return proxy;
305
+ }
@@ -0,0 +1,10 @@
1
+ import { type CapabilityReport, type CapabilitySpec } from '@volter/world-tooling';
2
+ /**
3
+ * The AREA CENSUS — enumerated TOP-DOWN from MySQL's own statement families and PlanetScale's own
4
+ * product surface (the psdb protocol, the branch credential model, the management API), NOT derived
5
+ * from the manifest below. Deriving it from the manifest would make `assertAreaCensus` a tautology;
6
+ * enumerating it independently is what gives the check teeth.
7
+ */
8
+ export declare const PLANETSCALE_AREAS: readonly ["auth", "collation", "conformance", "connector", "ddl", "delete", "errors", "functions", "insert", "limits", "management", "native", "protocol", "readonly", "select", "session", "transactions", "update", "wire"];
9
+ export declare const PLANETSCALE_CAPABILITIES: CapabilitySpec[];
10
+ export declare function planetscaleCapabilities(): Promise<CapabilityReport>;