@polardbx/pxbaas 0.0.0-stage → 0.1.0-beta.10

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 (312) hide show
  1. package/README.md +223 -2
  2. package/bin/pxbaas.js +13 -0
  3. package/dist/app/local-index.d.ts +70 -0
  4. package/dist/app/local-index.d.ts.map +1 -0
  5. package/dist/app/local-index.js +140 -0
  6. package/dist/app/local-index.js.map +1 -0
  7. package/dist/app/preflight.d.ts +61 -0
  8. package/dist/app/preflight.d.ts.map +1 -0
  9. package/dist/app/preflight.js +115 -0
  10. package/dist/app/preflight.js.map +1 -0
  11. package/dist/app/spec-lint.d.ts +45 -0
  12. package/dist/app/spec-lint.d.ts.map +1 -0
  13. package/dist/app/spec-lint.js +423 -0
  14. package/dist/app/spec-lint.js.map +1 -0
  15. package/dist/app/templates.d.ts +167 -0
  16. package/dist/app/templates.d.ts.map +1 -0
  17. package/dist/app/templates.js +151 -0
  18. package/dist/app/templates.js.map +1 -0
  19. package/dist/cli/help-guides.d.ts +17 -0
  20. package/dist/cli/help-guides.d.ts.map +1 -0
  21. package/dist/cli/help-guides.js +132 -0
  22. package/dist/cli/help-guides.js.map +1 -0
  23. package/dist/cli/root.d.ts +3 -0
  24. package/dist/cli/root.d.ts.map +1 -0
  25. package/dist/cli/root.js +75 -0
  26. package/dist/cli/root.js.map +1 -0
  27. package/dist/commands/app/index.d.ts +8 -0
  28. package/dist/commands/app/index.d.ts.map +1 -0
  29. package/dist/commands/app/index.js +1907 -0
  30. package/dist/commands/app/index.js.map +1 -0
  31. package/dist/commands/auth/index.d.ts +3 -0
  32. package/dist/commands/auth/index.d.ts.map +1 -0
  33. package/dist/commands/auth/index.js +198 -0
  34. package/dist/commands/auth/index.js.map +1 -0
  35. package/dist/commands/completion/index.d.ts +3 -0
  36. package/dist/commands/completion/index.d.ts.map +1 -0
  37. package/dist/commands/completion/index.js +14 -0
  38. package/dist/commands/completion/index.js.map +1 -0
  39. package/dist/commands/data/index.d.ts +55 -0
  40. package/dist/commands/data/index.d.ts.map +1 -0
  41. package/dist/commands/data/index.js +407 -0
  42. package/dist/commands/data/index.js.map +1 -0
  43. package/dist/commands/db/index.d.ts +29 -0
  44. package/dist/commands/db/index.d.ts.map +1 -0
  45. package/dist/commands/db/index.js +500 -0
  46. package/dist/commands/db/index.js.map +1 -0
  47. package/dist/commands/doctor/index.d.ts +3 -0
  48. package/dist/commands/doctor/index.d.ts.map +1 -0
  49. package/dist/commands/doctor/index.js +305 -0
  50. package/dist/commands/doctor/index.js.map +1 -0
  51. package/dist/commands/functions/delete.d.ts +3 -0
  52. package/dist/commands/functions/delete.d.ts.map +1 -0
  53. package/dist/commands/functions/delete.js +55 -0
  54. package/dist/commands/functions/delete.js.map +1 -0
  55. package/dist/commands/functions/deploy.d.ts +3 -0
  56. package/dist/commands/functions/deploy.d.ts.map +1 -0
  57. package/dist/commands/functions/deploy.js +118 -0
  58. package/dist/commands/functions/deploy.js.map +1 -0
  59. package/dist/commands/functions/index.d.ts +3 -0
  60. package/dist/commands/functions/index.d.ts.map +1 -0
  61. package/dist/commands/functions/index.js +15 -0
  62. package/dist/commands/functions/index.js.map +1 -0
  63. package/dist/commands/functions/list.d.ts +6 -0
  64. package/dist/commands/functions/list.d.ts.map +1 -0
  65. package/dist/commands/functions/list.js +63 -0
  66. package/dist/commands/functions/list.js.map +1 -0
  67. package/dist/commands/functions/new.d.ts +7 -0
  68. package/dist/commands/functions/new.d.ts.map +1 -0
  69. package/dist/commands/functions/new.js +72 -0
  70. package/dist/commands/functions/new.js.map +1 -0
  71. package/dist/commands/gen/index.d.ts +3 -0
  72. package/dist/commands/gen/index.d.ts.map +1 -0
  73. package/dist/commands/gen/index.js +323 -0
  74. package/dist/commands/gen/index.js.map +1 -0
  75. package/dist/commands/guide/index.d.ts +8 -0
  76. package/dist/commands/guide/index.d.ts.map +1 -0
  77. package/dist/commands/guide/index.js +20 -0
  78. package/dist/commands/guide/index.js.map +1 -0
  79. package/dist/commands/instance/index.d.ts +8 -0
  80. package/dist/commands/instance/index.d.ts.map +1 -0
  81. package/dist/commands/instance/index.js +481 -0
  82. package/dist/commands/instance/index.js.map +1 -0
  83. package/dist/commands/login/index.d.ts +3 -0
  84. package/dist/commands/login/index.d.ts.map +1 -0
  85. package/dist/commands/login/index.js +145 -0
  86. package/dist/commands/login/index.js.map +1 -0
  87. package/dist/commands/logout/index.d.ts +3 -0
  88. package/dist/commands/logout/index.d.ts.map +1 -0
  89. package/dist/commands/logout/index.js +41 -0
  90. package/dist/commands/logout/index.js.map +1 -0
  91. package/dist/commands/profile/import-env.d.ts +18 -0
  92. package/dist/commands/profile/import-env.d.ts.map +1 -0
  93. package/dist/commands/profile/import-env.js +206 -0
  94. package/dist/commands/profile/import-env.js.map +1 -0
  95. package/dist/commands/profile/index.d.ts +3 -0
  96. package/dist/commands/profile/index.d.ts.map +1 -0
  97. package/dist/commands/profile/index.js +124 -0
  98. package/dist/commands/profile/index.js.map +1 -0
  99. package/dist/commands/rest/index.d.ts +3 -0
  100. package/dist/commands/rest/index.d.ts.map +1 -0
  101. package/dist/commands/rest/index.js +102 -0
  102. package/dist/commands/rest/index.js.map +1 -0
  103. package/dist/commands/skill/index.d.ts +11 -0
  104. package/dist/commands/skill/index.d.ts.map +1 -0
  105. package/dist/commands/skill/index.js +228 -0
  106. package/dist/commands/skill/index.js.map +1 -0
  107. package/dist/commands/status/index.d.ts +3 -0
  108. package/dist/commands/status/index.d.ts.map +1 -0
  109. package/dist/commands/status/index.js +59 -0
  110. package/dist/commands/status/index.js.map +1 -0
  111. package/dist/commands/storage/index.d.ts +3 -0
  112. package/dist/commands/storage/index.d.ts.map +1 -0
  113. package/dist/commands/storage/index.js +400 -0
  114. package/dist/commands/storage/index.js.map +1 -0
  115. package/dist/commands/tenant/index.d.ts +19 -0
  116. package/dist/commands/tenant/index.d.ts.map +1 -0
  117. package/dist/commands/tenant/index.js +667 -0
  118. package/dist/commands/tenant/index.js.map +1 -0
  119. package/dist/commands/whoami/index.d.ts +3 -0
  120. package/dist/commands/whoami/index.d.ts.map +1 -0
  121. package/dist/commands/whoami/index.js +101 -0
  122. package/dist/commands/whoami/index.js.map +1 -0
  123. package/dist/credentials/commit.d.ts +38 -0
  124. package/dist/credentials/commit.d.ts.map +1 -0
  125. package/dist/credentials/commit.js +57 -0
  126. package/dist/credentials/commit.js.map +1 -0
  127. package/dist/credentials/credential-store.d.ts +86 -0
  128. package/dist/credentials/credential-store.d.ts.map +1 -0
  129. package/dist/credentials/credential-store.js +282 -0
  130. package/dist/credentials/credential-store.js.map +1 -0
  131. package/dist/credentials/env-file.d.ts +85 -0
  132. package/dist/credentials/env-file.d.ts.map +1 -0
  133. package/dist/credentials/env-file.js +193 -0
  134. package/dist/credentials/env-file.js.map +1 -0
  135. package/dist/credentials/formatter.d.ts +39 -0
  136. package/dist/credentials/formatter.d.ts.map +1 -0
  137. package/dist/credentials/formatter.js +79 -0
  138. package/dist/credentials/formatter.js.map +1 -0
  139. package/dist/credentials/index.d.ts +13 -0
  140. package/dist/credentials/index.d.ts.map +1 -0
  141. package/dist/credentials/index.js +13 -0
  142. package/dist/credentials/index.js.map +1 -0
  143. package/dist/credentials/jwt-introspect.d.ts +39 -0
  144. package/dist/credentials/jwt-introspect.d.ts.map +1 -0
  145. package/dist/credentials/jwt-introspect.js +64 -0
  146. package/dist/credentials/jwt-introspect.js.map +1 -0
  147. package/dist/credentials/masker.d.ts +23 -0
  148. package/dist/credentials/masker.d.ts.map +1 -0
  149. package/dist/credentials/masker.js +40 -0
  150. package/dist/credentials/masker.js.map +1 -0
  151. package/dist/credentials/profile.d.ts +95 -0
  152. package/dist/credentials/profile.d.ts.map +1 -0
  153. package/dist/credentials/profile.js +207 -0
  154. package/dist/credentials/profile.js.map +1 -0
  155. package/dist/credentials/tenant-guard.d.ts +16 -0
  156. package/dist/credentials/tenant-guard.d.ts.map +1 -0
  157. package/dist/credentials/tenant-guard.js +53 -0
  158. package/dist/credentials/tenant-guard.js.map +1 -0
  159. package/dist/credentials/tty-guard.d.ts +29 -0
  160. package/dist/credentials/tty-guard.d.ts.map +1 -0
  161. package/dist/credentials/tty-guard.js +79 -0
  162. package/dist/credentials/tty-guard.js.map +1 -0
  163. package/dist/db/apply-schema.d.ts +144 -0
  164. package/dist/db/apply-schema.d.ts.map +1 -0
  165. package/dist/db/apply-schema.js +359 -0
  166. package/dist/db/apply-schema.js.map +1 -0
  167. package/dist/db/introspect.d.ts +54 -0
  168. package/dist/db/introspect.d.ts.map +1 -0
  169. package/dist/db/introspect.js +140 -0
  170. package/dist/db/introspect.js.map +1 -0
  171. package/dist/db/sql-file.d.ts +104 -0
  172. package/dist/db/sql-file.d.ts.map +1 -0
  173. package/dist/db/sql-file.js +584 -0
  174. package/dist/db/sql-file.js.map +1 -0
  175. package/dist/instance/provision.d.ts +89 -0
  176. package/dist/instance/provision.d.ts.map +1 -0
  177. package/dist/instance/provision.js +139 -0
  178. package/dist/instance/provision.js.map +1 -0
  179. package/dist/instance/zero-store.d.ts +47 -0
  180. package/dist/instance/zero-store.d.ts.map +1 -0
  181. package/dist/instance/zero-store.js +91 -0
  182. package/dist/instance/zero-store.js.map +1 -0
  183. package/dist/instance/zero-whitelist.d.ts +39 -0
  184. package/dist/instance/zero-whitelist.d.ts.map +1 -0
  185. package/dist/instance/zero-whitelist.js +67 -0
  186. package/dist/instance/zero-whitelist.js.map +1 -0
  187. package/dist/main.d.ts +3 -0
  188. package/dist/main.d.ts.map +1 -0
  189. package/dist/main.js +69 -0
  190. package/dist/main.js.map +1 -0
  191. package/dist/skill/agents-md.d.ts +25 -0
  192. package/dist/skill/agents-md.d.ts.map +1 -0
  193. package/dist/skill/agents-md.js +62 -0
  194. package/dist/skill/agents-md.js.map +1 -0
  195. package/dist/skill/assets.d.ts +38 -0
  196. package/dist/skill/assets.d.ts.map +1 -0
  197. package/dist/skill/assets.js +111 -0
  198. package/dist/skill/assets.js.map +1 -0
  199. package/dist/skill/hint.d.ts +39 -0
  200. package/dist/skill/hint.d.ts.map +1 -0
  201. package/dist/skill/hint.js +109 -0
  202. package/dist/skill/hint.js.map +1 -0
  203. package/dist/skill/hosts.d.ts +45 -0
  204. package/dist/skill/hosts.d.ts.map +1 -0
  205. package/dist/skill/hosts.js +96 -0
  206. package/dist/skill/hosts.js.map +1 -0
  207. package/dist/skill/installer.d.ts +100 -0
  208. package/dist/skill/installer.d.ts.map +1 -0
  209. package/dist/skill/installer.js +241 -0
  210. package/dist/skill/installer.js.map +1 -0
  211. package/dist/skill/ownership.d.ts +35 -0
  212. package/dist/skill/ownership.d.ts.map +1 -0
  213. package/dist/skill/ownership.js +52 -0
  214. package/dist/skill/ownership.js.map +1 -0
  215. package/dist/tenant/bundle.d.ts +66 -0
  216. package/dist/tenant/bundle.d.ts.map +1 -0
  217. package/dist/tenant/bundle.js +132 -0
  218. package/dist/tenant/bundle.js.map +1 -0
  219. package/dist/tenant/guide.d.ts +50 -0
  220. package/dist/tenant/guide.d.ts.map +1 -0
  221. package/dist/tenant/guide.js +152 -0
  222. package/dist/tenant/guide.js.map +1 -0
  223. package/dist/tenant/probe.d.ts +65 -0
  224. package/dist/tenant/probe.d.ts.map +1 -0
  225. package/dist/tenant/probe.js +159 -0
  226. package/dist/tenant/probe.js.map +1 -0
  227. package/dist/tenant/verify.d.ts +37 -0
  228. package/dist/tenant/verify.d.ts.map +1 -0
  229. package/dist/tenant/verify.js +132 -0
  230. package/dist/tenant/verify.js.map +1 -0
  231. package/dist/version.d.ts +29 -0
  232. package/dist/version.d.ts.map +1 -0
  233. package/dist/version.js +65 -0
  234. package/dist/version.js.map +1 -0
  235. package/package.json +63 -3
  236. package/skills/pxbaas-app-spec/SKILL.md +165 -0
  237. package/skills/pxbaas-app-spec/references/deploy-options.md +158 -0
  238. package/skills/pxbaas-app-spec/references/guardrails-pitfalls.md +89 -0
  239. package/skills/pxbaas-app-spec/references/runtime-contract.md +151 -0
  240. package/skills/pxbaas-app-spec/references/ssr-session.md +121 -0
  241. package/skills/pxbaas-app-spec/references/templates-examples.md +311 -0
  242. package/skills/pxbaas-cli/SKILL.md +186 -0
  243. package/skills/pxbaas-cli/references/api-reference.md +68 -0
  244. package/skills/pxbaas-cli/references/app-deploy.md +186 -0
  245. package/skills/pxbaas-cli/references/auth-and-credentials.md +116 -0
  246. package/skills/pxbaas-cli/references/data-postgrest.md +101 -0
  247. package/skills/pxbaas-cli/references/db-and-meta.md +102 -0
  248. package/skills/pxbaas-cli/references/edge-functions.md +71 -0
  249. package/skills/pxbaas-cli/references/multi-tenant-apps.md +138 -0
  250. package/skills/pxbaas-cli/references/provision.md +101 -0
  251. package/skills/pxbaas-cli/references/safety-and-two-phase.md +78 -0
  252. package/skills/pxbaas-cli/references/storage.md +61 -0
  253. package/skills/pxbaas-cli/references/tenant-admin.md +122 -0
  254. package/skills/pxbaas-cli/references/troubleshooting.md +76 -0
  255. package/templates/webapp/api/index.ts +55 -0
  256. package/templates/webapp/csr-bff/README.md +81 -0
  257. package/templates/webapp/csr-bff/backend/index.ts +168 -0
  258. package/templates/webapp/csr-bff/db/rls.json +13 -0
  259. package/templates/webapp/csr-bff/db/schema.sql +49 -0
  260. package/templates/webapp/csr-bff/frontend/dist/admin.html +101 -0
  261. package/templates/webapp/csr-bff/frontend/dist/article.html +44 -0
  262. package/templates/webapp/csr-bff/frontend/dist/blog.js +206 -0
  263. package/templates/webapp/csr-bff/frontend/dist/blogger.html +53 -0
  264. package/templates/webapp/csr-bff/frontend/dist/index.html +70 -0
  265. package/templates/webapp/csr-bff/frontend/dist/login.html +56 -0
  266. package/templates/webapp/csr-bff/frontend/dist/profile.html +59 -0
  267. package/templates/webapp/csr-bff/frontend/dist/signup.html +78 -0
  268. package/templates/webapp/csr-bff/frontend/dist/style.css +27 -0
  269. package/templates/webapp/manifest.json +419 -0
  270. package/templates/webapp/mpa/about.html +29 -0
  271. package/templates/webapp/mpa/app.js +42 -0
  272. package/templates/webapp/mpa/index.html +42 -0
  273. package/templates/webapp/mpa/index.ts +60 -0
  274. package/templates/webapp/mpa/style.css +42 -0
  275. package/templates/webapp/react-vite/frontend/index.html +12 -0
  276. package/templates/webapp/react-vite/frontend/package-lock.json +1826 -0
  277. package/templates/webapp/react-vite/frontend/package.json +23 -0
  278. package/templates/webapp/react-vite/frontend/src/App.tsx +34 -0
  279. package/templates/webapp/react-vite/frontend/src/main.tsx +13 -0
  280. package/templates/webapp/react-vite/frontend/src/style.css +46 -0
  281. package/templates/webapp/react-vite/frontend/tsconfig.json +20 -0
  282. package/templates/webapp/react-vite/frontend/vite.config.ts +10 -0
  283. package/templates/webapp/react-vite-hono/backend/index.ts +38 -0
  284. package/templates/webapp/react-vite-hono/frontend/index.html +12 -0
  285. package/templates/webapp/react-vite-hono/frontend/package-lock.json +1826 -0
  286. package/templates/webapp/react-vite-hono/frontend/package.json +23 -0
  287. package/templates/webapp/react-vite-hono/frontend/src/App.tsx +47 -0
  288. package/templates/webapp/react-vite-hono/frontend/src/main.tsx +13 -0
  289. package/templates/webapp/react-vite-hono/frontend/src/style.css +66 -0
  290. package/templates/webapp/react-vite-hono/frontend/tsconfig.json +20 -0
  291. package/templates/webapp/react-vite-hono/frontend/vite.config.ts +10 -0
  292. package/templates/webapp/spa/index.ts +119 -0
  293. package/templates/webapp/ssr-auth/backend/index.ts +133 -0
  294. package/templates/webapp/ssr-auth/frontend/dist/index.html +15 -0
  295. package/templates/webapp/ssr-blog/README.md +111 -0
  296. package/templates/webapp/ssr-blog/backend/index.ts +764 -0
  297. package/templates/webapp/ssr-blog/db/rls.json +13 -0
  298. package/templates/webapp/ssr-blog/db/schema.sql +51 -0
  299. package/templates/webapp/ssr-blog/frontend/dist/index.html +39 -0
  300. package/templates/webapp/ssr-frameworks/README.md +64 -0
  301. package/templates/webapp/ssr-frameworks/backend/index.ts +241 -0
  302. package/templates/webapp/ssr-frameworks/frontend/dist/index.html +31 -0
  303. package/templates/webapp/ssr-hono/backend/index.ts +111 -0
  304. package/templates/webapp/ssr-hono/frontend/dist/index.html +12 -0
  305. package/templates/webapp/ssr-shop/README.md +80 -0
  306. package/templates/webapp/ssr-shop/backend/index.ts +491 -0
  307. package/templates/webapp/ssr-shop/db/rls.json +16 -0
  308. package/templates/webapp/ssr-shop/db/schema.sql +90 -0
  309. package/templates/webapp/ssr-shop/frontend/dist/index.html +12 -0
  310. package/templates/webapp/vanilla-static/frontend/dist/app.js +6 -0
  311. package/templates/webapp/vanilla-static/frontend/dist/index.html +19 -0
  312. package/templates/webapp/vanilla-static/frontend/dist/style.css +43 -0
@@ -0,0 +1,61 @@
1
+ # Storage Management (storage)
2
+
3
+ Covers: `storage ls` · `storage cp` · `storage mv` · `storage rm`.
4
+ Backed by the Storage API (capability `storage` → `${endpoint}/storage/v1`).
5
+
6
+ ## Path format
7
+
8
+ Always `bucket/path/to/object`: the first segment is the **bucket name**, the rest is the object key (prefix).
9
+
10
+ ## storage ls — list buckets or objects (read-only, safe)
11
+
12
+ ```bash
13
+ pxbaas storage ls --json # no path: list all buckets
14
+ pxbaas storage ls my-bucket --json # list objects in a bucket (top level)
15
+ pxbaas storage ls my-bucket/photos --json # list objects under a prefix
16
+ ```
17
+
18
+ - **Signature:** `storage ls [path]` · **Flags:** `--json`
19
+ - No path → buckets (name / public / created_at). With path → objects in that bucket.
20
+
21
+ ## storage cp — copy/upload/download (write)
22
+
23
+ ```bash
24
+ pxbaas storage cp <src> <dst> [--recursive] --json
25
+ ```
26
+
27
+ - **Flags:** `--recursive` · `--json`
28
+ - Both `src` and `dst` **must include bucket + path** (e.g. `bucket/path/to/file`), otherwise it errors.
29
+ - Supports local↔remote and remote↔remote.
30
+ - ⚠️ write — confirm source and destination with the user (beware silent overwrite).
31
+
32
+ ## storage mv — move/rename (write, remote only)
33
+
34
+ ```bash
35
+ pxbaas storage mv <srcBucket/srcPath> <dstBucket/dstPath> --json
36
+ ```
37
+
38
+ - Moves/renames within remote storage; both ends need bucket + path.
39
+ - ⚠️ changes object location — confirm with the user first.
40
+
41
+ ## storage rm — delete objects (destructive)
42
+
43
+ ```bash
44
+ pxbaas storage rm <bucket/path> [--recursive] --json
45
+ ```
46
+
47
+ - **Flags:** `--recursive` (delete all objects under the prefix) · `--json`
48
+ - ⚠️ **Irreversible, no built-in gate:** run `storage ls <path>` first to confirm the objects/count → restate to the user → get explicit consent → then delete.
49
+ Be especially careful with `--recursive`: a wrong prefix can delete many objects.
50
+
51
+ ## Workflow — safe delete/overwrite
52
+
53
+ ```
54
+ 1) storage ls <target prefix> # preview
55
+ 2) restate to user: bucket + path + object count + operation (rm / mv / cp overwrite)
56
+ 3) get explicit consent
57
+ 4) execute
58
+ 5) storage ls again to verify
59
+ ```
60
+
61
+ > Storage endpoints (public vs authenticated, signed URLs, multi-tenant path rewriting): fetch `${endpoint}/docs` — see api-reference.md.
@@ -0,0 +1,122 @@
1
+ # Tenant Administration (platform-operator surface)
2
+
3
+ Covers: `tenant login` · `tenant logout` · `tenant probe` · `tenant ls` · `tenant create` · `tenant keys` · `tenant repair-keys` · `tenant handoffs`, and the tenant access bundle.
4
+
5
+ **Who this is for:** the operator of a **multi-tenant** PolarDB-X BaaS instance. Ordinary tenants never use this surface — they hold per-tenant API keys and use `db` / `data` / `app`. Never point a tenant at these commands.
6
+
7
+ ## Credential model (read this first)
8
+
9
+ | | Data plane | Admin plane |
10
+ | --- | --- | --- |
11
+ | Secret | per-tenant `anon` + `service_role` JWT | instance dashboard HTTP Basic (`DASHBOARD_USERNAME` / `DASHBOARD_PASSWORD`) |
12
+ | Keyring account | `credentials/<profile>` | `admin/<profile>` |
13
+ | Used by | `db`, `data`, `storage`, `auth`, `functions`, `app` | `tenant *` |
14
+
15
+ Rules that hold in code, not convention:
16
+
17
+ - **The password never goes in argv.** `--dashboard-user`, `--dashboard-password`, `--admin-password` are refused by `assertNoKeyArgs`. Supply them via TTY prompt (masked) or via `--user-stdin --password-stdin` (two lines: username, then password; both flags are required together).
18
+ - **`DASHBOARD_*` found in an env file is NOT imported by `profile import`.** It is reported as present-but-not-imported. Only `--with-admin` binds it, and then only after a successful admin-channel probe.
19
+ - **Endpoint must be the Kong gateway.** Studio's `/api/platform/*` routes do not authenticate on their own (`apiWrapper` is called without `withAuth`); the only gate is Kong's dashboard `basic-auth`. Targeting a component port bypasses it and is refused at the prompt.
20
+ - Plaintext is never printed. Output carries fingerprints (`sha256:<8>...<4>`); the only sinks for plaintext keys are the keyring (`--as-profile`) and a `0600` access bundle (`--bundle`).
21
+
22
+ ## Capability probe
23
+
24
+ `pxbaas tenant probe --json` answers, in one read-only call, whether the current profile can operate the admin plane:
25
+
26
+ | `status` | meaning | next move |
27
+ | --- | --- | --- |
28
+ | `admin` | admin channel works | `tenant ls`, then `create` / `keys` |
29
+ | `not-admin` | 401/403 from the gateway | these are not dashboard credentials |
30
+ | `single-tenant` | instance reports `multiTenantMode:false` | tenant administration does not apply |
31
+ | `metadata-unreadable` | credentials accepted but tenant metadata fails to load | platform-side schema drift — do not report "no tenants" |
32
+ | `unreachable` | network/endpoint failure | fix connectivity first |
33
+
34
+ Why `metadata-unreadable` exists: the list endpoint also returns `200 + tenants: []` when its own query fails. The probe disambiguates by asking for a sentinel tenant id — `404` means the metadata path is healthy (so the empty list is real), `500` means it is not.
35
+
36
+ ## Command reference
37
+
38
+ | Command | Options (defaults) | Notes |
39
+ | --- | --- | --- |
40
+ | `tenant login` | `--endpoint <url>`, `--user <name>`, `--user-stdin`, `--password-stdin` | Probes **before** storing; a refused probe stores nothing. Creates an admin-only profile when none exists (it holds no tenant keys, so data-plane commands on it still ask for `pxbaas login`) |
41
+ | `tenant logout` | — | Removes dashboard credentials only; tenant keys untouched |
42
+ | `tenant probe` | — | Read-only |
43
+ | `tenant ls` | `--status <active\|suspended\|deleting\|deleted>` | Redacted rows: id, status, plan, db, plus `hasAnonKey` / `hasServiceRoleKey` booleans. A missing key shows a `repair-keys` hint |
44
+ | `tenant create [tenant-id]` | `--generate-id`, `--plan` (`free`), `--display-name`, `--retention-days <n>`, `--reactivate`, `--dry-run`, `--yes`, `--as-profile [name]`, `--bundle <file>`, `--intranet <url>`, `--overwrite` | Id must match `^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$` |
45
+ | `tenant keys <tenant-id>` | `--as-profile [name]`, `--bundle <file>`, `--intranet <url>`, `--overwrite` | Requires one destination; refuses to bundle when either key is NULL |
46
+ | `tenant repair-keys` | `--confirm` | Without `--confirm` prints which tenants are missing keys and changes nothing |
47
+ | `tenant handoffs` | — | Local ledger: tenant id, timestamps, fingerprints, bundle path + sha256. Never key material |
48
+
49
+ All of them accept `--json` and `--profile <name>`.
50
+
51
+ ## Two-phase behaviour
52
+
53
+ | Command | Preview | Authorization |
54
+ | --- | --- | --- |
55
+ | `tenant create` | `--dry-run` (shows plan + side effects) | TTY confirm, or `--yes`; without it **nothing is created** |
56
+ | `tenant repair-keys` | default (no `--confirm`) | `--confirm` |
57
+ | `tenant keys` / `ls` / `probe` / `handoffs` | n/a | read-only (writes are local: keyring / bundle file) |
58
+
59
+ `tenant create` exits non-zero (`process.exitCode = 2`) when post-create verification fails — a `201` from the platform is **not** proof of a usable tenant, because CREATE DATABASE / CREATE USER / GRANT / RLS tables / Realtime registration are non-fatal server-side steps. Verification runs `SELECT DATABASE()`, an `information_schema` table count, the RLS registry read, and reports `realtime_provisioned`.
60
+
61
+ ## Danger: same-name create against a soft-deleted tenant
62
+
63
+ The platform's create path resurrects a `status='deleted'` row and **regenerates its `jwt_secret` and both API keys**. That silently invalidates whoever held the old keys and brings any surviving data back under new credentials. The CLI therefore refuses a colliding id by default:
64
+
65
+ - live tenant with the same id → refused, tells you to use `tenant keys <id>` instead
66
+ - soft-deleted tenant → refused unless you pass `--reactivate`
67
+
68
+ Never retry a failed create with a different flag combination hoping it will pass. Read the message.
69
+
70
+ ## Tenant access bundle (handing keys to another human)
71
+
72
+ ```bash
73
+ pxbaas tenant create acme --bundle ./acme.pxbaas.env # new tenant + bundle
74
+ pxbaas tenant keys acme --bundle ./acme.pxbaas.env # existing tenant + bundle
75
+ pxbaas tenant keys acme --as-profile # bind into this machine's keyring
76
+ ```
77
+
78
+ Admin credentials come from the active profile (`tenant login`), not from a per-command flag.
79
+
80
+ `--bundle` writes `0600` and refuses an existing path unless `--overwrite`. The file contains exactly:
81
+
82
+ ```
83
+ MULTI_TENANT_MODE=true
84
+ SUPABASE_URL=<gateway>
85
+ DEFAULT_TENANT_ID=<tenant>
86
+ TENANT_ANON_KEY=<key>
87
+ TENANT_SERVICE_ROLE_KEY=<key>
88
+ ```
89
+
90
+ Built by explicit field list, so `jwt_secret`, `DASHBOARD_*` and `GLOBAL_*` cannot appear in it. It is directly consumable by the receiving user:
91
+
92
+ ```bash
93
+ pxbaas profile import ./acme.pxbaas.env --name acme --json
94
+ pxbaas whoami --json # expect mode=multi-tenant, tenantId=acme
95
+ pxbaas doctor --json # Tenant Consistency must pass
96
+ pxbaas db tables --json
97
+ ```
98
+
99
+ `create`/`keys` also print a copy-paste handoff note (import steps + both fingerprints) and append a line to `~/.baas/handoffs.json`. The receiver compares those fingerprints against `whoami` to prove nothing was swapped in transit.
100
+
101
+ **Treat a bundle as a long-lived plaintext credential.** Keys are signed with `exp ≈ 10 years` and the platform has **no revocation**: `PATCH /api/platform/tenants/[id]` does not accept `jwt_secret`, and HS256 tokens carry no `jti`, so re-signing keys does not invalidate old ones. Consequences for you as agent:
102
+
103
+ - never cat/read a bundle file into the conversation, never echo its contents, never commit it (add `*.pxbaas.env` to ignores)
104
+ - do not upload it to chat, ticket attachments, or shared drives; that channel choice is the operator's job
105
+ - tell the operator that distributing a bundle = distributing an irrevocable admin-grade credential for that tenant
106
+
107
+ ## Gap guidance from `profile import`
108
+
109
+ When a multi-tenant env file lacks tenant-level keys, `profile import` now prints a diagnosis (which variables exist, whether each JWT carries `tenant_id`, whether `DASHBOARD_*` is present), states `Nothing was created and nothing was imported`, and routes to one of:
110
+
111
+ - **admin path** — when the file's dashboard credentials actually open the admin channel: `tenant login` → `tenant ls` → `tenant keys <id> --as-profile|--bundle`
112
+ - **request path** — otherwise: a checklist to send to the operator (tenant id + both keys, plus how to prove a key is tenant-level: its JWT payload must contain `tenant_id`)
113
+
114
+ The transient probe used for this routing is read-only and does **not** persist the dashboard credentials it borrowed from the file.
115
+
116
+ ## Agent rules for this surface
117
+
118
+ 1. **Never create a tenant on someone else's behalf without explicit confirmation** — it provisions a database, a MySQL account, GRANTs, a storage bucket and quota consumption.
119
+ 2. **Prefer `tenant keys <id>` over `tenant create`**: most "I have no key" cases are an existing tenant whose key was never handed over. Run `tenant ls` first.
120
+ 3. `tenant repair-keys` is the safe fix for a tenant visible with `hasAnonKey=false`; it is idempotent and re-signs from the existing `jwt_secret`. Do not present it as rotation.
121
+ 4. Do not run `tenant create` with `--reactivate` unless the user has read the resurrection warning above.
122
+ 5. There is no `suspend` / `resume` / `delete` / `quotas` in this surface yet — do not invent them; use Studio for tenant lifecycle beyond create.
@@ -0,0 +1,76 @@
1
+ # Troubleshooting (doctor / status + error triage)
2
+
3
+ When the agent hits connection, auth, or permission problems, triage in this order — do not blindly retry or work around.
4
+
5
+ ## Step 1: doctor self-check
6
+
7
+ ```bash
8
+ pxbaas doctor --json
9
+ ```
10
+
11
+ `doctor` runs 5 checks; each returns `status` (pass/warn/fail/skip), `detail`, `duration`, and `remediation` (a fix hint):
12
+
13
+ | Check | Meaning |
14
+ |---|---|
15
+ | **Endpoint Connectivity** | is the endpoint reachable (HTTP) |
16
+ | **JWT Signature** | are the anon/service key JWTs well-formed and valid |
17
+ | **JWT Expiry** | are the JWTs expired |
18
+ | **Tenant Consistency** | in multi-tenant mode, does the key `tenant_id` match the profile (skip in single-tenant) |
19
+ | **Permissions** | are anon / service_role usable |
20
+
21
+ **Output (`--json`):**
22
+ ```json
23
+ {
24
+ "profile": "local",
25
+ "checks": [
26
+ { "name": "Endpoint Connectivity", "status": "pass", "detail": "HTTP 200", "duration": 42 },
27
+ { "name": "JWT Expiry", "status": "warn", "detail": "anon key expires in 3 days", "duration": 1, "remediation": "Re-login before expiry" }
28
+ ],
29
+ "summary": { "pass": 4, "warn": 1, "fail": 0, "skip": 0 }
30
+ }
31
+ ```
32
+
33
+ - Any `fail` → the process exits non-zero. **Read each `remediation` first** and follow it.
34
+ - Do not continue with data-plane commands while a check is failing — fix the root cause first.
35
+
36
+ ## Step 2: confirm capability URLs and identity
37
+
38
+ ```bash
39
+ pxbaas status --json # capability URLs: verify rest/auth/storage/functions/meta addresses
40
+ pxbaas whoami --json # verify profile / tenant / role / key fingerprints
41
+ ```
42
+
43
+ ## Common error triage
44
+
45
+ | Symptom | Likely cause | Action |
46
+ |---|---|---|
47
+ | `No active profile. Use "pxbaas login"` | not logged in / no profile set | **ask the user to run `pxbaas login`** (agents cannot) |
48
+ | `No credentials found for profile: <name>` | no credentials in keyring for that profile | same — ask the user to re-login that profile |
49
+ | **401 Unauthorized** | invalid/expired JWT, missing apikey | check doctor's JWT Signature / Expiry / Permissions; if expired, ask the user to re-login |
50
+ | **403 Forbidden** | insufficient role or RLS denial | confirm whether `service_role` is needed; `data`/`rest` default to anon and are RLS-constrained; do not escalate blindly — explain to the user |
51
+ | **404 Not Found** | table/function/object/bucket does not exist, or wrong capability URL | verify with `db tables` / `functions list` / `storage ls` / `status` |
52
+ | **429 Too Many Requests** | rate limit (esp. Edge Function quotas) | back off and retry slowly; explain the quota to the user; do not hammer |
53
+ | tenant-related failure | key `tenant_id` ≠ profile | check doctor's Tenant Consistency; never mix credentials across profiles |
54
+ | `tenant_suspended` (403) / `tenant_deleted` (410) | tenant not active | report to the user; this is a platform-side state, not fixable via the CLI |
55
+ | `functions deploy` 400 / parse error | multipart `metadata` field format (see edge-functions.md known pitfall) | check whether it is a driver-layer issue; consult the platform Management API |
56
+ | MT: `/api/platform/*` 401 | that Studio surface takes **dashboard Basic Auth**, not the global service key | `-u $DASHBOARD_USERNAME:$DASHBOARD_PASSWORD` (multi-tenant-apps.md §1) |
57
+ | MT: `/pg/query` 401 | it takes the **global** service key as both `Authorization` and `apikey`; Basic Auth and tenant keys are rejected | switch credential tier |
58
+ | MT: `403 Global credentials cannot access tenant resources` | global key used on a tenant-scoped endpoint | use that tenant's own anon/service key |
59
+ | MT: `403 Token tenant mismatch` | the key's `tenant_id` claim ≠ `X-Tenant-Id` | align them; never cross tenants |
60
+ | MT: `db query` 401 on a tenant profile | obsolete workaround — MT SQL no longer goes through `/pg/query`; something is wrong with the profile or the key | check `pxbaas doctor --json` (Tenant Consistency, JWT Expiry) and re-login with that tenant's per-tenant keys; `/pg/query` stays platform-admin only |
61
+ | `profile import` refuses a MT env file ("lacks tenant-level credentials") | the file only carries platform-global keys (JWT has no `tenant_id`) | follow the printed route: operator → `tenant login` then `tenant keys <id> --as-profile`; non-operator → send the printed checklist to the operator. Never create a tenant to work around a missing key |
62
+ | `tenant login` says "Not saved. ... could not open the tenant admin channel" | wrong credentials for the admin plane, or endpoint is not the Kong gateway | use the gateway URL (typically :8000); dashboard Basic creds are required — an API key never works here |
63
+ | `tenant probe` → `metadata-unreadable` | the credentials are fine but tenant metadata fails to load (schema drift on long-lived instances) | do not report "no tenants"; escalate to the platform operator |
64
+ | MT: RLS policies registered but have **no effect** | wrong `table_schema` (must be the tenant DB name, not `public`), or written into the tenant DB's unread `_mysqlrest_rls_*` copies | rewrite into the central `supabase._mysqlrest_rls_*`, then verify behaviourally (anon read / forged-owner write → 403) |
65
+ | `app publish` → `APP_MANIFEST_INVALID` | manifest `name` ≠ mount `appName`, or a v1 `mpa` manifest on a schemaVersion-2-only instance | align `name`; migrate the manifest (pxbaas-app-spec `deploy-options.md`) |
66
+ | `app publish` → `APP_SSR_PROBE_FAILED` | **platform-side**: cold tenant sandbox vs the probe budget (heavy `npm:` graph, ≈60 s) | warm the sandbox with a legacy function, then retry publish 3–5× |
67
+ | `app publish` → `APP_ARTIFACT_INVALID: Candidate is already active` | idempotent re-run of the same candidate | treat as success |
68
+
69
+ ## Principles
70
+
71
+ - **Diagnose before acting:** on any failure run `doctor --json` first and read `remediation`.
72
+ - **Never self-rescue auth issues:** for login/credential failures, always ask the user to handle it — do not attempt `login`, do not request plaintext keys.
73
+ - **Report faithfully:** distinguish permission / network / rate-limit / server errors; do not lump 404 as "resource missing" or treat 403 as 401.
74
+ - **Use `--debug`:** reproduce with the global `--debug` flag for more context and share the key output with the user.
75
+
76
+ > Full error-code table and multi-tenant status codes: fetch `${endpoint}/docs` — see api-reference.md.
@@ -0,0 +1,55 @@
1
+ // {{APP_NAME}} — headless JSON backend Edge Function (Deno.serve) with ?action= routing.
2
+ //
3
+ // Spec: follow the `pxbaas-app-spec` skill — `pxbaas app deploy` lints this file against it.
4
+ //
5
+ // Deploy with: pxbaas app deploy {{SLUG}}
6
+ // Base URL: {endpoint}/functions/v1/{{SLUG}}
7
+ // Examples: GET ?action=health -> { ok, now }
8
+ // POST ?action=echo -> echoes the JSON body
9
+ //
10
+ // Guardrails: CORS enabled; JSON-only; no root-absolute asset refs (none here).
11
+ // Extend the switch below with your own actions. Keep secrets out of a
12
+ // verify_jwt=false function (it is publicly reachable).
13
+
14
+ function corsHeaders(): Record<string, string> {
15
+ return {
16
+ "Access-Control-Allow-Origin": "*",
17
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
18
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, X-Tenant-Id",
19
+ };
20
+ }
21
+
22
+ function json(body: unknown, status = 200): Response {
23
+ return new Response(JSON.stringify(body), {
24
+ status,
25
+ headers: { "Content-Type": "application/json", ...corsHeaders() },
26
+ });
27
+ }
28
+
29
+ Deno.serve(async (req: Request) => {
30
+ const url = new URL(req.url);
31
+
32
+ if (req.method === "OPTIONS") {
33
+ return new Response(null, { status: 204, headers: corsHeaders() });
34
+ }
35
+
36
+ const action = url.searchParams.get("action") ?? "health";
37
+
38
+ switch (action) {
39
+ case "health":
40
+ return json({ ok: true, app: "{{APP_NAME}}", now: new Date().toISOString() });
41
+
42
+ case "echo": {
43
+ let body: unknown = null;
44
+ try {
45
+ body = await req.json();
46
+ } catch {
47
+ return json({ ok: false, error: "invalid JSON body" }, 400);
48
+ }
49
+ return json({ ok: true, echo: body });
50
+ }
51
+
52
+ default:
53
+ return json({ ok: false, error: `unknown action: ${action}` }, 400);
54
+ }
55
+ });
@@ -0,0 +1,81 @@
1
+ # {{APP_NAME}} — 前后端分离(CSR + BFF)参考模板
2
+
3
+ **形态**:7 个真实静态页(`index/article/blogger/login/signup/admin/profile`)+ `style.css` + `blog.js`,由 proxy 静态分支直发(**不启 worker**);所有数据/认证走同源 `./api/*` 打到 Deno BFF worker,worker 再转发到实例**内网**的 `/rest/v1`、`/auth/v1`。
4
+ **会话模型**:`proxy-injected` —— 浏览器不直连 Supabase;登录经平台 `./_session/login` 建立 per-mount HttpOnly cookie,proxy 校验后向 worker 注入 `X-App-User-*`,worker 用 `X-App-User-Token` 作为下游 `Authorization`,由 GoTrue/MysqlREST 的 RLS 做最终裁决。
5
+
6
+ ## 为什么 BFF 不能靠"透传浏览器 Authorization"
7
+
8
+ `/apps/{mount}/api/*` 走 `TRUSTED_INTERNAL` 凭据策略,proxy 会剥掉客户端的
9
+ `authorization / cookie / apikey / x-api-key`。实测判据:同一 token 直连公网 `/auth/v1/user` 得 200,
10
+ 经 BFF 透传却得 `403 bad_jwt: invalid claim: missing sub claim` —— 说明中间层剥了头,worker 恒回落 anon。
11
+ 因此本模板的身份来源**只有**注入头:
12
+
13
+ ```ts
14
+ const injected = req.headers.get("x-app-user-token") || ""; // 方案 C
15
+ const userAuth = injected || incomingAuth; // 兼容 /functions 入口的透传
16
+ ```
17
+
18
+ 前端侧对应:登录/注册成功后追加 `fetch("./_session/login")` 建立 cookie,`clearToken()` 时
19
+ `fetch("./_session/logout", { keepalive: true })`(`keepalive` 保证跳转后请求仍能完成);
20
+ 端点不存在时静默忽略,以兼容旧实例与 `/functions` 入口。
21
+
22
+ ## 目录
23
+
24
+ ```
25
+ {{APP_NAME}}/
26
+ ├── app.json # schemaVersion 2;fallback=null(页面都是真实静态文件)
27
+ ├── backend/index.ts # BFF worker:/api/health /api/config /api/me /api/rest/* /api/auth/*
28
+ ├── frontend/dist/ # 7 个页面 + style.css + blog.js(相对路径引用)
29
+ ├── db/schema.sql # 幂等数据层:posts + 5 条 RLS + 3 篇种子
30
+ └── README.md
31
+ ```
32
+
33
+ `fallback: null` 意味着静态未命中即 404(不落到 worker);BFF 只经 `/api/*` 访问,因此不需要 SSR 探针。
34
+
35
+ ## 部署前必须做的一件事:填 DEPLOY CONFIG
36
+
37
+ `backend/index.ts` 顶部两个常量默认为空;留空时除 `/api/health` 外一律返回
38
+ `503 {"error":"deploy config missing"}`(刻意设计:绝不回落到任何真实实例)。
39
+
40
+ ```ts
41
+ const INTERNAL_BASE = ""; // 目标实例内网基址(公网 host 去掉 http./https. 中缀)
42
+ const ANON_KEY = ""; // 单租户=实例 anon;多租户=该租户 anon
43
+ ```
44
+
45
+ worker 只用 anon 作 Kong 的 `apikey`,并把用户 JWT 原样带给下游;**从不使用 service_role**,
46
+ 因此它拿到的权限不会超过调用者本人。
47
+
48
+ ## 数据层
49
+
50
+ ```bash
51
+ sed 's/__SCHEMA__/public/g' db/schema.sql # 单租户
52
+ sed 's/__SCHEMA__/tenant_acme/g' db/schema.sql # 多租户
53
+ ```
54
+
55
+ RLS 元数据写**中心库** `supabase._mysqlrest_rls_*`,`table_schema` = 上面的 `__SCHEMA__`;写错静默失效。
56
+
57
+ ## 部署
58
+
59
+ ```bash
60
+ pxbaas app deploy {{APP_NAME}} --dir . # appName 必须与 app.json 的 name 一致
61
+ ```
62
+
63
+ 走 curl Management API 时:静态文件必须用显式嵌套 `filename=frontend/dist/<name>`,否则路径被压平导致 publish 报缺文件。
64
+
65
+ ## 验收清单
66
+
67
+ | # | 检查 | 期望 |
68
+ |---|---|---|
69
+ | 1 | 7 个页面 + `style.css` + `blog.js` | 全 200,content-type 正确(`text/css` / `text/javascript`) |
70
+ | 2 | `GET /apps/{m}/api/health` | `{"ok":true,"via":"bff"}`(未配置 DEPLOY CONFIG 时也应可用) |
71
+ | 3 | `GET /apps/{m}/api/config` | 返回的 `anon` **等于目标实例/租户的 anon**(证明参数化生效,没串到别的实例) |
72
+ | 4 | `GET /apps/{m}/api/rest/posts` 匿名 | 只见 published |
73
+ | 5 | `POST /apps/{m}/_session/login` | 200 + `Set-Cookie: pxbaas-app-session; Path=/apps/{m}/; HttpOnly` |
74
+ | 6 | **只带 cookie**(不带 Authorization)`GET /api/me` | 返回完整用户对象 |
75
+ | 7 | 带 cookie 发文(`author_id`=自己 / 他人) | 201 / **403 PGRST302** |
76
+ | 8 | 带 cookie 读 posts | published + 自己的 draft |
77
+ | 9 | `POST /api/../_session/logout` 后 `GET /api/me` | `{"user":null}` |
78
+ | 10 | 双用户交替请求 | 各自只见自己(无共享 jar 串味) |
79
+
80
+ > 注意:`logout` 页靠内联 JS 调 `./_session/logout`,**curl 不执行 JS**,用 curl 复验时要直接打 proxy 的退出端点。
81
+ > 另:在容器内以 `Host: 127.0.0.1:8000` 打 `/apps/*` 会得到 `APP_HOST_REJECTED`,属预期;验证请从公网侧发起。
@@ -0,0 +1,168 @@
1
+ // mpa-blog BFF worker (front/back separated).
2
+ //
3
+ // browser ──./api/*──> THIS worker ──internal DNS──> /rest/v1 + /auth/v1 ──> db / GoTrue
4
+ //
5
+ // The sandbox CANNOT reach the instance's public SUPABASE_URL (egress-filtered), but CAN reach
6
+ // the internal/private DNS entry (pxsp-<id>.pxbaas...:8000 -> 10.x VPC VIP). So the worker proxies
7
+ // browser calls to the internal Kong base. Credential model = JWT PASSTHROUGH: the browser's
8
+ // Authorization header (GoTrue user JWT) is forwarded verbatim so MysqlREST RLS and GoTrue enforce
9
+ // per-user rules server-side. The worker injects only the global `apikey` (anon) for Kong key-auth
10
+ // and NEVER uses service_role, so it gains no privilege beyond the calling user.
11
+ //
12
+ // Static pages/assets are served by the edge-function-proxy static branch (app.json mode=mpa);
13
+ // this worker only handles /api/*.
14
+
15
+ const CORS: Record<string, string> = {
16
+ "Access-Control-Allow-Origin": "*",
17
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, PUT, DELETE, OPTIONS",
18
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, Prefer, Range, X-Tenant-Id",
19
+ "Access-Control-Expose-Headers": "Content-Range, Prefer, Location",
20
+ };
21
+
22
+ function json(body: unknown, status = 200): Response {
23
+ return new Response(JSON.stringify(body), {
24
+ status,
25
+ headers: { "Content-Type": "application/json; charset=utf-8", ...CORS },
26
+ });
27
+ }
28
+
29
+ // ─── DEPLOY CONFIG(平台注入优先;缺失则除 /api/health 外一律 503,绝不回落到任何真实实例)───
30
+ // env 契约(MR #30497903 起):平台按三键白名单向 App candidate 注入 SUPABASE_INTERNAL_URL /
31
+ // SUPABASE_ANON_KEY / SUPABASE_URL,spec-lint 的 deno-env-not-allowed 放行这三键。本模板直读
32
+ // SUPABASE_INTERNAL_URL(数据面唯一可达基址)与 SUPABASE_ANON_KEY(数据面凭据),无需手填明文 key。
33
+ // 若目标实例 sandbox 镜像尚未含该 MR,两值为空 → configError 明确 503,绝不静默连到别的实例/租户。
34
+ // INTERNAL_BASE —— 平台注入的内网基址(公网 host 去掉 http./https. 中缀);公网 VIP 在沙箱内因
35
+ // egress 过滤不可达(非 hairpin),切勿改填公网地址。
36
+ // ANON_KEY —— 单租户=实例 anon key;多租户=**该租户** anon key(自带 tenant_id claim,
37
+ // REST/Auth 据此路由,无需 X-Tenant-Id)。anon 是公开值;service_role 严禁入 worker。
38
+ const INTERNAL_BASE = Deno.env.get("SUPABASE_INTERNAL_URL") ?? "";
39
+ const ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? "";
40
+
41
+ function configError(): string | null {
42
+ if (!INTERNAL_BASE) return "SUPABASE_INTERNAL_URL not injected: the sandbox image predates MR #30497903; the platform must inject the internal base before this BFF can reach the data plane";
43
+ if (!ANON_KEY) return "SUPABASE_ANON_KEY not injected: the sandbox image predates MR #30497903; the platform must inject the anon key";
44
+ return null;
45
+ }
46
+
47
+ function anonKey(): string {
48
+ return ANON_KEY;
49
+ }
50
+
51
+ /** 服务端转发唯一合法通道:内网基址(见上方 DEPLOY CONFIG)。 */
52
+ function internalBase(): string {
53
+ return INTERNAL_BASE.replace(/\/+$/, "");
54
+ }
55
+
56
+ /** Extract the segments after the "api" marker, robust to mount/dispatch prefix differences. */
57
+ function apiTail(pathname: string): string[] {
58
+ // Mount/webapp dispatch hands the worker the candidate-slug-prefixed logical path
59
+ // (/{app_<uuid>_r_<rev>}/...); strip that first, then locate the "api" marker.
60
+ const norm = pathname.replace(/^\/app_[^/]+/, "") || pathname;
61
+ const segs = norm.split("/").filter(Boolean);
62
+ const i = segs.lastIndexOf("api");
63
+ if (i >= 0) return segs.slice(i + 1);
64
+ return segs; // fallback: whole path
65
+ }
66
+
67
+ async function readBody(req: Request): Promise<BodyInit | null> {
68
+ if (req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS") return null;
69
+ try {
70
+ return await req.arrayBuffer();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /** Forward a request to the internal Kong base, injecting apikey and passing through the user's JWT. */
77
+ async function proxy(req: Request, internalPath: string, search: string): Promise<Response> {
78
+ const base = internalBase();
79
+ const url = base + internalPath + (search || "");
80
+ const anon = anonKey();
81
+ // /apps/{mount}/api/* runs under TRUSTED_INTERNAL, which strips the browser's
82
+ // Authorization/Cookie before the worker ever sees them, so plain JWT passthrough
83
+ // cannot work on that entry. There the proxy validates the per-mount session
84
+ // cookie and injects the trusted X-App-User-Token instead. Prefer the injected
85
+ // token, fall back to a passed-through Authorization (/functions entry), then to
86
+ // anon for public reads.
87
+ const injected = req.headers.get("x-app-user-token") || "";
88
+ const incomingAuth = req.headers.get("Authorization") || "";
89
+ const userAuth = injected || incomingAuth;
90
+ const authValue = userAuth
91
+ ? (/^Bearer\s+/i.test(userAuth) ? userAuth : `Bearer ${userAuth}`)
92
+ : (anon ? `Bearer ${anon}` : "");
93
+
94
+ const headers: Record<string, string> = {
95
+ "apikey": anon,
96
+ // Forward the caller's JWT (RLS/GoTrue act on it); fall back to anon for public reads.
97
+ "Authorization": authValue,
98
+ "Accept": req.headers.get("Accept") || "application/json",
99
+ };
100
+ const ct = req.headers.get("Content-Type");
101
+ if (ct) headers["Content-Type"] = ct;
102
+ const prefer = req.headers.get("Prefer");
103
+ if (prefer) headers["Prefer"] = prefer;
104
+
105
+ const body = await readBody(req);
106
+ let upstream: Response;
107
+ try {
108
+ upstream = await fetch(url, { method: req.method, headers, body });
109
+ } catch (e) {
110
+ const msg = String((e as any)?.message ?? e);
111
+ return json({ error: "upstream_unreachable", detail: msg, internalBase: base, internalPath }, 502);
112
+ }
113
+
114
+ const respHeaders: Record<string, string> = { ...CORS };
115
+ const rct = upstream.headers.get("Content-Type");
116
+ if (rct) respHeaders["Content-Type"] = rct;
117
+ for (const h of ["Content-Range", "Prefer", "Location"]) {
118
+ const v = upstream.headers.get(h);
119
+ if (v) respHeaders[h] = v;
120
+ }
121
+ // 204/205/304 are "null body" statuses: constructing a Response with a body throws.
122
+ // PostgREST returns 204 for DELETE/PATCH without Prefer: return=representation.
123
+ if (upstream.status === 204 || upstream.status === 205 || upstream.status === 304) {
124
+ return new Response(null, { status: upstream.status, headers: respHeaders });
125
+ }
126
+ const payload = await upstream.arrayBuffer().catch(() => new ArrayBuffer(0));
127
+ return new Response(payload, { status: upstream.status, headers: respHeaders });
128
+ }
129
+
130
+ Deno.serve(async (req: Request): Promise<Response> => {
131
+ if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS });
132
+
133
+ const url = new URL(req.url);
134
+ const tail = apiTail(url.pathname);
135
+ const head = (tail[0] || "").toLowerCase();
136
+ const search = url.search || "";
137
+
138
+ // ---- worker-local endpoints ----
139
+ if (head === "health") return json({ ok: true, now: new Date().toISOString(), via: "bff" });
140
+ const cfgErr = configError();
141
+ if (cfgErr) return json({ error: "deploy config missing", detail: cfgErr }, 503);
142
+ if (head === "config") return json({ anon: anonKey(), mode: "bff" });
143
+ if (head === "me") {
144
+ // Server-side session check via internal GoTrue, using the caller's JWT.
145
+ const r = await proxy(req, "/auth/v1/user", "");
146
+ if (r.status !== 200) return json({ user: null });
147
+ try {
148
+ const u = await r.clone().json();
149
+ return json({ user: u && u.id ? u : null });
150
+ } catch {
151
+ return json({ user: null });
152
+ }
153
+ }
154
+
155
+ // ---- generic authenticated proxies ----
156
+ // /api/rest/<rest...> -> internal /rest/v1/<rest...>
157
+ if (head === "rest") {
158
+ const sub = tail.slice(1).join("/");
159
+ return proxy(req, "/rest/v1/" + sub, search);
160
+ }
161
+ // /api/auth/<auth...> -> internal /auth/v1/<auth...>
162
+ if (head === "auth") {
163
+ const sub = tail.slice(1).join("/");
164
+ return proxy(req, "/auth/v1/" + sub, search);
165
+ }
166
+
167
+ return json({ error: "not found", path: url.pathname, tail }, 404);
168
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "$comment": "Declarative RLS for the CSR/BFF blog webapp. Applied by `pxbaas app db-init` (single-tenant AND multi-tenant). table_schema is NEVER declared here: single-tenant binds it to --schema (default public); multi-tenant binds it server-side to the tenant db.",
3
+ "tables": [
4
+ { "table": "posts", "rlsEnabled": true, "rlsForced": false }
5
+ ],
6
+ "policies": [
7
+ { "table": "posts", "name": "posts_select_anon", "role": "anon", "command": "SELECT", "using": "`status` = 'published'", "check": null, "permissive": true },
8
+ { "table": "posts", "name": "posts_select_auth", "role": "authenticated", "command": "SELECT", "using": "`status` = 'published' OR author_id = {{request.jwt.claim.sub}}", "check": null, "permissive": true },
9
+ { "table": "posts", "name": "posts_insert_auth", "role": "authenticated", "command": "INSERT", "using": null, "check": "author_id = {{request.jwt.claim.sub}}", "permissive": true },
10
+ { "table": "posts", "name": "posts_update_auth", "role": "authenticated", "command": "UPDATE", "using": "author_id = {{request.jwt.claim.sub}}", "check": "author_id = {{request.jwt.claim.sub}}", "permissive": true },
11
+ { "table": "posts", "name": "posts_delete_auth", "role": "authenticated", "command": "DELETE", "using": "author_id = {{request.jwt.claim.sub}}", "check": null, "permissive": true }
12
+ ]
13
+ }
@@ -0,0 +1,49 @@
1
+ -- =============================================================================
2
+ -- multi-page-blog-csr — 数据层(幂等,可重复执行)
3
+ --
4
+ -- 用法:把 __SCHEMA__ 替换为目标库名后逐条执行
5
+ -- · 单租户实例:__SCHEMA__ = public
6
+ -- · 多租户实例:__SCHEMA__ = tenant_<tenant_id>
7
+ -- sed 's/__SCHEMA__/public/g' db/schema.sql
8
+ --
9
+ -- 执行通道(推荐):`pxbaas app db-init <name>` —— 单/多租户通用:
10
+ -- 单租户 __SCHEMA__→public 经 /pg/query;多租户 __SCHEMA__ 去限定经 /api/tenant-ddl
11
+ -- 用 per-tenant service_role key 执行(GRANT 仅本租户库)。本文件只含 DDL + 种子;
12
+ -- RLS 声明在同目录 db/rls.json,由 db-init 一并应用。
13
+ --
14
+ -- ⚠️ RLS 元数据落点(写错静默失效=RLS 关闭):永远写中心库 supabase._mysqlrest_rls_*;
15
+ -- db-init 自动绑定 table_schema(单租户=--schema,多租户=服务端强绑),勿手写库名。
16
+ -- =============================================================================
17
+
18
+ CREATE TABLE IF NOT EXISTS `__SCHEMA__`.`posts` (
19
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
20
+ title VARCHAR(200) NOT NULL,
21
+ content MEDIUMTEXT,
22
+ author_id VARCHAR(64) NOT NULL,
23
+ author_name VARCHAR(100) DEFAULT '',
24
+ status VARCHAR(16) DEFAULT 'draft',
25
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
26
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
27
+ );
28
+
29
+ -- ---- RLS 注册与策略:声明在同目录 db/rls.json,由 `pxbaas app db-init` 应用 ----
30
+ -- anon 只读 published;authenticated 读 published 或本人;写/改/删限本人(author_id = jwt.sub)。
31
+
32
+ -- ---- 种子文章(3 篇 published,幂等)----
33
+ INSERT INTO `__SCHEMA__`.`posts` (title, content, author_id, author_name, status)
34
+ SELECT * FROM (
35
+ SELECT '前后端分离的博客长什么样' a,
36
+ '静态多页 + BFF worker:浏览器只跟自己同源的 ./api/* 说话,worker 再转发到内网 REST/Auth,凭据由平台注入的身份头承载,RLS 在服务端兜底。' b,
37
+ 'demo-author' c, '架构笔记' d, 'published' e
38
+ UNION ALL SELECT '为什么浏览器不该直连数据库网关',
39
+ '直连意味着 anon key 与用户 JWT 都暴露在页面里,跨域与凭据边界都变复杂;走同源 BFF 后前端只需相对路径,权限判定全部落在服务端。',
40
+ 'demo-author', '安全', 'published'
41
+ UNION ALL SELECT 'CSR 与 SSR 的取舍',
42
+ 'CSR 首屏靠 JS 拉数据,利于交互复杂、SEO 要求不高的页面;SSR 首屏即内容,利于分享与收录。两者可共存于同一套数据层与 RLS 策略之上。',
43
+ 'demo-author', '随笔', 'published'
44
+ ) t WHERE NOT EXISTS (SELECT 1 FROM `__SCHEMA__`.`posts` WHERE title='前后端分离的博客长什么样');
45
+
46
+ -- ---- 验收 ----
47
+ -- 1) anon 读 posts → 只见 3 篇 published;
48
+ -- 2) 用户 token 写 posts(author_id=自己)→ 201;伪造他人 author_id → 403 PGRST302;
49
+ -- 3) 用户读 posts → 3 篇 published + 自己的 draft。