@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.
Files changed (115) hide show
  1. package/README.md +116 -13
  2. package/dist/app/local-index.d.ts +70 -0
  3. package/dist/app/local-index.d.ts.map +1 -0
  4. package/dist/app/local-index.js +140 -0
  5. package/dist/app/local-index.js.map +1 -0
  6. package/dist/app/preflight.d.ts +24 -1
  7. package/dist/app/preflight.d.ts.map +1 -1
  8. package/dist/app/preflight.js +47 -11
  9. package/dist/app/preflight.js.map +1 -1
  10. package/dist/app/spec-lint.d.ts.map +1 -1
  11. package/dist/app/spec-lint.js +16 -0
  12. package/dist/app/spec-lint.js.map +1 -1
  13. package/dist/cli/root.d.ts.map +1 -1
  14. package/dist/cli/root.js +3 -0
  15. package/dist/cli/root.js.map +1 -1
  16. package/dist/commands/app/index.d.ts.map +1 -1
  17. package/dist/commands/app/index.js +428 -44
  18. package/dist/commands/app/index.js.map +1 -1
  19. package/dist/commands/completion/index.d.ts.map +1 -1
  20. package/dist/commands/completion/index.js +5 -1
  21. package/dist/commands/completion/index.js.map +1 -1
  22. package/dist/commands/data/index.d.ts +52 -0
  23. package/dist/commands/data/index.d.ts.map +1 -1
  24. package/dist/commands/data/index.js +201 -39
  25. package/dist/commands/data/index.js.map +1 -1
  26. package/dist/commands/db/index.d.ts +14 -0
  27. package/dist/commands/db/index.d.ts.map +1 -1
  28. package/dist/commands/db/index.js +151 -40
  29. package/dist/commands/db/index.js.map +1 -1
  30. package/dist/commands/functions/new.d.ts +4 -0
  31. package/dist/commands/functions/new.d.ts.map +1 -1
  32. package/dist/commands/functions/new.js +24 -9
  33. package/dist/commands/functions/new.js.map +1 -1
  34. package/dist/commands/gen/index.d.ts.map +1 -1
  35. package/dist/commands/gen/index.js +13 -10
  36. package/dist/commands/gen/index.js.map +1 -1
  37. package/dist/commands/login/index.d.ts.map +1 -1
  38. package/dist/commands/login/index.js +15 -30
  39. package/dist/commands/login/index.js.map +1 -1
  40. package/dist/commands/profile/import-env.d.ts +7 -2
  41. package/dist/commands/profile/import-env.d.ts.map +1 -1
  42. package/dist/commands/profile/import-env.js +141 -16
  43. package/dist/commands/profile/import-env.js.map +1 -1
  44. package/dist/commands/profile/index.d.ts.map +1 -1
  45. package/dist/commands/profile/index.js +13 -1
  46. package/dist/commands/profile/index.js.map +1 -1
  47. package/dist/commands/storage/index.d.ts.map +1 -1
  48. package/dist/commands/storage/index.js +209 -7
  49. package/dist/commands/storage/index.js.map +1 -1
  50. package/dist/commands/tenant/index.d.ts +19 -0
  51. package/dist/commands/tenant/index.d.ts.map +1 -0
  52. package/dist/commands/tenant/index.js +662 -0
  53. package/dist/commands/tenant/index.js.map +1 -0
  54. package/dist/commands/whoami/index.js +19 -0
  55. package/dist/commands/whoami/index.js.map +1 -1
  56. package/dist/credentials/credential-store.d.ts +30 -1
  57. package/dist/credentials/credential-store.d.ts.map +1 -1
  58. package/dist/credentials/credential-store.js +114 -12
  59. package/dist/credentials/credential-store.js.map +1 -1
  60. package/dist/credentials/env-file.d.ts +48 -7
  61. package/dist/credentials/env-file.d.ts.map +1 -1
  62. package/dist/credentials/env-file.js +91 -16
  63. package/dist/credentials/env-file.js.map +1 -1
  64. package/dist/credentials/jwt-introspect.d.ts +7 -0
  65. package/dist/credentials/jwt-introspect.d.ts.map +1 -1
  66. package/dist/credentials/jwt-introspect.js.map +1 -1
  67. package/dist/credentials/masker.d.ts +3 -0
  68. package/dist/credentials/masker.d.ts.map +1 -1
  69. package/dist/credentials/masker.js +3 -0
  70. package/dist/credentials/masker.js.map +1 -1
  71. package/dist/credentials/profile.d.ts +12 -0
  72. package/dist/credentials/profile.d.ts.map +1 -1
  73. package/dist/credentials/profile.js +23 -0
  74. package/dist/credentials/profile.js.map +1 -1
  75. package/dist/credentials/tty-guard.d.ts +10 -0
  76. package/dist/credentials/tty-guard.d.ts.map +1 -1
  77. package/dist/credentials/tty-guard.js +32 -0
  78. package/dist/credentials/tty-guard.js.map +1 -1
  79. package/dist/db/introspect.d.ts +54 -0
  80. package/dist/db/introspect.d.ts.map +1 -0
  81. package/dist/db/introspect.js +140 -0
  82. package/dist/db/introspect.js.map +1 -0
  83. package/dist/main.js +21 -2
  84. package/dist/main.js.map +1 -1
  85. package/dist/tenant/bundle.d.ts +66 -0
  86. package/dist/tenant/bundle.d.ts.map +1 -0
  87. package/dist/tenant/bundle.js +132 -0
  88. package/dist/tenant/bundle.js.map +1 -0
  89. package/dist/tenant/guide.d.ts +50 -0
  90. package/dist/tenant/guide.d.ts.map +1 -0
  91. package/dist/tenant/guide.js +152 -0
  92. package/dist/tenant/guide.js.map +1 -0
  93. package/dist/tenant/probe.d.ts +65 -0
  94. package/dist/tenant/probe.d.ts.map +1 -0
  95. package/dist/tenant/probe.js +159 -0
  96. package/dist/tenant/probe.js.map +1 -0
  97. package/dist/tenant/verify.d.ts +37 -0
  98. package/dist/tenant/verify.d.ts.map +1 -0
  99. package/dist/tenant/verify.js +132 -0
  100. package/dist/tenant/verify.js.map +1 -0
  101. package/dist/version.d.ts +29 -0
  102. package/dist/version.d.ts.map +1 -0
  103. package/dist/version.js +65 -0
  104. package/dist/version.js.map +1 -0
  105. package/package.json +2 -2
  106. package/skills/pxbaas-app-spec/references/runtime-contract.md +2 -2
  107. package/skills/pxbaas-cli/SKILL.md +14 -5
  108. package/skills/pxbaas-cli/references/auth-and-credentials.md +19 -3
  109. package/skills/pxbaas-cli/references/data-postgrest.md +19 -3
  110. package/skills/pxbaas-cli/references/db-and-meta.md +5 -0
  111. package/skills/pxbaas-cli/references/safety-and-two-phase.md +42 -13
  112. package/skills/pxbaas-cli/references/tenant-admin.md +122 -0
  113. package/skills/pxbaas-cli/references/troubleshooting.md +4 -1
  114. package/templates/webapp/react-vite-hono/backend/index.ts +21 -4
  115. package/templates/webapp/ssr-hono/backend/index.ts +50 -31
@@ -0,0 +1,159 @@
1
+ /**
2
+ * 平台管理面能力探测(tenant probe)。
3
+ *
4
+ * 为什么要它:`profile import` / `login` 撞墙时,CLI 必须先知道"这个人到底是不是
5
+ * 管理员"才能给出正确的下一步。判据不能用"本机有没有 DASHBOARD_* 凭据"——持有凭据
6
+ * 与被授予权限是两件事(运维把服务端全量 env 发给客户排查问题,客户就"被管理员"了)。
7
+ * 所以这里用一次**只读**调用去问平台,按回答分流。
8
+ *
9
+ * 关键陷阱(方案文档 G3):Studio 的列表接口在「元数据查询失败」时同样返回
10
+ * `200 + { tenants: [], multiTenantMode: true }`(见 pages/api/platform/tenants/index.ts
11
+ * 的三处 200 空返回)。只看 HTTP 状态会把"平台库有问题/存量 schema 漂移"误报成
12
+ * "你没有权限 / 这个实例没有租户"。本模块用详情接口做二次判别:
13
+ * GET /api/platform/tenants/{不存在的 id}
14
+ * → 404 = 查询链路健康 ⇒ 空列表确实是"没有租户"
15
+ * → 500 = 查询本身失败 ⇒ 元数据不可读(多半是 schema 漂移)
16
+ * 这条判据来自 [id].ts 的实现(result.error → 500,未找到 → 404),非猜测。
17
+ */
18
+ import { TenantAdminDriver, TenantAdminError, } from '@polardbx/pxbaas-client';
19
+ import { CredentialStore, ProfileManager } from '../credentials/index.js';
20
+ /** 探测用的哨兵租户 ID:合法字符集内、且按命名规范不可能真实存在。 */
21
+ const METADATA_PROBE_ID = '__pxbaas_probe__';
22
+ /**
23
+ * 从当前(或指定)profile 的钥匙串账目里取管理凭据。
24
+ * 缺失时抛可直接展示的错误 —— 调用方不需要再判断"是 CLI 坏了还是没登录"。
25
+ */
26
+ export async function loadAdminContext(options = {}) {
27
+ const profiles = new ProfileManager();
28
+ const profile = options.profile
29
+ ? await profiles.getProfile(options.profile)
30
+ : await profiles.getCurrentProfile();
31
+ if (!profile) {
32
+ throw new Error(options.profile
33
+ ? `Profile '${options.profile}' not found. Run "pxbaas profile ls" to list profiles.`
34
+ : 'No active profile. Run "pxbaas login" or "pxbaas tenant login --endpoint <url>" first.');
35
+ }
36
+ const store = new CredentialStore();
37
+ const admin = await store.getAdminCredentials(profile.name);
38
+ if (!admin) {
39
+ throw new Error(`Profile '${profile.name}' has no platform admin credentials. ` +
40
+ `Run "pxbaas tenant login --endpoint ${profile.endpoint}" (or add --with-admin to "pxbaas profile import") ` +
41
+ `with the instance dashboard credentials.`);
42
+ }
43
+ return {
44
+ endpoint: profile.endpoint,
45
+ username: admin.username,
46
+ password: admin.password,
47
+ profileName: profile.name,
48
+ };
49
+ }
50
+ /** 用给定凭据探测管理面能力。纯只读:不会发出任何写请求。 */
51
+ export async function probeTenantAdmin(input) {
52
+ const driver = new TenantAdminDriver({
53
+ endpoint: input.endpoint,
54
+ username: input.username,
55
+ password: input.password,
56
+ timeout: input.timeout,
57
+ });
58
+ let tenants;
59
+ let multiTenantMode;
60
+ try {
61
+ const listed = await driver.list();
62
+ tenants = listed.tenants;
63
+ multiTenantMode = listed.multiTenantMode;
64
+ }
65
+ catch (error) {
66
+ return classifyFailure(input.endpoint, error);
67
+ }
68
+ if (!multiTenantMode) {
69
+ return {
70
+ status: 'single-tenant',
71
+ adminCapable: false,
72
+ endpoint: input.endpoint,
73
+ multiTenantMode: false,
74
+ detail: 'This instance is not in multi-tenant mode (multiTenantMode=false). Tenant administration does not apply here.',
75
+ };
76
+ }
77
+ if (tenants.length === 0) {
78
+ // 空列表有两种成因,必须用详情接口判别,否则会把平台故障报成"没有租户"。
79
+ const health = await probeMetadataHealth(driver);
80
+ if (!health.readable) {
81
+ return {
82
+ status: 'metadata-unreadable',
83
+ adminCapable: false,
84
+ endpoint: input.endpoint,
85
+ multiTenantMode: true,
86
+ tenants: [],
87
+ errorCode: health.errorCode,
88
+ detail: 'Credentials are accepted, but the tenant metadata could not be read ' +
89
+ `(${health.errorCode ?? 'unknown error'}). On long-lived instances this usually means schema drift ` +
90
+ '(e.g. a missing column in _platform.tenants) rather than an empty tenant list. ' +
91
+ 'Verify with the platform operator before retrying.',
92
+ };
93
+ }
94
+ return {
95
+ status: 'admin',
96
+ adminCapable: true,
97
+ endpoint: input.endpoint,
98
+ multiTenantMode: true,
99
+ tenants: [],
100
+ detail: 'Admin channel works. This instance has no tenants yet — you can create the first one with "pxbaas tenant create".',
101
+ };
102
+ }
103
+ return {
104
+ status: 'admin',
105
+ adminCapable: true,
106
+ endpoint: input.endpoint,
107
+ multiTenantMode: true,
108
+ tenants,
109
+ detail: `Admin channel works. ${tenants.length} tenant(s) visible on this instance.`,
110
+ };
111
+ }
112
+ async function probeMetadataHealth(driver) {
113
+ try {
114
+ await driver.get(METADATA_PROBE_ID);
115
+ // 哨兵 ID 竟然存在:极不可能,但存在即说明查询链路是通的。
116
+ return { readable: true };
117
+ }
118
+ catch (error) {
119
+ if (error instanceof TenantAdminError) {
120
+ // 404 ⇒ 查询执行成功且没找到 ⇒ 链路健康
121
+ if (error.code === 'TENANT_NOT_FOUND')
122
+ return { readable: true, errorCode: error.code };
123
+ return { readable: false, errorCode: error.code };
124
+ }
125
+ return { readable: false };
126
+ }
127
+ }
128
+ function classifyFailure(endpoint, error) {
129
+ const adminError = error instanceof TenantAdminError
130
+ ? error
131
+ : new TenantAdminError('TENANT_ADMIN_UNKNOWN_ERROR', error instanceof Error ? error.message : String(error));
132
+ if (adminError.code === 'TENANT_ADMIN_UNAUTHORIZED' || adminError.code === 'TENANT_ADMIN_FORBIDDEN') {
133
+ return {
134
+ status: 'not-admin',
135
+ adminCapable: false,
136
+ endpoint,
137
+ multiTenantMode: false,
138
+ errorCode: adminError.code,
139
+ detail: 'These credentials are not accepted by the tenant admin channel ' +
140
+ `(${adminError.code}, HTTP ${adminError.status ?? 'n/a'}). ` +
141
+ 'Tenant administration needs the instance dashboard credentials, not an API key.',
142
+ };
143
+ }
144
+ // 注意:多租户关闭的信号是列表响应里的 `multiTenantMode: false`(GET 集合接口对
145
+ // 非多租户实例返回 200 + 空数组),**不是** HTTP 400 —— 400 只出现在写操作分支
146
+ // ("Multi-tenant mode is not enabled")。所以这里不能把 400 解读成单租户实例,
147
+ // 否则等于凭一个猜测替用户下结论。400 归到"端点没按预期应答"。
148
+ return {
149
+ status: 'unreachable',
150
+ adminCapable: false,
151
+ endpoint,
152
+ multiTenantMode: false,
153
+ errorCode: adminError.code,
154
+ detail: adminError.code === 'TENANT_ADMIN_BAD_REQUEST'
155
+ ? `Tenant admin channel answered 400: ${adminError.message}. This instance did not report multi-tenant mode, so no tenant operation was attempted.`
156
+ : `Tenant admin channel could not be reached: ${adminError.message}`,
157
+ };
158
+ }
159
+ //# sourceMappingURL=probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/tenant/probe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 建租户后的验证闭环(方案文档 G4)。
3
+ *
4
+ * 平台侧的建租户接口会把「建库 / 建账号 / GRANT / 租户库内 RLS 表 / Realtime 注册」
5
+ * 全部当作**非致命**步骤:任何一步失败只 console.error,HTTP 仍然返回 201
6
+ * (pages/api/platform/tenants/index.ts:265 起)。也就是说"创建成功"并不等于
7
+ * "这个租户可用"。CLI 若在 create 之后直接报成功,管理员就会把平台缺陷当成 CLI 的 bug。
8
+ *
9
+ * 这里用新签发的**租户自己的** service_role key 走一遍租户自助 DDL 通道,逐项确认:
10
+ * 1. SELECT DATABASE() ⇒ 密钥有效、租户处于 active、自助 DDL 通道可用,并拿到真实库名
11
+ * 2. information_schema 计数 ⇒ 租户库确实存在且可内省(0 张表对新租户是正常的)
12
+ * 3. RLS 注册表可读 ⇒ 中心 RLS 元数据对该租户可达(RLS 落库的前置条件)
13
+ * 三条都是只读语句,不改变任何状态。
14
+ */
15
+ export type VerifyStatus = 'pass' | 'fail' | 'skip';
16
+ export interface VerifyStep {
17
+ name: string;
18
+ status: VerifyStatus;
19
+ detail: string;
20
+ /** 失败时给管理员的可执行补救建议。 */
21
+ remediation?: string;
22
+ }
23
+ export interface VerifyTenantResult {
24
+ ok: boolean;
25
+ /** 租户实际生效的库名(来自 SELECT DATABASE(),非拼接推断)。 */
26
+ dbName?: string;
27
+ steps: VerifyStep[];
28
+ }
29
+ export interface VerifyTenantInput {
30
+ endpoint: string;
31
+ serviceRoleKey: string;
32
+ /** create 响应里的 realtime_provisioned;为 false 时记为 warn 级步骤(平台侧非致命)。 */
33
+ realtimeProvisioned?: boolean;
34
+ timeout?: number;
35
+ }
36
+ export declare function verifyTenantUsable(input: VerifyTenantInput): Promise<VerifyTenantResult>;
37
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/tenant/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkH9F"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * 建租户后的验证闭环(方案文档 G4)。
3
+ *
4
+ * 平台侧的建租户接口会把「建库 / 建账号 / GRANT / 租户库内 RLS 表 / Realtime 注册」
5
+ * 全部当作**非致命**步骤:任何一步失败只 console.error,HTTP 仍然返回 201
6
+ * (pages/api/platform/tenants/index.ts:265 起)。也就是说"创建成功"并不等于
7
+ * "这个租户可用"。CLI 若在 create 之后直接报成功,管理员就会把平台缺陷当成 CLI 的 bug。
8
+ *
9
+ * 这里用新签发的**租户自己的** service_role key 走一遍租户自助 DDL 通道,逐项确认:
10
+ * 1. SELECT DATABASE() ⇒ 密钥有效、租户处于 active、自助 DDL 通道可用,并拿到真实库名
11
+ * 2. information_schema 计数 ⇒ 租户库确实存在且可内省(0 张表对新租户是正常的)
12
+ * 3. RLS 注册表可读 ⇒ 中心 RLS 元数据对该租户可达(RLS 落库的前置条件)
13
+ * 三条都是只读语句,不改变任何状态。
14
+ */
15
+ import { TenantDdlDriver, TenantDdlError } from '@polardbx/pxbaas-client';
16
+ export async function verifyTenantUsable(input) {
17
+ const driver = new TenantDdlDriver({
18
+ endpoint: input.endpoint,
19
+ serviceRoleKey: input.serviceRoleKey,
20
+ timeout: input.timeout ?? 30_000,
21
+ });
22
+ const steps = [];
23
+ let dbName;
24
+ // 1) 租户自助 DDL 通道 + 密钥有效性
25
+ let channelOk = false;
26
+ try {
27
+ const rows = (await driver.query('SELECT DATABASE() AS db'));
28
+ const value = rows?.[0]?.db;
29
+ dbName = typeof value === 'string' && value ? value : undefined;
30
+ channelOk = Boolean(dbName);
31
+ steps.push({
32
+ name: 'Tenant DDL channel',
33
+ status: channelOk ? 'pass' : 'fail',
34
+ detail: channelOk
35
+ ? `Connected as tenant database "${dbName}"`
36
+ : 'SELECT DATABASE() returned no database name',
37
+ remediation: channelOk
38
+ ? undefined
39
+ : 'Verify the tenant is active and that the key belongs to this tenant (pxbaas doctor --json).',
40
+ });
41
+ }
42
+ catch (error) {
43
+ steps.push({
44
+ name: 'Tenant DDL channel',
45
+ status: 'fail',
46
+ detail: describe(error),
47
+ remediation: 'The tenant database or its account may not have been provisioned. Platform-side tenant creation treats those steps as non-fatal, so a 201 response does not guarantee a usable tenant — ask the operator to check the tenant db, user and GRANT.',
48
+ });
49
+ }
50
+ // 2) 库可内省(存在性代理判据:能查 information_schema 即库已在)
51
+ if (channelOk) {
52
+ try {
53
+ const rows = (await driver.query('SELECT COUNT(*) AS t FROM information_schema.tables WHERE table_schema = DATABASE()'));
54
+ const count = Number(rows?.[0]?.t ?? NaN);
55
+ steps.push({
56
+ name: 'Tenant database introspectable',
57
+ status: Number.isFinite(count) ? 'pass' : 'fail',
58
+ detail: Number.isFinite(count)
59
+ ? `${count} table(s) currently in "${dbName}"`
60
+ : 'Count query returned an unexpected shape',
61
+ });
62
+ }
63
+ catch (error) {
64
+ steps.push({
65
+ name: 'Tenant database introspectable',
66
+ status: 'fail',
67
+ detail: describe(error),
68
+ remediation: 'Create tables with "pxbaas db push <file>" or "pxbaas app db-init <name>".',
69
+ });
70
+ }
71
+ }
72
+ else {
73
+ steps.push({
74
+ name: 'Tenant database introspectable',
75
+ status: 'skip',
76
+ detail: 'Skipped because the DDL channel is not usable.',
77
+ });
78
+ }
79
+ // 3) 中心 RLS 注册表对该租户可达
80
+ if (channelOk) {
81
+ try {
82
+ const tables = await driver.listRlsTables();
83
+ steps.push({
84
+ name: 'RLS registry reachable',
85
+ status: 'pass',
86
+ detail: `${tables.length} RLS-registered table(s) for this tenant`,
87
+ });
88
+ }
89
+ catch (error) {
90
+ steps.push({
91
+ name: 'RLS registry reachable',
92
+ status: 'fail',
93
+ detail: describe(error),
94
+ remediation: 'RLS metadata may not be initialized for this tenant. Register policies through "pxbaas db push --rls db/rls.json" (multi-tenant binds table_schema server-side).',
95
+ });
96
+ }
97
+ }
98
+ else {
99
+ steps.push({ name: 'RLS registry reachable', status: 'skip', detail: 'Skipped (channel unavailable).' });
100
+ }
101
+ // 4) Realtime 注册(平台侧非致命;false 时该租户 WS 握手会 403)
102
+ if (input.realtimeProvisioned === undefined) {
103
+ steps.push({
104
+ name: 'Realtime registration',
105
+ status: 'skip',
106
+ detail: 'Not reported by this endpoint (only the create response carries it).',
107
+ });
108
+ }
109
+ else {
110
+ steps.push({
111
+ name: 'Realtime registration',
112
+ status: input.realtimeProvisioned ? 'pass' : 'fail',
113
+ detail: input.realtimeProvisioned
114
+ ? 'Tenant registered for Realtime'
115
+ : 'Realtime registration failed (platform-side, non-fatal for REST/Auth)',
116
+ remediation: input.realtimeProvisioned
117
+ ? undefined
118
+ : 'WebSocket subscriptions for this tenant will be rejected until it is registered in Realtime; ask the operator to re-run registration.',
119
+ });
120
+ }
121
+ return {
122
+ ok: steps.every((step) => step.status !== 'fail'),
123
+ dbName,
124
+ steps,
125
+ };
126
+ }
127
+ function describe(error) {
128
+ if (error instanceof TenantDdlError)
129
+ return `${error.code}: ${error.message}`;
130
+ return error instanceof Error ? error.message : String(error);
131
+ }
132
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/tenant/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Runtime CLI version resolution.
3
+ *
4
+ * `npm run build` is a plain `tsc --build`, so there is no bundler step that
5
+ * could inline `package.json#version` into the emitted JS — and adding one is
6
+ * out of the question for a build pipeline this simple. Read the manifest from
7
+ * disk instead, walking up from this module so every supported layout works:
8
+ *
9
+ * - source tree : <pkg>/dist/version.js -> <pkg>/package.json
10
+ * - npm install : node_modules/@polardbx/pxbaas/dist/version.js -> .../package.json
11
+ *
12
+ * The walk-up must not stop at the first manifest it finds: in a monorepo the
13
+ * workspace root (`cli/package.json`, version `0.0.0-dev`) is also an ancestor,
14
+ * which is exactly how `--version` used to report a version nobody published.
15
+ * Only the manifest that owns this package (matching `name`, or declaring the
16
+ * `pxbaas` bin) counts.
17
+ *
18
+ * Never throws: an unreadable manifest must not stop `pxbaas --help`.
19
+ */
20
+ export declare const PACKAGE_NAME = "@polardbx/pxbaas";
21
+ /** Reported when no owning manifest can be found (e.g. a single-file bundle). */
22
+ export declare const UNKNOWN_VERSION = "0.0.0-unknown";
23
+ /**
24
+ * The published version of this CLI, or {@link UNKNOWN_VERSION} when it cannot be
25
+ * determined. Synchronous on purpose: commander's `.version()` is wired up at
26
+ * module scope, before any await could run.
27
+ */
28
+ export declare function resolvePackageVersion(startDir?: string): string;
29
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C,iFAAiF;AACjF,eAAO,MAAM,eAAe,kBAAkB,CAAC;AA8B/C;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,GAAE,MAAoB,GAAG,MAAM,CAY5E"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Runtime CLI version resolution.
3
+ *
4
+ * `npm run build` is a plain `tsc --build`, so there is no bundler step that
5
+ * could inline `package.json#version` into the emitted JS — and adding one is
6
+ * out of the question for a build pipeline this simple. Read the manifest from
7
+ * disk instead, walking up from this module so every supported layout works:
8
+ *
9
+ * - source tree : <pkg>/dist/version.js -> <pkg>/package.json
10
+ * - npm install : node_modules/@polardbx/pxbaas/dist/version.js -> .../package.json
11
+ *
12
+ * The walk-up must not stop at the first manifest it finds: in a monorepo the
13
+ * workspace root (`cli/package.json`, version `0.0.0-dev`) is also an ancestor,
14
+ * which is exactly how `--version` used to report a version nobody published.
15
+ * Only the manifest that owns this package (matching `name`, or declaring the
16
+ * `pxbaas` bin) counts.
17
+ *
18
+ * Never throws: an unreadable manifest must not stop `pxbaas --help`.
19
+ */
20
+ import { readFileSync } from 'node:fs';
21
+ import { dirname, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ export const PACKAGE_NAME = '@polardbx/pxbaas';
24
+ /** Reported when no owning manifest can be found (e.g. a single-file bundle). */
25
+ export const UNKNOWN_VERSION = '0.0.0-unknown';
26
+ /** How many parent directories to probe before giving up. */
27
+ const MAX_WALK_DEPTH = 8;
28
+ function moduleDir() {
29
+ return dirname(fileURLToPath(import.meta.url));
30
+ }
31
+ function readManifest(dir) {
32
+ try {
33
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /** True when the manifest belongs to this CLI package rather than an ancestor workspace. */
40
+ function ownsThisPackage(manifest) {
41
+ if (manifest.name === PACKAGE_NAME)
42
+ return true;
43
+ const bin = manifest.bin;
44
+ return typeof bin === 'object' && bin !== null && 'pxbaas' in bin;
45
+ }
46
+ /**
47
+ * The published version of this CLI, or {@link UNKNOWN_VERSION} when it cannot be
48
+ * determined. Synchronous on purpose: commander's `.version()` is wired up at
49
+ * module scope, before any await could run.
50
+ */
51
+ export function resolvePackageVersion(startDir = moduleDir()) {
52
+ let dir = startDir;
53
+ for (let i = 0; i < MAX_WALK_DEPTH; i += 1) {
54
+ const manifest = readManifest(dir);
55
+ if (manifest && ownsThisPackage(manifest)) {
56
+ return manifest.version ?? UNKNOWN_VERSION;
57
+ }
58
+ const parent = dirname(dir);
59
+ if (parent === dir)
60
+ break;
61
+ dir = parent;
62
+ }
63
+ return UNKNOWN_VERSION;
64
+ }
65
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,YAAY,GAAG,kBAAkB,CAAC;AAE/C,iFAAiF;AACjF,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC;AAE/C,6DAA6D;AAC7D,MAAM,cAAc,GAAG,CAAC,CAAC;AAQzB,SAAS,SAAS;IAChB,OAAO,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAoB,CAAC;IACxF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,SAAS,eAAe,CAAC,QAAyB;IAChD,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IAChD,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC;IACzB,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,QAAQ,IAAI,GAAG,CAAC;AACpE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,WAAmB,SAAS,EAAE;IAClE,IAAI,GAAG,GAAG,QAAQ,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,QAAQ,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,OAAO,QAAQ,CAAC,OAAO,IAAI,eAAe,CAAC;QAC7C,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC;AACzB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polardbx/pxbaas",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "PolarDB-X BaaS CLI application",
5
5
  "keywords": [
6
6
  "polardbx",
@@ -35,7 +35,7 @@
35
35
  "inquirer": "^9.2.0",
36
36
  "jsonwebtoken": "^9.0.0",
37
37
  "keytar": "^7.9.0",
38
- "@polardbx/pxbaas-client": "0.1.0-beta.2"
38
+ "@polardbx/pxbaas-client": "0.1.0-beta.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/inquirer": "^9.0.0",
@@ -98,7 +98,7 @@ Apps may declare `frontend.fallback: "worker"` in app.json: static-miss non-`/ap
98
98
  - **React SSR emits no event handlers**: `renderToString` drops every `on*` prop (lowercase `onclick` included). Wire interactivity with `data-*` attributes (passed through verbatim) plus one client-side delegated listener; verify by grepping the served HTML for the attribute, not by reading component code.
99
99
  - **MUST** fetch data client-side after hydration (relative `./api/*` or the instance REST/Auth URLs with the user's JWT) for the default SSR-shell shape. For server-side data fetching (L3 full SSR / SEO first paint), **MUST** use the platform-injected `SUPABASE_INTERNAL_URL` as the base — it is the ONLY origin reachable from inside the sandbox (the public `SUPABASE_URL` is egress-filtered and cluster service names do not resolve). The platform derives `SUPABASE_INTERNAL_URL` from the public host via DX-12 (edge-runtime `app-env.ts`, MR #30497903); the worker just reads it and **MUST NOT** re-derive from `SUPABASE_URL` or fetch `SUPABASE_URL` directly. Use `SUPABASE_ANON_KEY` (or the forwarded user JWT) as the credential; server-side RLS still applies on the internal base. These three public keys reach App candidate workers only on sandbox images carrying MR #30497903 — degrade gracefully (render the shell without server data) when `SUPABASE_INTERNAL_URL` is empty.
100
100
  - SSR responses are `private, no-cache` with the platform CSP (inline hydrate scripts allowed); do not rely on shared caching of rendered pages.
101
- - **Path shapes (interim contract while `APP_WORKER_PATH_REWRITE` is off)**: the `/api/*` branch delivers the STRIPPED logical path to the worker (`/apps/{m}/api/data` → worker sees `/data`), while static-miss fallthrough delivers the FULL logical path (`/apps/{m}/page` → worker sees `/page`). Workers MUST register both the stripped and `/api`-prefixed shapes for every JSON endpoint (the `ssr-hono` template does); hydrate scripts calling `./api/*` from an SSR page rely on the stripped shape being routed.
101
+ - **Path shapes (current default, `APP_WORKER_PATH_REWRITE` OFF)**: the proxy forwards EVERY worker request as `/<candidateSlug>/<logicalPath>` — the candidate slug prefix is **not** stripped at runtime. So `/apps/{m}/api/data` reaches the worker as `/<candidateSlug>/data`, and a static-miss fallthrough `/apps/{m}/page` reaches it as `/<candidateSlug>/page` (the `/api` segment is consumed by the proxy; the slug is prepended for edge-runtime function routing). The stripped logical path is delivered ONLY via the `X-Pxbaas-Worker-Path` header when `APP_WORKER_PATH_REWRITE` is ON, which is **disabled by default** (the runtime `new Request(url, req)` rewrite crashes with a streamRid TypeError on current sandbox binaries — D18). Workers MUST therefore normalize the leading `/<candidateSlug>` prefix and match on the logical path, e.g. `const p = new URL(req.url).pathname.replace(/^\/app_[^/]+/, "")` then dispatch on `p` (the `ssr-hono`, `ssr-shop`, `ssr-blog`, `csr-bff` templates all do this). A literal `app.get("/data")` will NOT match; do not rely on the declared `pathMode: "strip-app-api-prefix"` being honored at runtime yet. Hydrate scripts calling `./api/*` from an SSR page are routed the same way (browser `/apps/{m}/api/data` → worker `/<candidateSlug>/data`).
102
102
  - Start from the `ssr-hono` template (`pxbaas app new --template ssr-hono`); spec-lint enforces the rules above (`ssr-probe-declared`, `ssr-self-contained`, `ssr-marker-present`, `ssr-no-browser-globals`, `ssr-lib-not-allowed`).
103
103
 
104
104
  ## Lifecycle & cold start
@@ -139,7 +139,7 @@ For large frontends regardless of mode, host heavy static assets on Storage/CDN
139
139
 
140
140
  - The backend entrypoint must call `Deno.serve`; do not bind a port.
141
141
  - Node APIs (`node:*`, `require`, `process`, and Node server APIs) are forbidden.
142
- - Runtime `Deno.env` access is limited to the platform public three-key whitelist `SUPABASE_URL` / `SUPABASE_ANON_KEY` / `SUPABASE_INTERNAL_URL` (injected into App candidate workers per edge-runtime `app-env.ts`, MR #30497903). Reading any other variable or `Deno.env.toObject()`, non-empty Secrets, and any egress policy other than `deny` are unsupported in the first release (`deno-env-not-allowed`, `custom-secrets-not-supported`). `SUPABASE_URL` is render-only — never fetch through it; use `SUPABASE_INTERNAL_URL` as the data-plane base.
142
+ - Runtime `Deno.env` access is limited to the platform public three-key whitelist `SUPABASE_URL` / `SUPABASE_ANON_KEY` / `SUPABASE_INTERNAL_URL` (injected into App candidate workers per edge-runtime `app-env.ts`, MR #30497903). Reading any other variable or `Deno.env.toObject()`, non-empty Secrets, and any egress policy other than `deny` are unsupported in the first release (`deno-env-not-allowed`, `custom-secrets-not-supported`). `SUPABASE_URL` is render-only — never fetch through it; use `SUPABASE_INTERNAL_URL` as the data-plane base. These three names are platform-reserved: the platform value always wins over a user Secret of the same name, so never declare them in `backend.secrets`.
143
143
  - `egressPolicy: "deny"` means **no public egress**, not "no network": the sandbox egress filter blocks public origins and arbitrary outbound, but the platform-internal base (`SUPABASE_INTERNAL_URL`) stays reachable, so the worker MAY fetch the instance's own REST/Auth/Storage through it. Only `deny` is accepted in the first release (any other value is a blocker).
144
144
  - Arbitrary `http:`, `https:`, and `jsr:` imports are blocked. The only approved `npm:` packages are Hono (exact capability version) plus the curated SSR renderers reported by capability `ssrLibs` (exact `npm:<name>@<version>`, optional `/subpath`); anything else fails spec-lint (`ssr-lib-not-allowed`, or `no-remote-import` when the instance reports no `ssrLibs`).
145
145
  - Scaffolded Hono source uses `npm:hono@{{HONO_VERSION}}`; candidate staging substitutes the exact version without modifying local source.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pxbaas-cli
3
- description: Use the PolarDB-X BaaS CLI (command `pxbaas`) to consume the data plane from the command line — run SQL, inspect tables/schema, query and edit table data (PostgREST), deploy/list/delete Edge Functions, manage storage objects and auth users, generate types, claim a free disposable instance (`pxbaas provision`), and scaffold/deploy/publish/rollback Webapps (`pxbaas app`). Use when the user works with a PolarDB-X BaaS / MySQL-Supabase instance through `pxbaas`, has no instance yet and wants to provision one, wants to deploy a demo webapp, or mentions the pxbaas CLI, querying BaaS tables/data, running SQL, edge function deploy, storage objects, auth users, gen types, or pxbaas login/doctor/401/403 errors. Data-plane consumption + self-service provisioning (not platform deployment or tenant lifecycle). Agent rules — never run `pxbaas login` (human-only TTY), but `pxbaas provision` and `pxbaas profile import <env-file>` (bind a single-tenant instance from a `.env` file) ARE agent-runnable; always pass `--json`; confirm destructive operations with the user before executing.
3
+ description: Use the PolarDB-X BaaS CLI (`pxbaas`) to work with a PolarDB-X BaaS / MySQL-Supabase instance: run SQL and inspect tables/schema (`db`), read/write rows PostgREST-style (`data`, `rest`), deploy/list/delete Edge Functions (`functions`), scaffold-deploy-publish Webapps and share links (`app`), storage objects, auth users, type generation, claim a free disposable instance (`provision`), and - for instance operators - administer tenants and hand out per-tenant keys (`tenant probe/ls/create/keys/repair-keys`, access bundles). Trigger on pxbaas commands, BaaS SQL or table questions, function/webapp deploy, storage or users, gen types, provisioning, multi-tenant login, and 401/403 errors. Agent rules: never run `pxbaas login` (TTY-only); `provision` and `profile import <env-file>` are agent-runnable; always pass `--json`; never print or request plaintext keys; never create a tenant without explicit user confirmation.
4
4
  ---
5
5
 
6
6
  # pxbaas-cli
@@ -54,7 +54,7 @@ All requests go through a single Kong gateway (port **8000**). Key path prefixes
54
54
  | **1** | **NEVER run `pxbaas login`** | It requires an interactive TTY; agents cannot complete it. Credentials are provisioned by a human. If not logged in, **ask the user to run `pxbaas login`** — do not attempt it and do not ask for plaintext keys. **Exception:** `pxbaas provision` IS agent-runnable (keys come from Zero over TLS and are stored without being shown) — see `references/provision.md`. |
55
55
  | **2** | **Pass `--json` for anything you parse** | Status queries, remote operations, and any command whose result you read MUST use `--json` (parse JSON, never human-readable text). Pure local scaffolding, shell completion, and file-output commands (e.g. `gen types --output <file>`) are human-facing — `--json` is optional there. |
56
56
  | **3** | **You cannot see plaintext keys** | Output shows only fingerprints (sha256 first 8 + last 4). Never expect, echo, or reconstruct a plaintext anon/service key. |
57
- | **4** | **Confirm destructive ops first** | ⚠️ The CLI currently has **no built-in confirmation gate**. Before any write/delete/DDL, YOU must restate the target and impact and get the user's explicit consent. See `references/safety-and-two-phase.md`. |
57
+ | **4** | **Confirm destructive ops first** | Some commands gate themselves (`--dry-run`/`--yes` on `db push`, `app db-init`, `data delete`; `--confirm` on `app remove`, `app destroy`, `instance release`, `app share revoke`, `skill remove`, `tenant create`, `tenant repair-keys`). **Many do not** — `db create-table` / `db add-column`, `functions deploy` / `delete`, `storage rm` / `mv` / `cp` overwrite, `auth users delete`, `rest` writes execute immediately. For those, YOU are the gate: restate target, impact and reversibility, then get explicit consent. See `references/safety-and-two-phase.md`. |
58
58
  | **5** | **Respect role & tenant isolation** | Prefer `anon` for reads; explain when `service_role` is needed (it bypasses RLS). Never mix credentials across profiles. |
59
59
 
60
60
  ## First-run discovery
@@ -76,7 +76,7 @@ Legend — **R** = read/safe · **W** = write · **D** = destructive (consent re
76
76
  | Command | Default role | R/W/D | Details |
77
77
  |---|---|---|---|
78
78
  | `login` (human TTY only) | — | special | auth-and-credentials.md |
79
- | `profile import <env-file>` (agent-runnable, single-tenant) | — | W (local) | auth-and-credentials.md |
79
+ | `profile import <env-file>` (agent-runnable; single-tenant, or multi-tenant when the file carries `TENANT_*` keys) | — | W (local) | auth-and-credentials.md |
80
80
  | `logout` | — | W (local) | auth-and-credentials.md |
81
81
  | `whoami` | — | R | auth-and-credentials.md |
82
82
  | `status` | — | R | auth-and-credentials.md |
@@ -109,6 +109,11 @@ Legend — **R** = read/safe · **W** = write · **D** = destructive (consent re
109
109
  | `auth users delete <userId>` | service_role | **D** | auth-and-credentials.md |
110
110
  | `auth users invite <email>` | service_role | W | auth-and-credentials.md |
111
111
  | `completion <shell>` | — | R (local) | (this file) |
112
+ | `tenant login` / `tenant logout` | dashboard Basic | W (local) | tenant-admin.md |
113
+ | `tenant probe` / `tenant ls` / `tenant handoffs` | dashboard Basic | R | tenant-admin.md |
114
+ | `tenant create [id]` | dashboard Basic | **D/W** (provisions db, account, GRANT, bucket, quota) | tenant-admin.md |
115
+ | `tenant keys <id>` | dashboard Basic | W (local; writes keyring or 0600 bundle) | tenant-admin.md |
116
+ | `tenant repair-keys` | dashboard Basic | W (`--confirm`) | tenant-admin.md |
112
117
  | `provision` (agent-runnable) | — | W (local+remote) | provision.md |
113
118
  | `instance list` / `instance current` | — | R | provision.md |
114
119
  | `instance credentials <id>` | — | R (`--reveal` for dashboard pw) | provision.md |
@@ -122,7 +127,7 @@ Legend — **R** = read/safe · **W** = write · **D** = destructive (consent re
122
127
  | `app remove <name>` | service_role | **D** (`--confirm`; legacy `spa/api/mpa` only) | app-deploy.md |
123
128
  | `app destroy <name>` | service_role | **D** (two-phase `--confirm`; webapp only; deletes edge funcs + mount, **keeps DB tables**) | app-deploy.md |
124
129
 
125
- For provisioning / multi-instance selection see **provision.md**; for the `pxbaas app` commands see **app-deploy.md**. For how to **write** edge-function webapp code (Deno runtime contract, guardrails, templates, raw deploy paths), use the companion **`pxbaas-app-spec` skill**. For the live platform API beyond the CLI (all endpoints, multi-tenant rules, error codes, limitations), see **api-reference.md** (`GET ${endpoint}/docs`).
130
+ For provisioning / multi-instance selection see **provision.md**; for the `pxbaas app` commands see **app-deploy.md**; for tenant provisioning and handing per-tenant keys to end users see **tenant-admin.md** (operator-only surface). For how to **write** edge-function webapp code (Deno runtime contract, guardrails, templates, raw deploy paths), use the companion **`pxbaas-app-spec` skill**. For the live platform API beyond the CLI (all endpoints, multi-tenant rules, error codes, limitations), see **api-reference.md** (`GET ${endpoint}/docs`).
126
131
 
127
132
  ## Natural language → command
128
133
 
@@ -142,13 +147,15 @@ For provisioning / multi-instance selection see **provision.md**; for the `pxbaa
142
147
  "give me a shareable demo URL" → pxbaas app url <name> --json
143
148
  "destroy/delete this webapp" → confirm first → pxbaas app destroy <name> --confirm --json (removes edge funcs + mount; DB tables kept)
144
149
  "can't connect / error" → pxbaas doctor --json; if credentials missing, ask user to run pxbaas login
150
+ "env says no tenant-level keys" → read the guidance it prints; if the user operates the instance: pxbaas tenant login → tenant ls → tenant keys <id> --as-profile (never create a tenant without asking)
151
+ "give this tenant to a new user" → pxbaas tenant keys <id> --bundle ./<id>.pxbaas.env, then send the printed handoff note (contains fingerprints, not keys)
145
152
  ```
146
153
 
147
154
  ## What NOT to do
148
155
 
149
156
  - **NEVER run `pxbaas login`** — it needs an interactive TTY. Ask the user to log in instead.
150
157
  - **NEVER expect or echo plaintext keys** — output is fingerprint-only; never ask the user for a raw key to build a command.
151
- - **NEVER run `data delete` / `storage rm` / `functions delete` / DDL without previewing first** and getting explicit user consent (no built-in gate exists).
158
+ - **NEVER run `storage rm` / `functions delete` / `db create-table` / `db add-column` / `rest` writes / `auth users delete` without previewing first** and getting explicit user consent — those have **no** gate. `data delete`, `db push`, `app db-init`, `app destroy`, `instance release`, `tenant create` and `tenant repair-keys` do gate themselves; satisfy their `--yes`/`--confirm` only **after** the user agreed.
152
159
  - **NEVER write PostgreSQL SQL in `db query`** — the backend is MySQL/PolarDB-X (use `CAST(x AS t)`, backticks, `JSON_EXTRACT`, `UUID()`, not `::`, `"col"`, `->>`, `gen_random_uuid()`).
153
160
  - **NEVER assume an API listed in `/docs` is available via the CLI** — `/docs` covers the whole platform including admin APIs (PG-Meta, Management API) and tenant lifecycle; the CLI wraps only the customer data-plane subset in the command map above.
154
161
  - **NEVER mix credentials across profiles** or target another tenant; the CLI enforces tenant isolation.
@@ -157,6 +164,8 @@ For provisioning / multi-instance selection see **provision.md**; for the `pxbaa
157
164
  - **NEVER parse human-readable output** — always use `--json`.
158
165
  - **NEVER loop-retry Zero capacity/rate errors** (`429 SUPABASE_LIMIT_REACHED` / `503 SUPABASE_POOL_EXHAUSTED`) — stop, inform the user, offer `pxbaas instance release`; retry at most once for pool exhaustion.
159
166
  - **NEVER expect plaintext keys from `pxbaas provision`** — it stores them in the keyring and shows only fingerprints (this is why it is agent-runnable, unlike `pxbaas login`).
167
+ - **NEVER cat, echo, commit or transmit a tenant access bundle** (`*.pxbaas.env`) — it holds plaintext, ~10-year, non-revocable keys. The CLI only writes them to a `0600` file or the keyring; distributing the file is the operator's decision, made over a secure channel.
168
+ - **NEVER create a tenant as a workaround for a missing key.** Run `tenant ls` first: most "no key" cases are an existing tenant whose key was never handed over. A create that collides with a soft-deleted id resurrects it and regenerates that tenant's keys — the CLI refuses this without `--reactivate`.
160
169
  - **NEVER upload frontend source, `.env` files, source maps, `node_modules`, Service Workers, or binary assets as a Webapp artifact.** Build first; upload only the bounded static output. Put large/binary assets on Storage/CDN (see app-deploy.md).
161
170
 
162
171
  ## Global flags
@@ -1,6 +1,6 @@
1
1
  # Credentials, Environment & User Management
2
2
 
3
- Covers: `login` (human-only) · `profile import` (agent-runnable, env-file) · `logout` · `whoami` · `status` · `doctor` · `profile` · `auth users`.
3
+ Covers: `login` (human-only) · `profile import` (agent-runnable, env-file) · `logout` · `whoami` · `status` · `doctor` · `profile` · `auth users` · `tenant login` / `tenant logout` (operator credentials; see tenant-admin.md).
4
4
 
5
5
  ## login — agents must NEVER run this
6
6
 
@@ -13,7 +13,7 @@ pxbaas login --endpoint <url> --tenant-id <id> --anon-key-stdin --service-key-st
13
13
  - Credentials are stored in the OS keyring (keytar), falling back to a `0600` file when no keyring is available.
14
14
  - If another command reports `No active profile. Use "pxbaas login" ...` → **ask the user to log in manually**, OR — if a Supabase/BaaS `.env` file for the instance is available — bind it non-interactively with `pxbaas profile import <env-file>` (see below; that one IS agent-runnable).
15
15
 
16
- ## profile import — bind a single-tenant instance from a `.env` file (agents MAY run this)
16
+ ## profile import — bind an instance from a `.env` file (agents MAY run this)
17
17
 
18
18
  ```bash
19
19
  pxbaas profile import <env-file> --name <profile> [--intranet] --json
@@ -22,7 +22,23 @@ pxbaas profile import <env-file> --name <profile> [--intranet] --json
22
22
  - Reads `SUPABASE_URL` (fallback `SUPABASE_PUBLIC_URL` / `SUPABASE_INTRANET_URL`), `SUPABASE_ANON_KEY` (fallback `ANON_KEY` / `GLOBAL_ANON_KEY`) and `SUPABASE_SERVICE_ROLE_KEY` (fallback `SERVICE_ROLE_KEY` / `GLOBAL_SERVICE_ROLE_KEY`) from the file, validates the JWTs (rejects expired keys), then writes the profile + credentials and switches current to it.
23
23
  - **Agent-runnable**: keys come from a file, never from argv/shell history, so no TTY is needed. Prefer this over `login` when you already have the instance `.env`.
24
24
  - `--intranet` prefers `SUPABASE_INTRANET_URL` (VPC-internal entrypoint) over the public URL. `--name` sets the profile name (default: global `--profile`, else `default`); an existing profile of the same name is **overwritten**.
25
- - **Single-tenant only for now.** A `MULTI_TENANT_MODE=true` env file is rejected with guidance — its global keys cannot access tenant resources; use `pxbaas login` with per-tenant anon/service keys instead.
25
+ - **Multi-tenant env files are supported** when they carry tenant-level credentials: `MULTI_TENANT_MODE=true` + `DEFAULT_TENANT_ID` + `TENANT_ANON_KEY` + `TENANT_SERVICE_ROLE_KEY`. Global keys (`GLOBAL_ANON_KEY` / `GLOBAL_SERVICE_ROLE_KEY`) are reported as present-but-not-imported — they cannot reach tenant resources, so the profile never absorbs them.
26
+ - If a `MULTI_TENANT_MODE=true` file lacks the tenant keys, the command refuses and prints a diagnosis (which variables exist, whether each JWT carries a `tenant_id` claim) plus one of two routes: the operator path (`pxbaas tenant login` → `tenant ls` → `tenant keys <id> --as-profile`) or a checklist to send to the operator. Nothing is created and nothing is imported either way; the temporary admin probe it runs to pick the route does not persist the credentials it borrowed.
27
+ - **`DASHBOARD_USERNAME` / `DASHBOARD_PASSWORD` in an env file are NOT imported by default.** Only `--with-admin` binds them, and then only after the credentials actually open the tenant admin channel. Pass `--with-admin` only when the user is the instance operator.
28
+
29
+ ## Operator credentials (`tenant login`)
30
+
31
+ ```bash
32
+ pxbaas tenant login --endpoint https://<gateway> # TTY: masked password prompt
33
+ printf '%s\n%s\n' "$USER" "$PASS" | \
34
+ pxbaas tenant login --endpoint https://<gateway> --user-stdin --password-stdin
35
+ pxbaas tenant logout # drops dashboard creds only, keeps tenant keys
36
+ ```
37
+
38
+ - Stored under a separate keyring account (`admin/<profile>`); data-plane commands cannot read it.
39
+ - The password is refused in argv (`--dashboard-password` / `--admin-password` are blocked by the same guard as API keys).
40
+ - `tenant login` probes the admin channel **before** storing; a refused probe stores nothing.
41
+ - `pxbaas whoami --json` reports `platformAdmin: {available, userMasked, passwordFingerprint}` — never the secret.
26
42
 
27
43
  ## Verify login state (run these first)
28
44