@polardbx/pxbaas 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -13
- package/dist/app/local-index.d.ts +70 -0
- package/dist/app/local-index.d.ts.map +1 -0
- package/dist/app/local-index.js +140 -0
- package/dist/app/local-index.js.map +1 -0
- package/dist/app/preflight.d.ts +24 -1
- package/dist/app/preflight.d.ts.map +1 -1
- package/dist/app/preflight.js +47 -11
- package/dist/app/preflight.js.map +1 -1
- package/dist/app/spec-lint.d.ts.map +1 -1
- package/dist/app/spec-lint.js +16 -0
- package/dist/app/spec-lint.js.map +1 -1
- package/dist/cli/root.d.ts.map +1 -1
- package/dist/cli/root.js +3 -0
- package/dist/cli/root.js.map +1 -1
- package/dist/commands/app/index.d.ts.map +1 -1
- package/dist/commands/app/index.js +428 -44
- package/dist/commands/app/index.js.map +1 -1
- package/dist/commands/completion/index.d.ts.map +1 -1
- package/dist/commands/completion/index.js +5 -1
- package/dist/commands/completion/index.js.map +1 -1
- package/dist/commands/data/index.d.ts +52 -0
- package/dist/commands/data/index.d.ts.map +1 -1
- package/dist/commands/data/index.js +201 -39
- package/dist/commands/data/index.js.map +1 -1
- package/dist/commands/db/index.d.ts +14 -0
- package/dist/commands/db/index.d.ts.map +1 -1
- package/dist/commands/db/index.js +151 -40
- package/dist/commands/db/index.js.map +1 -1
- package/dist/commands/functions/new.d.ts +4 -0
- package/dist/commands/functions/new.d.ts.map +1 -1
- package/dist/commands/functions/new.js +24 -9
- package/dist/commands/functions/new.js.map +1 -1
- package/dist/commands/gen/index.d.ts.map +1 -1
- package/dist/commands/gen/index.js +13 -10
- package/dist/commands/gen/index.js.map +1 -1
- package/dist/commands/login/index.d.ts.map +1 -1
- package/dist/commands/login/index.js +15 -30
- package/dist/commands/login/index.js.map +1 -1
- package/dist/commands/profile/import-env.d.ts +7 -2
- package/dist/commands/profile/import-env.d.ts.map +1 -1
- package/dist/commands/profile/import-env.js +141 -16
- package/dist/commands/profile/import-env.js.map +1 -1
- package/dist/commands/profile/index.d.ts.map +1 -1
- package/dist/commands/profile/index.js +13 -1
- package/dist/commands/profile/index.js.map +1 -1
- package/dist/commands/storage/index.d.ts.map +1 -1
- package/dist/commands/storage/index.js +209 -7
- package/dist/commands/storage/index.js.map +1 -1
- package/dist/commands/tenant/index.d.ts +19 -0
- package/dist/commands/tenant/index.d.ts.map +1 -0
- package/dist/commands/tenant/index.js +662 -0
- package/dist/commands/tenant/index.js.map +1 -0
- package/dist/commands/whoami/index.js +19 -0
- package/dist/commands/whoami/index.js.map +1 -1
- package/dist/credentials/credential-store.d.ts +30 -1
- package/dist/credentials/credential-store.d.ts.map +1 -1
- package/dist/credentials/credential-store.js +114 -12
- package/dist/credentials/credential-store.js.map +1 -1
- package/dist/credentials/env-file.d.ts +48 -7
- package/dist/credentials/env-file.d.ts.map +1 -1
- package/dist/credentials/env-file.js +91 -16
- package/dist/credentials/env-file.js.map +1 -1
- package/dist/credentials/jwt-introspect.d.ts +7 -0
- package/dist/credentials/jwt-introspect.d.ts.map +1 -1
- package/dist/credentials/jwt-introspect.js.map +1 -1
- package/dist/credentials/masker.d.ts +3 -0
- package/dist/credentials/masker.d.ts.map +1 -1
- package/dist/credentials/masker.js +3 -0
- package/dist/credentials/masker.js.map +1 -1
- package/dist/credentials/profile.d.ts +12 -0
- package/dist/credentials/profile.d.ts.map +1 -1
- package/dist/credentials/profile.js +23 -0
- package/dist/credentials/profile.js.map +1 -1
- package/dist/credentials/tty-guard.d.ts +10 -0
- package/dist/credentials/tty-guard.d.ts.map +1 -1
- package/dist/credentials/tty-guard.js +32 -0
- package/dist/credentials/tty-guard.js.map +1 -1
- package/dist/db/introspect.d.ts +54 -0
- package/dist/db/introspect.d.ts.map +1 -0
- package/dist/db/introspect.js +140 -0
- package/dist/db/introspect.js.map +1 -0
- package/dist/main.js +21 -2
- package/dist/main.js.map +1 -1
- package/dist/tenant/bundle.d.ts +66 -0
- package/dist/tenant/bundle.d.ts.map +1 -0
- package/dist/tenant/bundle.js +132 -0
- package/dist/tenant/bundle.js.map +1 -0
- package/dist/tenant/guide.d.ts +50 -0
- package/dist/tenant/guide.d.ts.map +1 -0
- package/dist/tenant/guide.js +152 -0
- package/dist/tenant/guide.js.map +1 -0
- package/dist/tenant/probe.d.ts +65 -0
- package/dist/tenant/probe.d.ts.map +1 -0
- package/dist/tenant/probe.js +159 -0
- package/dist/tenant/probe.js.map +1 -0
- package/dist/tenant/verify.d.ts +37 -0
- package/dist/tenant/verify.d.ts.map +1 -0
- package/dist/tenant/verify.js +132 -0
- package/dist/tenant/verify.js.map +1 -0
- package/dist/version.d.ts +29 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +65 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -2
- package/skills/pxbaas-app-spec/references/runtime-contract.md +2 -2
- package/skills/pxbaas-cli/SKILL.md +14 -5
- package/skills/pxbaas-cli/references/auth-and-credentials.md +19 -3
- package/skills/pxbaas-cli/references/data-postgrest.md +19 -3
- package/skills/pxbaas-cli/references/db-and-meta.md +5 -0
- package/skills/pxbaas-cli/references/safety-and-two-phase.md +42 -13
- package/skills/pxbaas-cli/references/tenant-admin.md +122 -0
- package/skills/pxbaas-cli/references/troubleshooting.md +4 -1
- package/templates/webapp/react-vite-hono/backend/index.ts +21 -4
- package/templates/webapp/ssr-hono/backend/index.ts +50 -31
|
@@ -55,10 +55,11 @@ pxbaas data update <table> --values '{"done":true}' --filter "id=eq.1" --json
|
|
|
55
55
|
pxbaas data delete <table> --filter "id=eq.1" --json
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
- **Flags:** `--filter <f>` (**required
|
|
58
|
+
- **Flags:** `--filter <f>` (**required**, repeatable to combine) · `--yes` · `--json`
|
|
59
59
|
- **Output (`--json`):** array of deleted rows.
|
|
60
|
-
-
|
|
61
|
-
|
|
60
|
+
- **Gated in the CLI:** it first runs a read-only preview of the affected row count and prints the exact request URL, then requires authorization — an interactive confirm, or `--yes` for non-interactive use. Nothing is deleted without it, and a refused run still carries a `preview` object in `--json` so an audit consumer can see what was declined. A failed preview also blocks the delete.
|
|
61
|
+
- `--filter` is mandatory because an unfiltered delete would take the whole table.
|
|
62
|
+
- Still preview the target yourself first (`data select` with the SAME filter) and get the user's explicit consent before passing `--yes`.
|
|
62
63
|
|
|
63
64
|
## rest — raw HTTP passthrough (debugging)
|
|
64
65
|
|
|
@@ -83,3 +84,18 @@ pxbaas rest POST "/todos" --data '{"title":"x"}' --role anon --json
|
|
|
83
84
|
```
|
|
84
85
|
|
|
85
86
|
> REST endpoint details, embedding/RPC constraints: fetch `${endpoint}/docs` — see api-reference.md.
|
|
87
|
+
|
|
88
|
+
## Schema cache after DDL
|
|
89
|
+
|
|
90
|
+
The REST layer (`/rest/v1`) caches each table's column list. After a DDL change
|
|
91
|
+
(`ALTER TABLE ... ADD/DROP/MODIFY COLUMN`, or `CREATE`/`DROP TABLE`) the cache
|
|
92
|
+
re-introspects automatically: immediately where the platform wires DDL to a
|
|
93
|
+
cache reload, otherwise within one refresh interval (~30s). Until then:
|
|
94
|
+
|
|
95
|
+
- `select=*` (and `Prefer: return=representation` bodies) may still omit a
|
|
96
|
+
just-added column. This is a stale column list, not an error — the row is
|
|
97
|
+
returned, only the new field is missing.
|
|
98
|
+
- Workaround: enumerate columns explicitly (`?select=id,room_id,board`) instead
|
|
99
|
+
of `*`, or retry after a few seconds.
|
|
100
|
+
|
|
101
|
+
`pxbaas db add-column` / `db create-table` print a reminder about this.
|
|
@@ -40,6 +40,7 @@ pxbaas db tables --schema public --role service_role --json
|
|
|
40
40
|
|
|
41
41
|
- **Flags:** `--schema <name>` (default `public`) · `--role` (default **service_role**) · `--json`
|
|
42
42
|
- Listing all tables needs the admin view, hence the service_role default.
|
|
43
|
+
- **Multi-tenant:** works — introspection routes through `/api/tenant-ddl` (`information_schema`) with the per-tenant service_role key, so `--schema` is bound to the tenant database. The `schema` field in `--json` reports the database actually queried.
|
|
43
44
|
|
|
44
45
|
## db schema — inspect a table (columns + indexes)
|
|
45
46
|
|
|
@@ -60,6 +61,8 @@ pxbaas db create-table <name> \
|
|
|
60
61
|
|
|
61
62
|
- **Flags:** `--columns <json>` (array of `{name,type,nullable?,primaryKey?}`; omit → default `id bigint PK` + `created_at timestamp`) · `--schema` (default `public`) · `--role` (default service_role) · `--json`
|
|
62
63
|
- ⚠️ **Destructive DDL, no built-in gate:** restate the table name, schema, and column definitions and get explicit user consent before running (see safety-and-two-phase.md).
|
|
64
|
+
- **Multi-tenant:** works on an MT profile — the DDL runs through `/api/tenant-ddl` against the tenant's own database, always as the tenant service_role. `--schema` is ignored (warning prints the effective tenant db) and `--role anon` is ignored.
|
|
65
|
+
- Prints a REST schema-cache reminder: `/rest/v1` caches column lists, so `select=*` may not show the new column for up to ~30s. Enumerate columns explicitly or retry.
|
|
63
66
|
|
|
64
67
|
## db add-column — add a column (destructive DDL)
|
|
65
68
|
|
|
@@ -70,6 +73,8 @@ pxbaas db add-column <table> <column> --type text --nullable --default <value> \
|
|
|
70
73
|
|
|
71
74
|
- **Flags:** `--type <t>` (default `text`) · `--nullable` (default true) · `--default <value>` · `--schema` (default `public`) · `--role` (default service_role) · `--json`
|
|
72
75
|
- ⚠️ **Destructive DDL, no built-in gate:** confirm with the user first.
|
|
76
|
+
- **Multi-tenant:** same as `db create-table` — tenant's own database, `--schema`/`--role anon` ignored with a warning.
|
|
77
|
+
- Restarts apply after the schema cache refreshes; see the cache note under `db create-table`.
|
|
73
78
|
|
|
74
79
|
## gen types — generate type definitions
|
|
75
80
|
|
|
@@ -2,28 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
This is the safety baseline for using `pxbaas`. **The CLI currently has NO built-in confirmation gate**, so the responsibility to confirm falls on YOU (the agent).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Which commands actually gate themselves
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Implication:** do NOT assume the CLI will block a dangerous operation for you. **You are the confirmation gate.**
|
|
7
|
+
Gating landed after this page was first written, so it is now a per-command fact rather
|
|
8
|
+
than a global one. Verified against `--help` output:
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
| Command | Gate |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `db push <file>`, `app db-init <name>` | shows the full plan, then TTY confirm or `--yes`; without authorization **no statement runs** |
|
|
13
|
+
| `data delete <table>` | read-only preview of affected rows + exact request URL, then TTY confirm or `--yes`; `--json` keeps a `preview` field even when refused |
|
|
14
|
+
| `app remove`, `app destroy`, `app share revoke`, `instance release`, `skill remove` | require `--confirm`; without them the command returns a `dry-run` preview |
|
|
15
|
+
| `tenant create` | `--dry-run` plan, then TTY confirm or `--yes`; refuses a colliding tenant id unless `--reactivate` |
|
|
16
|
+
| `tenant repair-keys` | requires `--confirm` |
|
|
17
|
+
|
|
18
|
+
**No gate at all** — you are the confirmation layer for these:
|
|
19
|
+
|
|
20
|
+
| Command | Risk |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `db create-table`, `db add-column` | DDL, executes immediately (help text says so) |
|
|
23
|
+
| `db query "<write/DDL>"` | executes whatever the SQL says |
|
|
24
|
+
| `data insert`, `data update` | writes; `update` requires `--filter` but may still hit many rows |
|
|
25
|
+
| `rest` with POST/PATCH/PUT/DELETE | whatever the method does |
|
|
26
|
+
| `functions deploy`, `functions delete` | publishes/overwrites or deletes a live function, irreversible |
|
|
27
|
+
| `app deploy` | publishes live (preflight + spec lint run, but no interactive confirm) |
|
|
28
|
+
| `storage rm` / `mv` / `cp` overwrite | deletes, moves, or silently overwrites objects |
|
|
29
|
+
| `auth users create` / `delete` / `invite` | mutates users; `invite` sends real email |
|
|
30
|
+
| `profile rm`, `logout`, `tenant logout` | removes local credentials (config, not instance data) |
|
|
31
|
+
|
|
32
|
+
Read-only and always safe: `whoami` `status` `doctor` `profile ls` `tenant probe` `tenant ls`
|
|
33
|
+
`tenant handoffs` `db tables` `db schema` `gen types` `data select` `functions list` `storage ls`
|
|
34
|
+
`auth users list/get` `app list` `app url` `app capabilities` `app templates` `instance list/current`.
|
|
35
|
+
|
|
36
|
+
## Risk classification (what impact to expect)
|
|
12
37
|
|
|
13
38
|
| Command | Risk |
|
|
14
39
|
|---|---|
|
|
15
|
-
| `data delete <table> --filter ...` | deletes rows, irreversible |
|
|
40
|
+
| `data delete <table> --filter ...` | deletes rows, irreversible (gated: preview + `--yes`) |
|
|
16
41
|
| `data insert` / `data update` | writes / mutates data |
|
|
17
|
-
| `db create-table` / `db add-column` | schema change (DDL) |
|
|
42
|
+
| `db create-table` / `db add-column` | schema change (DDL), executes immediately |
|
|
18
43
|
| `db query "<write or DDL SQL>"` | depends on the SQL |
|
|
44
|
+
| `db push` / `app db-init` | DDL + RLS + seed; sequential, stops on first error, **never rolls back** |
|
|
19
45
|
| `functions deploy` | publishes / overwrites a live function |
|
|
20
|
-
| `functions delete <slug>` | deletes a live function, irreversible |
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
46
|
+
| `functions delete <slug>` | deletes a live function, irreversible; also revokes its share links |
|
|
47
|
+
| `app deploy` / `publish` / `rollback` / `unpublish` | changes what is live for users |
|
|
48
|
+
| `app remove` / `app destroy [--purge-db]` | deletes functions + mount; `--purge-db` also drops tables and RLS rows |
|
|
49
|
+
| `storage rm` / `mv` / `cp` (overwrite) | deletes / moves / overwrites objects; remote→remote `cp` is a **move** (source is removed) |
|
|
50
|
+
| `auth users delete` / `create` / `invite` | deletes / creates users; `invite` sends real email |
|
|
23
51
|
| `rest` POST/PATCH/DELETE/PUT | depends on method and path |
|
|
24
|
-
| `profile rm` / `logout` | removes local
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
| `profile rm` / `logout` / `tenant logout` | removes local credentials (also the stored keys, so re-login is required) |
|
|
53
|
+
| `tenant create` | provisions a database, a MySQL account + GRANTs, a storage bucket, RLS tables, Realtime registration; consumes quota |
|
|
54
|
+
| `tenant repair-keys` | re-signs NULL-keyed tenants from the existing `jwt_secret`; idempotent, **not** a rotation |
|
|
55
|
+
| `instance release` | destroys cloud resources and the local profile |
|
|
27
56
|
|
|
28
57
|
## Agent confirmation protocol (for destructive ops)
|
|
29
58
|
|
|
@@ -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.
|
|
@@ -57,7 +57,10 @@ pxbaas whoami --json # verify profile / tenant / role / key fingerprints
|
|
|
57
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
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
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 |
|
|
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 |
|
|
61
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) |
|
|
62
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`) |
|
|
63
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× |
|
|
@@ -10,10 +10,27 @@ app.use("*", async (context, next) => {
|
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
app.options("*", (context) => context.body(null, 204));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
)
|
|
13
|
+
|
|
14
|
+
// Path contract (APP_WORKER_PATH_REWRITE off = current default): the proxy
|
|
15
|
+
// forwards /apps/{mount}/api/<x> to the worker as /<candidateSlug>/<x>, so a
|
|
16
|
+
// literal app.get("/health") never matches at runtime. Normalize the candidate
|
|
17
|
+
// slug prefix away and dispatch on the logical path (same convention as the
|
|
18
|
+
// ssr-shop / ssr-blog / csr-bff templates).
|
|
19
|
+
function norm(path: string): string {
|
|
20
|
+
return path.replace(/^\/app_[^/]+/, "") || path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
app.get("*", (context) => {
|
|
24
|
+
const p = norm(new URL(context.req.url).pathname);
|
|
25
|
+
if (p === "/health" || p === "/api/health") {
|
|
26
|
+
return context.json({ ok: true, app: "{{SLUG}}" });
|
|
27
|
+
}
|
|
28
|
+
if (p === "/hello" || p === "/api/hello") {
|
|
29
|
+
return context.json({ message: "Hello from {{APP_NAME}}" });
|
|
30
|
+
}
|
|
31
|
+
return context.json({ code: "NOT_FOUND", message: "Route not found", path: p }, 404);
|
|
32
|
+
});
|
|
33
|
+
|
|
17
34
|
app.notFound((context) =>
|
|
18
35
|
context.json({ code: "NOT_FOUND", message: "Route not found" }, 404),
|
|
19
36
|
);
|
|
@@ -37,30 +37,45 @@ function esc(s: string): string {
|
|
|
37
37
|
.split(String.fromCharCode(34)).join(""");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
// Path contract (APP_WORKER_PATH_REWRITE off = current default): the proxy
|
|
41
|
+
// forwards /apps/{mount}/api/<x> to the worker as /<candidateSlug>/<x> — the
|
|
42
|
+
// candidate slug prefix is NOT stripped at runtime (the stripped path is only
|
|
43
|
+
// delivered via X-Pxbaas-Worker-Path when the rewrite kill-switch is ON, which
|
|
44
|
+
// is disabled by default due to the D18 streamRid regression). Normalize the
|
|
45
|
+
// slug prefix away and match on the logical path, so handlers resolve under ANY
|
|
46
|
+
// prefix shape (/{slug}/x, /api/x, or /x) and still work if the switch flips on.
|
|
47
|
+
// Same convention as the ssr-shop / ssr-blog / csr-bff templates.
|
|
48
|
+
function norm(path: string): string {
|
|
49
|
+
return path.replace(/^\/app_[^/]+/, "") || path;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
app.get("*", async (c) => {
|
|
53
|
+
const p = norm(new URL(c.req.url).pathname);
|
|
54
|
+
|
|
55
|
+
// L3 shape demo: server-side fetch of DB content via the derived internal base
|
|
56
|
+
// (anon key from env; RLS enforced server-side). Degrades gracefully so the
|
|
57
|
+
// shell still renders if the internal entry is ever unreachable.
|
|
58
|
+
if (p === "/ssr-data") {
|
|
59
|
+
let rows: Array<Record<string, unknown>> = [];
|
|
60
|
+
let note = "";
|
|
61
|
+
try {
|
|
62
|
+
const r = await fetch(internalBase() + "/rest/v1/?select=1", {
|
|
63
|
+
headers: { apikey: Deno.env.get("SUPABASE_ANON_KEY") || "" },
|
|
64
|
+
});
|
|
65
|
+
note = `internal base reachable: http-${r.status}`;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
note = `internal base unreachable: ${String(e).slice(0, 60)}`;
|
|
68
|
+
}
|
|
69
|
+
return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>L3 demo</title></head><body>
|
|
55
70
|
<div id="__ssr" data-ssr="v1"><h1>L3 full-SSR demo</h1><p>${esc(note)}</p><pre>${esc(JSON.stringify(rows))}</pre></div>
|
|
56
71
|
</body></html>`);
|
|
57
|
-
}
|
|
72
|
+
}
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if (p === "/page") {
|
|
75
|
+
const theme = c.req.query("theme") === "dark" ? "dark" : "light";
|
|
76
|
+
const bg = theme === "dark" ? "#111" : "#fafafa";
|
|
77
|
+
const fg = theme === "dark" ? "#eee" : "#222";
|
|
78
|
+
const html = `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
64
79
|
<title>{{APP_NAME}} SSR</title>
|
|
65
80
|
<style>body{font-family:sans-serif;max-width:720px;margin:40px auto;background:${bg};color:${fg}}</style>
|
|
66
81
|
</head><body>
|
|
@@ -77,16 +92,20 @@ app.get("/page", (c) => {
|
|
|
77
92
|
})();
|
|
78
93
|
</script>
|
|
79
94
|
</body></html>`;
|
|
80
|
-
|
|
81
|
-
}
|
|
95
|
+
return c.html(html);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// JSON API. /apps/{m}/api/data and /api/health normalize to /data and /health
|
|
99
|
+
// after the slug strip; the /api-prefixed shapes are kept as a defensive
|
|
100
|
+
// fallback for deployments that deliver the logical path un-stripped.
|
|
101
|
+
if (p === "/data" || p === "/api/data") {
|
|
102
|
+
return c.json({ ok: true, engine: "hono", now: new Date().toISOString() });
|
|
103
|
+
}
|
|
104
|
+
if (p === "/health" || p === "/api/health") {
|
|
105
|
+
return c.json({ ok: true });
|
|
106
|
+
}
|
|
82
107
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// fallthrough delivers the FULL logical path (/page). Register both shapes so
|
|
86
|
-
// the worker behaves identically when the rewrite kill-switch is flipped back on.
|
|
87
|
-
app.get("/data", (c) => c.json({ ok: true, engine: "hono", now: new Date().toISOString() }));
|
|
88
|
-
app.get("/api/data", (c) => c.json({ ok: true, engine: "hono", now: new Date().toISOString() }));
|
|
89
|
-
app.get("/health", (c) => c.json({ ok: true }));
|
|
90
|
-
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
108
|
+
return c.json({ ok: false, error: "not_found", path: p }, 404);
|
|
109
|
+
});
|
|
91
110
|
|
|
92
111
|
Deno.serve(app.fetch);
|