@supacloud/cli 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/index.js +201 -62
- package/package.json +1 -1
- package/skills/supacloud-cli/references/command-map.md +4 -2
package/README.md
CHANGED
|
@@ -108,10 +108,12 @@ loopback development origins, with the default `:80` likewise omitted. Use
|
|
|
108
108
|
|
|
109
109
|
### Verified release controls
|
|
110
110
|
|
|
111
|
-
`release` is an official CLI entry point for
|
|
112
|
-
|
|
113
|
-
API context above
|
|
114
|
-
|
|
111
|
+
`release` is an official CLI entry point for verified Management API controls
|
|
112
|
+
and the release-canary fixture receipt replay. Management actions require the
|
|
113
|
+
Management API context above. `release_canary_fixture_stage_replay` additionally
|
|
114
|
+
requires the same project's `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`; the
|
|
115
|
+
key is sent only to that application origin and is never promoted to Management
|
|
116
|
+
authority.
|
|
115
117
|
|
|
116
118
|
Project pause and restore are explicit lifecycle commands. A logical backup
|
|
117
119
|
restore requires an operator to pause the selected project first and then use
|
|
@@ -138,6 +140,8 @@ supacloud-cli release logical_backup_restore --ref abc123 \
|
|
|
138
140
|
--restore_confirmation RESTORE_PROJECT:abc123:logical-full_abc123_<backup-id-suffix>:<64-lowercase-hex>
|
|
139
141
|
supacloud-cli release postgrest_status --ref abc123
|
|
140
142
|
supacloud-cli release postgrest_restart --ref abc123
|
|
143
|
+
supacloud-cli release release_canary_fixture_stage_replay --ref abc123 \
|
|
144
|
+
--subject <central-subject-uuid> --request_id <stage-request-uuid>
|
|
141
145
|
```
|
|
142
146
|
|
|
143
147
|
Backup creation reports success only after the CLI verifies exactly one new
|
|
@@ -158,6 +162,15 @@ receipt and reads back `desired=running`, `actual=running`, and
|
|
|
158
162
|
`health=healthy`. Both mutating controls follow the normal production
|
|
159
163
|
confirmation and read-only protections.
|
|
160
164
|
|
|
165
|
+
`release_canary_fixture_stage_replay` performs one non-retried, response-bounded
|
|
166
|
+
call to the fixed `fa_release_canary_fixture_stage` PostgREST RPC with both
|
|
167
|
+
bearer and `apikey` service-role headers. It accepts only canonical subject and
|
|
168
|
+
request UUIDs, requires a strict `staged` and `idempotent=true` four-field
|
|
169
|
+
receipt, and emits only that safe projection. Before and after the call, the CLI
|
|
170
|
+
reads the selected project's authoritative endpoint projection and requires the
|
|
171
|
+
configured application origin to match its API origin or alias. Production
|
|
172
|
+
confirmation is mandatory.
|
|
173
|
+
|
|
161
174
|
The legacy `.env` fallback is unclassified and therefore does not enable the
|
|
162
175
|
production confirmation gate. Production automation must select a `prod` or
|
|
163
176
|
`production` profile, or set `SUPACLOUD_ENV=production` together with a complete
|
package/dist/index.js
CHANGED
|
@@ -6491,7 +6491,7 @@ var ACTION_POLICY = {
|
|
|
6491
6491
|
mutations: { read: ["status"] },
|
|
6492
6492
|
release: {
|
|
6493
6493
|
read: ["logical_backup_list", "postgrest_status"],
|
|
6494
|
-
write: ["logical_backup_create", "logical_backup_restore", "postgrest_restart"]
|
|
6494
|
+
write: ["logical_backup_create", "logical_backup_restore", "postgrest_restart", "release_canary_fixture_stage_replay"]
|
|
6495
6495
|
},
|
|
6496
6496
|
secrets: { read: ["list"], write: ["upsert", "delete"] },
|
|
6497
6497
|
frontend: {
|
|
@@ -6805,14 +6805,17 @@ async function responseJsonOrNull(response) {
|
|
|
6805
6805
|
class HttpTransport {
|
|
6806
6806
|
baseUrl;
|
|
6807
6807
|
token;
|
|
6808
|
+
apiKey;
|
|
6808
6809
|
constructor(config) {
|
|
6809
6810
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
6810
6811
|
this.token = config.token;
|
|
6812
|
+
this.apiKey = config.apiKey ?? "";
|
|
6811
6813
|
}
|
|
6812
6814
|
headers() {
|
|
6813
6815
|
return {
|
|
6814
6816
|
Authorization: `Bearer ${this.token}`,
|
|
6815
|
-
"Content-Type": "application/json"
|
|
6817
|
+
"Content-Type": "application/json",
|
|
6818
|
+
...this.apiKey ? { apikey: this.apiKey } : {}
|
|
6816
6819
|
};
|
|
6817
6820
|
}
|
|
6818
6821
|
async mutationWithResponseReader(method, path, serializedBody, responseReader, timeoutMs = DEFAULT_TIMEOUT) {
|
|
@@ -7971,22 +7974,20 @@ var safeAuthMutationCodes = new Set([
|
|
|
7971
7974
|
var MAX_AUTH_READ_BYTES = 64 * 1024;
|
|
7972
7975
|
var AUTH_READ_TIMEOUT_MS = 5000;
|
|
7973
7976
|
var USER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7974
|
-
var AUTH_LINK_TYPES = [
|
|
7975
|
-
"signup",
|
|
7976
|
-
"magiclink",
|
|
7977
|
-
"recovery",
|
|
7978
|
-
"invite",
|
|
7979
|
-
"email_change",
|
|
7980
|
-
"email_change_current",
|
|
7981
|
-
"email_change_new"
|
|
7982
|
-
];
|
|
7977
|
+
var AUTH_LINK_TYPES = ["magiclink", "recovery", "invite"];
|
|
7983
7978
|
var SAFE_USER_FIELDS = [
|
|
7984
|
-
"id",
|
|
7985
7979
|
"email",
|
|
7986
7980
|
"phone",
|
|
7987
7981
|
"created_at",
|
|
7988
7982
|
"last_sign_in_at"
|
|
7989
7983
|
];
|
|
7984
|
+
var MAX_AUTH_SEARCH_LENGTH = 256;
|
|
7985
|
+
var MAX_AUTH_EMAIL_LENGTH = 320;
|
|
7986
|
+
var MAX_AUTH_PHONE_LENGTH = 64;
|
|
7987
|
+
var MAX_AUTH_TIMESTAMP_LENGTH = 64;
|
|
7988
|
+
var MAX_AUTH_REDIRECT_LENGTH = 4096;
|
|
7989
|
+
var MAX_AUTH_ACTION_LINK_LENGTH = 8192;
|
|
7990
|
+
var INVALID_FIELD = Symbol("invalid-auth-field");
|
|
7990
7991
|
function parseAuthConfig(input) {
|
|
7991
7992
|
if (typeof input !== "string")
|
|
7992
7993
|
return input;
|
|
@@ -8065,18 +8066,32 @@ function boundedPerPage(candidate) {
|
|
|
8065
8066
|
}
|
|
8066
8067
|
return candidate;
|
|
8067
8068
|
}
|
|
8069
|
+
function boundedText(candidate, field, maxLength) {
|
|
8070
|
+
if (typeof candidate !== "string")
|
|
8071
|
+
throw new Error(`'${field}' must be a string`);
|
|
8072
|
+
const value = candidate.trim();
|
|
8073
|
+
if (!value || value.length > maxLength || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
8074
|
+
throw new Error(`'${field}' is invalid or exceeds ${maxLength} characters`);
|
|
8075
|
+
}
|
|
8076
|
+
return value;
|
|
8077
|
+
}
|
|
8078
|
+
function boundedSearch(candidate, field) {
|
|
8079
|
+
return boundedText(candidate, field, MAX_AUTH_SEARCH_LENGTH);
|
|
8080
|
+
}
|
|
8081
|
+
function requiredEmail(candidate) {
|
|
8082
|
+
return boundedText(candidate, "email", MAX_AUTH_EMAIL_LENGTH);
|
|
8083
|
+
}
|
|
8068
8084
|
function safeRedirectTo(candidate) {
|
|
8069
8085
|
if (candidate === undefined)
|
|
8070
8086
|
return;
|
|
8071
|
-
|
|
8072
|
-
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL");
|
|
8087
|
+
const value = boundedText(candidate, "redirect_to", MAX_AUTH_REDIRECT_LENGTH);
|
|
8073
8088
|
let uri;
|
|
8074
8089
|
try {
|
|
8075
|
-
uri = new URL(
|
|
8090
|
+
uri = new URL(value);
|
|
8076
8091
|
} catch {
|
|
8077
8092
|
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL");
|
|
8078
8093
|
}
|
|
8079
|
-
const loopback = uri.hostname === "127.0.0.1" || uri.hostname === "[::1]";
|
|
8094
|
+
const loopback = uri.hostname === "localhost" || uri.hostname.endsWith(".localhost") || uri.hostname === "127.0.0.1" || uri.hostname === "[::1]";
|
|
8080
8095
|
const validProtocol = uri.protocol === "https:" || uri.protocol === "http:" && loopback && Boolean(uri.port);
|
|
8081
8096
|
if (!validProtocol || uri.username || uri.password || uri.hash) {
|
|
8082
8097
|
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL without credentials or fragment");
|
|
@@ -8086,28 +8101,66 @@ function safeRedirectTo(candidate) {
|
|
|
8086
8101
|
function isRecord(candidate) {
|
|
8087
8102
|
return candidate !== null && typeof candidate === "object" && !Array.isArray(candidate);
|
|
8088
8103
|
}
|
|
8104
|
+
function safeOptionalUserField(candidate, field, maxLength) {
|
|
8105
|
+
if (!(field in candidate))
|
|
8106
|
+
return;
|
|
8107
|
+
const value = candidate[field];
|
|
8108
|
+
if (value === null)
|
|
8109
|
+
return null;
|
|
8110
|
+
if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/u.test(value))
|
|
8111
|
+
return INVALID_FIELD;
|
|
8112
|
+
return value;
|
|
8113
|
+
}
|
|
8089
8114
|
function projectUser(candidate) {
|
|
8090
|
-
if (!isRecord(candidate) || typeof candidate.id !== "string")
|
|
8115
|
+
if (!isRecord(candidate) || typeof candidate.id !== "string" || !USER_ID_PATTERN.test(candidate.id))
|
|
8091
8116
|
return null;
|
|
8092
|
-
const projectedUser = {};
|
|
8117
|
+
const projectedUser = { id: candidate.id.toLowerCase() };
|
|
8118
|
+
const fieldLimits = {
|
|
8119
|
+
email: MAX_AUTH_EMAIL_LENGTH,
|
|
8120
|
+
phone: MAX_AUTH_PHONE_LENGTH,
|
|
8121
|
+
created_at: MAX_AUTH_TIMESTAMP_LENGTH,
|
|
8122
|
+
last_sign_in_at: MAX_AUTH_TIMESTAMP_LENGTH
|
|
8123
|
+
};
|
|
8093
8124
|
for (const field of SAFE_USER_FIELDS) {
|
|
8094
|
-
|
|
8095
|
-
|
|
8125
|
+
const value = safeOptionalUserField(candidate, field, fieldLimits[field]);
|
|
8126
|
+
if (value === INVALID_FIELD)
|
|
8127
|
+
return null;
|
|
8128
|
+
if (value !== undefined)
|
|
8129
|
+
projectedUser[field] = value;
|
|
8096
8130
|
}
|
|
8097
8131
|
return projectedUser;
|
|
8098
8132
|
}
|
|
8099
|
-
function
|
|
8100
|
-
if (
|
|
8133
|
+
function safePaginationField(candidate) {
|
|
8134
|
+
if (candidate === undefined)
|
|
8135
|
+
return;
|
|
8136
|
+
if (candidate === null)
|
|
8137
|
+
return null;
|
|
8138
|
+
return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : INVALID_FIELD;
|
|
8139
|
+
}
|
|
8140
|
+
function projectUserList(candidate, expectedPage, expectedPerPage) {
|
|
8141
|
+
if (!isRecord(candidate) || !Array.isArray(candidate.users) || candidate.users.length > expectedPerPage)
|
|
8101
8142
|
return null;
|
|
8102
8143
|
const users = candidate.users.map(projectUser);
|
|
8103
8144
|
if (users.some((user) => user === null))
|
|
8104
8145
|
return null;
|
|
8105
|
-
const
|
|
8146
|
+
const projectedUsers = users;
|
|
8147
|
+
const userIds = projectedUsers.map((user) => user.id);
|
|
8148
|
+
if (new Set(userIds).size !== userIds.length)
|
|
8149
|
+
return null;
|
|
8150
|
+
const projectedFields = { users: projectedUsers };
|
|
8106
8151
|
for (const field of ["total", "page", "per_page", "next_page", "last_page"]) {
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8152
|
+
const value = safePaginationField(candidate[field]);
|
|
8153
|
+
if (value === INVALID_FIELD)
|
|
8154
|
+
return null;
|
|
8155
|
+
if (value !== undefined)
|
|
8156
|
+
projectedFields[field] = value;
|
|
8110
8157
|
}
|
|
8158
|
+
if (projectedFields.page !== undefined && projectedFields.page !== expectedPage)
|
|
8159
|
+
return null;
|
|
8160
|
+
if (projectedFields.per_page !== undefined && projectedFields.per_page !== expectedPerPage)
|
|
8161
|
+
return null;
|
|
8162
|
+
if (typeof projectedFields.total === "number" && projectedFields.total < projectedUsers.length)
|
|
8163
|
+
return null;
|
|
8111
8164
|
return projectedFields;
|
|
8112
8165
|
}
|
|
8113
8166
|
function actionLink(candidate) {
|
|
@@ -8117,10 +8170,17 @@ function actionLink(candidate) {
|
|
|
8117
8170
|
if (isRecord(candidate.data))
|
|
8118
8171
|
candidates.push(candidate.data.properties);
|
|
8119
8172
|
}
|
|
8120
|
-
for (const
|
|
8121
|
-
if (isRecord(
|
|
8122
|
-
|
|
8123
|
-
|
|
8173
|
+
for (const nested of candidates) {
|
|
8174
|
+
if (!isRecord(nested) || typeof nested.action_link !== "string")
|
|
8175
|
+
continue;
|
|
8176
|
+
const link = nested.action_link;
|
|
8177
|
+
if (!link || link.length > MAX_AUTH_ACTION_LINK_LENGTH || /[\u0000-\u001f\u007f]/u.test(link))
|
|
8178
|
+
continue;
|
|
8179
|
+
try {
|
|
8180
|
+
const uri = new URL(link);
|
|
8181
|
+
if ((uri.protocol === "https:" || uri.protocol === "http:") && !uri.username && !uri.password && !uri.hash && uri.toString() === link)
|
|
8182
|
+
return link;
|
|
8183
|
+
} catch {}
|
|
8124
8184
|
}
|
|
8125
8185
|
return null;
|
|
8126
8186
|
}
|
|
@@ -8144,11 +8204,8 @@ async function listUsers(http, args) {
|
|
|
8144
8204
|
const perPage = boundedPerPage(args.per_page);
|
|
8145
8205
|
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) });
|
|
8146
8206
|
for (const key of ["search", "email_like"]) {
|
|
8147
|
-
if (args[key] !== undefined)
|
|
8148
|
-
|
|
8149
|
-
throw new Error(`'${key}' must be a non-empty string`);
|
|
8150
|
-
params.set(key, args[key].trim());
|
|
8151
|
-
}
|
|
8207
|
+
if (args[key] !== undefined)
|
|
8208
|
+
params.set(key, boundedSearch(args[key], key));
|
|
8152
8209
|
}
|
|
8153
8210
|
const response = await http.get(`/v1/projects/${ref}/auth/users?${params.toString()}`, {
|
|
8154
8211
|
maxJsonBytes: MAX_AUTH_READ_BYTES,
|
|
@@ -8156,7 +8213,7 @@ async function listUsers(http, args) {
|
|
|
8156
8213
|
});
|
|
8157
8214
|
if (!response.ok)
|
|
8158
8215
|
return safeAuthReadFailure("auth.list_users", response);
|
|
8159
|
-
const users = projectUserList(response.data);
|
|
8216
|
+
const users = projectUserList(response.data, page, perPage);
|
|
8160
8217
|
if (!users)
|
|
8161
8218
|
return safeAuthReadFailure("auth.list_users", { ...response, responseReadError: true });
|
|
8162
8219
|
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.list_users", project_ref: ref, ...users }, null, 2) }] };
|
|
@@ -8171,27 +8228,43 @@ async function getUser(http, args) {
|
|
|
8171
8228
|
if (!response.ok)
|
|
8172
8229
|
return safeAuthReadFailure("auth.get_user", response);
|
|
8173
8230
|
const user = projectUser(response.data);
|
|
8174
|
-
if (!user)
|
|
8231
|
+
if (!user || user.id !== userId) {
|
|
8175
8232
|
return safeAuthReadFailure("auth.get_user", { ...response, responseReadError: true });
|
|
8233
|
+
}
|
|
8176
8234
|
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.get_user", project_ref: ref, user }, null, 2) }] };
|
|
8177
8235
|
}
|
|
8236
|
+
function generateLinkFailure(response) {
|
|
8237
|
+
const outcomeUnknown = response.responseReadError || response.transportError || response.status === 408 || response.status >= 500;
|
|
8238
|
+
return {
|
|
8239
|
+
isError: true,
|
|
8240
|
+
content: [{
|
|
8241
|
+
type: "text",
|
|
8242
|
+
text: JSON.stringify({
|
|
8243
|
+
ok: false,
|
|
8244
|
+
operation: "auth.generate_link",
|
|
8245
|
+
error: {
|
|
8246
|
+
code: outcomeUnknown ? "OUTCOME_UNKNOWN" : "HTTP_ERROR",
|
|
8247
|
+
http_status: response.transportError ? null : response.status
|
|
8248
|
+
}
|
|
8249
|
+
}, null, 2)
|
|
8250
|
+
}]
|
|
8251
|
+
};
|
|
8252
|
+
}
|
|
8178
8253
|
async function generateLink(http, args) {
|
|
8179
8254
|
const ref = requiredRef(args.ref);
|
|
8180
8255
|
if (typeof args.type !== "string" || !AUTH_LINK_TYPES.includes(args.type)) {
|
|
8181
|
-
throw new Error("'type'
|
|
8256
|
+
throw new Error("'type' must be one of magiclink, recovery, or invite for 'generate_link'");
|
|
8182
8257
|
}
|
|
8183
|
-
|
|
8184
|
-
throw new Error("'email' is required for 'generate_link'");
|
|
8185
|
-
const body = { type: args.type, email: args.email.trim() };
|
|
8258
|
+
const body = { type: args.type, email: requiredEmail(args.email) };
|
|
8186
8259
|
const redirectTo = safeRedirectTo(args.redirect_to);
|
|
8187
8260
|
if (redirectTo)
|
|
8188
8261
|
body.redirect_to = redirectTo;
|
|
8189
8262
|
const response = await http.postReleaseMutation(`/v1/projects/${ref}/auth/generate_link`, body);
|
|
8190
8263
|
if (!response.ok)
|
|
8191
|
-
return
|
|
8264
|
+
return generateLinkFailure(response);
|
|
8192
8265
|
const link = actionLink(response.data);
|
|
8193
8266
|
if (!link)
|
|
8194
|
-
return
|
|
8267
|
+
return generateLinkFailure({ ...response, responseReadError: true });
|
|
8195
8268
|
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.generate_link", action_link: link }, null, 2) }] };
|
|
8196
8269
|
}
|
|
8197
8270
|
function formatProviders(data) {
|
|
@@ -8251,7 +8324,7 @@ Actions: list_users, get_user, generate_link, list_providers, get_provider, conf
|
|
|
8251
8324
|
per_page: optional(Type.Integer({ minimum: 1, maximum: 100 }), "[list_users] Users per page (1-100)"),
|
|
8252
8325
|
search: optional(Type.String(), "[list_users] Search user email, phone, or UUID"),
|
|
8253
8326
|
email_like: optional(Type.String(), "[list_users] Search user email or phone"),
|
|
8254
|
-
type: optional(stringEnum(AUTH_LINK_TYPES), "[generate_link]
|
|
8327
|
+
type: optional(stringEnum(AUTH_LINK_TYPES), "[generate_link] magiclink, recovery, or invite"),
|
|
8255
8328
|
email: optional(Type.String(), "[generate_link] User email"),
|
|
8256
8329
|
redirect_to: optional(Type.String(), "[generate_link] Absolute HTTPS or loopback callback"),
|
|
8257
8330
|
provider: optional(Type.String(), "[*_provider] Provider name (github, google, wechat, etc.)"),
|
|
@@ -10246,15 +10319,15 @@ function hasWellFormedUnicode(text) {
|
|
|
10246
10319
|
}
|
|
10247
10320
|
return true;
|
|
10248
10321
|
}
|
|
10249
|
-
function
|
|
10322
|
+
function boundedText2(candidate, maxLength) {
|
|
10250
10323
|
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
|
|
10251
10324
|
}
|
|
10252
10325
|
function matchingText(candidate, maxLength, pattern) {
|
|
10253
|
-
const candidateText =
|
|
10326
|
+
const candidateText = boundedText2(candidate, maxLength);
|
|
10254
10327
|
return candidateText && pattern.test(candidateText) ? candidateText : null;
|
|
10255
10328
|
}
|
|
10256
10329
|
function canonicalTimestamp(candidate) {
|
|
10257
|
-
const timestamp =
|
|
10330
|
+
const timestamp = boundedText2(candidate, 64);
|
|
10258
10331
|
if (!timestamp)
|
|
10259
10332
|
return null;
|
|
10260
10333
|
const milliseconds = Date.parse(timestamp);
|
|
@@ -10266,7 +10339,7 @@ function projectedSummary(project) {
|
|
|
10266
10339
|
ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN3),
|
|
10267
10340
|
organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
10268
10341
|
organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
|
|
10269
|
-
name:
|
|
10342
|
+
name: boundedText2(project.name, 100),
|
|
10270
10343
|
region: matchingText(project.region, 64, REGION_PATTERN),
|
|
10271
10344
|
created_at: canonicalTimestamp(project.created_at),
|
|
10272
10345
|
status: matchingText(project.status, 64, STATUS_PATTERN)
|
|
@@ -10274,7 +10347,7 @@ function projectedSummary(project) {
|
|
|
10274
10347
|
return Object.values(summary).every((field) => field !== null) ? summary : null;
|
|
10275
10348
|
}
|
|
10276
10349
|
function databaseHost(candidate) {
|
|
10277
|
-
const host =
|
|
10350
|
+
const host = boundedText2(candidate, 255);
|
|
10278
10351
|
if (!host)
|
|
10279
10352
|
return null;
|
|
10280
10353
|
if (host.startsWith("[") && host.endsWith("]")) {
|
|
@@ -10314,7 +10387,7 @@ function projectEndpoint(candidate) {
|
|
|
10314
10387
|
const endpoint = plainRecord(candidate);
|
|
10315
10388
|
if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
|
|
10316
10389
|
return null;
|
|
10317
|
-
const endpointUrl =
|
|
10390
|
+
const endpointUrl = boundedText2(endpoint.url, 2048);
|
|
10318
10391
|
if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
|
|
10319
10392
|
return null;
|
|
10320
10393
|
try {
|
|
@@ -10331,7 +10404,7 @@ function projectEndpoint(candidate) {
|
|
|
10331
10404
|
function discardedDetailFieldsAreValid(project) {
|
|
10332
10405
|
if (project.config !== undefined && plainRecord(project.config) === null)
|
|
10333
10406
|
return false;
|
|
10334
|
-
if (project.anon_key !== undefined &&
|
|
10407
|
+
if (project.anon_key !== undefined && boundedText2(project.anon_key, 16384) === null)
|
|
10335
10408
|
return false;
|
|
10336
10409
|
return project.services === undefined || Array.isArray(project.services);
|
|
10337
10410
|
}
|
|
@@ -10410,11 +10483,11 @@ function plainRecord2(candidate) {
|
|
|
10410
10483
|
function hasOnlyKeys2(record, allowedKeys) {
|
|
10411
10484
|
return Object.keys(record).every((key) => allowedKeys.has(key));
|
|
10412
10485
|
}
|
|
10413
|
-
function
|
|
10486
|
+
function boundedText3(candidate, maxLength) {
|
|
10414
10487
|
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) ? candidate : null;
|
|
10415
10488
|
}
|
|
10416
10489
|
function canonicalHost(candidate, scheme) {
|
|
10417
|
-
const host =
|
|
10490
|
+
const host = boundedText3(candidate, 255);
|
|
10418
10491
|
if (!host)
|
|
10419
10492
|
return null;
|
|
10420
10493
|
try {
|
|
@@ -10429,8 +10502,8 @@ function projectEndpoint2(candidate) {
|
|
|
10429
10502
|
if (!endpoint || !hasOnlyKeys2(endpoint, ENDPOINT_KEYS))
|
|
10430
10503
|
return null;
|
|
10431
10504
|
const scheme = endpoint.scheme === "http" || endpoint.scheme === "https" ? endpoint.scheme : null;
|
|
10432
|
-
const origin =
|
|
10433
|
-
const source =
|
|
10505
|
+
const origin = boundedText3(endpoint.origin, 2048);
|
|
10506
|
+
const source = boundedText3(endpoint.source, 64);
|
|
10434
10507
|
if (!scheme || !origin || !source || !PROJECT_ENDPOINT_SOURCES.has(source))
|
|
10435
10508
|
return null;
|
|
10436
10509
|
let parsedOrigin;
|
|
@@ -10492,6 +10565,15 @@ function projectEndpointRead(response, expectedRef) {
|
|
|
10492
10565
|
const projection = projectEndpointProjection(response.data);
|
|
10493
10566
|
return projection && projection.project_ref === expectedRef ? successfulResult2(projection) : failedResult2("Invalid project endpoint response");
|
|
10494
10567
|
}
|
|
10568
|
+
function projectApiOrigins(response, expectedRef) {
|
|
10569
|
+
if (!successfulResponse2(response))
|
|
10570
|
+
return null;
|
|
10571
|
+
const projection = projectEndpointProjection(response.data);
|
|
10572
|
+
if (!projection || projection.project_ref !== expectedRef)
|
|
10573
|
+
return null;
|
|
10574
|
+
const api = projection.endpoints.api;
|
|
10575
|
+
return [api.origin, ...api.aliases.map((alias) => `${api.scheme}://${alias}`)];
|
|
10576
|
+
}
|
|
10495
10577
|
|
|
10496
10578
|
// src/shared/tools/project-cli-tools.ts
|
|
10497
10579
|
function projectReadResponse(readResult) {
|
|
@@ -12759,6 +12841,9 @@ var INVENTORY_MAX_BYTES = 1024 * 1024;
|
|
|
12759
12841
|
var MUTATION_MAX_BYTES = 64 * 1024;
|
|
12760
12842
|
var BACKUP_TIMEOUT_MS = 36 * 60000;
|
|
12761
12843
|
var RELEASE_READ_RESPONSE_TIMEOUT_MS = 5000;
|
|
12844
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12845
|
+
var RELEASE_CANARY_TENANT_KEY = /^release-canary-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12846
|
+
var RELEASE_CANARY_STAGE_RECEIPT_KEYS = new Set(["fixtureId", "tenantKey", "state", "idempotent"]);
|
|
12762
12847
|
function isRecord3(value) {
|
|
12763
12848
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12764
12849
|
}
|
|
@@ -12891,20 +12976,44 @@ function readPostgrestFailure(operation, read) {
|
|
|
12891
12976
|
function isRestartReceipt(value) {
|
|
12892
12977
|
return isRecord3(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
|
|
12893
12978
|
}
|
|
12979
|
+
function releaseCanaryStageInput(subject, requestId) {
|
|
12980
|
+
if (typeof subject !== "string" || !UUID.test(subject))
|
|
12981
|
+
throw new Error("'subject' must be a canonical UUID");
|
|
12982
|
+
if (typeof requestId !== "string" || !UUID.test(requestId))
|
|
12983
|
+
throw new Error("'request_id' must be a canonical UUID");
|
|
12984
|
+
return { p_subject: subject, p_request_id: requestId };
|
|
12985
|
+
}
|
|
12986
|
+
function releaseCanaryStageReceipt(value) {
|
|
12987
|
+
if (!isRecord3(value) || Object.keys(value).some((key) => !RELEASE_CANARY_STAGE_RECEIPT_KEYS.has(key)) || Object.keys(value).length !== RELEASE_CANARY_STAGE_RECEIPT_KEYS.size || typeof value.fixtureId !== "string" || !UUID.test(value.fixtureId) || typeof value.tenantKey !== "string" || !RELEASE_CANARY_TENANT_KEY.test(value.tenantKey) || value.state !== "staged" || value.idempotent !== true)
|
|
12988
|
+
return null;
|
|
12989
|
+
return {
|
|
12990
|
+
fixtureId: value.fixtureId,
|
|
12991
|
+
tenantKey: value.tenantKey,
|
|
12992
|
+
state: "staged",
|
|
12993
|
+
idempotent: true
|
|
12994
|
+
};
|
|
12995
|
+
}
|
|
12996
|
+
async function applicationOriginMatches(http, projectRef2, applicationOrigin) {
|
|
12997
|
+
const endpointRead = await http.get(`${endpoint(projectRef2)}/endpoint/projection`, { maxResponseBytes: PROJECT_ENDPOINT_RESPONSE_MAX_BYTES });
|
|
12998
|
+
return projectApiOrigins(endpointRead, projectRef2)?.includes(applicationOrigin) === true;
|
|
12999
|
+
}
|
|
12894
13000
|
function registerReleaseTools(server, http, options = {}) {
|
|
12895
|
-
server.tool("release", "Verified release controls
|
|
13001
|
+
server.tool("release", "Verified release controls. Management actions use the Management API; release_canary_fixture_stage_replay additionally requires the selected project's SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY.", {
|
|
12896
13002
|
action: withDescription(stringEnum([
|
|
12897
13003
|
"logical_backup_list",
|
|
12898
13004
|
"logical_backup_create",
|
|
12899
13005
|
"logical_backup_restore",
|
|
12900
13006
|
"postgrest_status",
|
|
12901
|
-
"postgrest_restart"
|
|
13007
|
+
"postgrest_restart",
|
|
13008
|
+
"release_canary_fixture_stage_replay"
|
|
12902
13009
|
]), "Release control action"),
|
|
12903
13010
|
ref: optional(Type.String(), options.projectRef ? "Optional override when not auto-linked" : "Project ref"),
|
|
12904
13011
|
backup_id: optional(Type.String(), "[logical_backup_restore] Exact verified logical-full backup ID from the selected project inventory"),
|
|
12905
13012
|
expected_sha256: optional(Type.String(), "[logical_backup_restore] Exact lowercase SHA-256 from the selected project inventory"),
|
|
12906
|
-
restore_confirmation: optional(Type.String(), "[logical_backup_restore] Exact RESTORE_PROJECT:<ref>:<backup_id>:<sha256> confirmation")
|
|
12907
|
-
|
|
13013
|
+
restore_confirmation: optional(Type.String(), "[logical_backup_restore] Exact RESTORE_PROJECT:<ref>:<backup_id>:<sha256> confirmation"),
|
|
13014
|
+
subject: optional(Type.String(), "[release_canary_fixture_stage_replay] Exact central subject UUID"),
|
|
13015
|
+
request_id: optional(Type.String(), "[release_canary_fixture_stage_replay] Exact idempotent stage request UUID")
|
|
13016
|
+
}, async ({ action, ref, backup_id, expected_sha256, restore_confirmation, subject, request_id }) => {
|
|
12908
13017
|
const projectRef2 = typeof ref === "string" && ref || options.projectRef;
|
|
12909
13018
|
if (!projectRef2)
|
|
12910
13019
|
throw new Error("'ref' is required for release controls");
|
|
@@ -12975,6 +13084,27 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12975
13084
|
postgrest: read2.status
|
|
12976
13085
|
});
|
|
12977
13086
|
}
|
|
13087
|
+
if (action === "release_canary_fixture_stage_replay") {
|
|
13088
|
+
if (!options.applicationHttp || !options.applicationOrigin) {
|
|
13089
|
+
throw new Error("release_canary_fixture_stage_replay requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY");
|
|
13090
|
+
}
|
|
13091
|
+
const request = releaseCanaryStageInput(subject, request_id);
|
|
13092
|
+
if (!await applicationOriginMatches(http, projectRef2, options.applicationOrigin)) {
|
|
13093
|
+
return releaseControlFailure("release.release_canary.fixture_stage_replay", "INVALID_RESPONSE", null);
|
|
13094
|
+
}
|
|
13095
|
+
const response = await options.applicationHttp.postReleaseMutation("/rest/v1/rpc/fa_release_canary_fixture_stage", request);
|
|
13096
|
+
if (!response.ok || response.status !== 200) {
|
|
13097
|
+
return mutationFailure("release.release_canary.fixture_stage_replay", response);
|
|
13098
|
+
}
|
|
13099
|
+
const receipt = releaseCanaryStageReceipt(response.data);
|
|
13100
|
+
if (!receipt || !await applicationOriginMatches(http, projectRef2, options.applicationOrigin)) {
|
|
13101
|
+
return releaseControlFailure("release.release_canary.fixture_stage_replay", "OUTCOME_UNKNOWN", response.status);
|
|
13102
|
+
}
|
|
13103
|
+
return releaseControlSuccess("release.release_canary.fixture_stage_replay", {
|
|
13104
|
+
project_ref: projectRef2,
|
|
13105
|
+
receipt
|
|
13106
|
+
});
|
|
13107
|
+
}
|
|
12978
13108
|
if (action !== "postgrest_restart")
|
|
12979
13109
|
throw new Error("Unknown release control action");
|
|
12980
13110
|
const mutation = await http.postReleaseMutation(`${endpoint(projectRef2)}/services/postgrest/restart`);
|
|
@@ -12995,7 +13125,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12995
13125
|
// package.json
|
|
12996
13126
|
var package_default = {
|
|
12997
13127
|
name: "@supacloud/cli",
|
|
12998
|
-
version: "0.
|
|
13128
|
+
version: "0.29.0",
|
|
12999
13129
|
description: "Project-scoped CLI for SupaCloud users",
|
|
13000
13130
|
type: "module",
|
|
13001
13131
|
main: "./dist/index.js",
|
|
@@ -13224,7 +13354,8 @@ DEFAULT CONTEXT
|
|
|
13224
13354
|
Without a selector or project variables, runs use the current project's legacy .env.
|
|
13225
13355
|
Application status accepts SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY.
|
|
13226
13356
|
Management-backed project commands require SUPACLOUD_API_URL +
|
|
13227
|
-
SUPACLOUD_API_TOKEN.
|
|
13357
|
+
SUPACLOUD_API_TOKEN. Only release release_canary_fixture_stage_replay may additionally use
|
|
13358
|
+
the selected project's SUPABASE_* pair for one application-origin RPC.
|
|
13228
13359
|
SUPACLOUD_PROJECT_REF is required when it cannot be inferred from <ref>.api.*.
|
|
13229
13360
|
|
|
13230
13361
|
SUPACLOUD_READ_ONLY=true blocks remote writes. Production writes require an
|
|
@@ -13245,6 +13376,7 @@ EXAMPLES
|
|
|
13245
13376
|
${preferredCommand} release logical_backup_restore --ref abc123 --backup_id <backup_id> --expected_sha256 <sha256> --restore_confirmation RESTORE_PROJECT:abc123:<backup_id>:<sha256>
|
|
13246
13377
|
${preferredCommand} release postgrest_status --ref abc123
|
|
13247
13378
|
${preferredCommand} release postgrest_restart --ref abc123
|
|
13379
|
+
${preferredCommand} release release_canary_fixture_stage_replay --ref abc123 --subject <uuid> --request_id <uuid>
|
|
13248
13380
|
${preferredCommand} queue stats --queue emails
|
|
13249
13381
|
${preferredCommand} queue dlq --queue emails --limit 20
|
|
13250
13382
|
${preferredCommand} frontend list --ref abc123
|
|
@@ -13403,6 +13535,11 @@ function createCliTools(context, confirmProduction) {
|
|
|
13403
13535
|
baseUrl: context.apiUrl,
|
|
13404
13536
|
token: context.apiToken
|
|
13405
13537
|
});
|
|
13538
|
+
const applicationHttp = context.inferredSupabaseUrl && context.inferredServiceRoleKey ? new HttpTransport({
|
|
13539
|
+
baseUrl: context.inferredSupabaseUrl,
|
|
13540
|
+
token: context.inferredServiceRoleKey,
|
|
13541
|
+
apiKey: context.inferredServiceRoleKey
|
|
13542
|
+
}) : undefined;
|
|
13406
13543
|
const assign = (extra) => Object.assign(tools, extra);
|
|
13407
13544
|
assign(captureTools((server) => registerUserProjectCliTools(server, http, {
|
|
13408
13545
|
projectRef: context.projectRef || undefined
|
|
@@ -13424,7 +13561,9 @@ function createCliTools(context, confirmProduction) {
|
|
|
13424
13561
|
})));
|
|
13425
13562
|
assign(captureTools((server) => registerMutationTools(server, http)));
|
|
13426
13563
|
assign(captureTools((server) => registerReleaseTools(server, http, {
|
|
13427
|
-
projectRef: context.projectRef || undefined
|
|
13564
|
+
projectRef: context.projectRef || undefined,
|
|
13565
|
+
applicationHttp,
|
|
13566
|
+
applicationOrigin: context.inferredSupabaseUrl || undefined
|
|
13428
13567
|
})));
|
|
13429
13568
|
assign(captureTools((server) => registerFrontendTools(server, http)));
|
|
13430
13569
|
assign(captureTools((server) => registerGatewayTools(server, http, {
|
package/package.json
CHANGED
|
@@ -15,9 +15,10 @@ Load this reference when selecting a command surface or when a user asks an AI t
|
|
|
15
15
|
| Generate migration from local schema changes | `supabase db_diff` | Review generated SQL before use |
|
|
16
16
|
| Rebuild local database | `supabase db_reset` | Local only; preserve required seed behavior |
|
|
17
17
|
| Inspect or back up a remote database | `supabase db_pull`, `migration_list`, `db_dump`, `gen_types` | Requires explicit PostgreSQL DSN; redact it |
|
|
18
|
+
| Replay the release-canary fixture stage receipt | `release release_canary_fixture_stage_replay` | Dual-bind Management project context and that project's service-role application origin; accepts only the exact subject/request UUID pair and a strict idempotent receipt |
|
|
18
19
|
| Preview/apply migrations remotely | `supabase push` | Always dry-run first; production needs explicit approval |
|
|
19
20
|
| Mark proven-equivalent historical migrations as applied | `database baseline_migrations` | Dry-run, schema-equivalence proof, backup, explicit approval |
|
|
20
|
-
| Inspect auth users or generate a controlled login link | `auth list_users`, `auth get_user`, `auth generate_link` | User reads are bounded;
|
|
21
|
+
| Inspect auth users or generate a controlled login link | `auth list_users`, `auth get_user`, `auth generate_link` | User reads are bounded; generation supports only `magiclink`, `recovery`, and `invite`, requires production confirmation, and returns only a validated action URL |
|
|
21
22
|
| Manage Auth/Storage/Edge Functions/frontend/secrets | Corresponding project module | Keep deployable config/code in version control |
|
|
22
23
|
| Configure project gateway routes | `gateway` | Requires an admin-capable project token; inspect before write |
|
|
23
24
|
| Install/upgrade/debug SupaCloud servers | `supacloud-admin` | Platform boundary; not a project CLI action |
|
|
@@ -32,13 +33,14 @@ until a project-scoped context is resolved.
|
|
|
32
33
|
- `project`: selected-project metadata, authoritative endpoint projection, health, logs, API keys/settings, background tasks, retry/cancel, DLQ, and background settings. `project list` deliberately redirects to `supacloud-admin`; cross-project enumeration is not a project CLI capability.
|
|
33
34
|
- `database`: read/query, schema inspection, extensions, indexes, RLS, stats, migration push, controlled historical baseline, and SQL-file execution.
|
|
34
35
|
- `supabase`: allowlisted official CLI adapter for migration authoring, local reset/diff, explicit-DSN inspection/backup/type generation, and SupaCloud-controlled migration push.
|
|
35
|
-
- `auth`: provider/configuration plus bounded user lookup and production-confirmed
|
|
36
|
+
- `auth`: provider/configuration plus bounded user lookup and production-confirmed `magiclink`, `recovery`, or `invite` generation. Search/email/redirect inputs and returned action URLs are bounded and validated before use.
|
|
36
37
|
- `storage`: buckets and object-management workflows.
|
|
37
38
|
- `edge_functions`: list, atomically read one active or deleted identity with `get_config`, read immutable source, deploy, activate, configure, and delete Edge Functions. For every mutation, pass the `activation_id` read from the same `list` or `get_config` snapshot as `--expected-activation-id`; use `legacy` only for a never-created or listed legacy Function, not for a deleted slug with a tombstone UUID. Deploy and activate actions also require the non-negative observed version as `--expected-active-version`; use `absent` for a never-created slug or a `get_config` tombstone. Version `0` is a legacy version token and cannot be used as a source or activation target.
|
|
38
39
|
- `frontend`: list, build/deploy, domain, and deployment workflows.
|
|
39
40
|
- `secrets`: project secret management; never print values after write.
|
|
40
41
|
- `queue`, `task_events`, `diagnostics`: asynchronous workload operations and bounded diagnostics.
|
|
41
42
|
- `gateway`: project route/config/rebuild operations.
|
|
43
|
+
- `release`: verified backup/PostgREST lifecycle controls plus a production-confirmed, non-retried, strict release-canary fixture stage replay. The replay requires both the selected Management project binding and that project's `SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY`.
|
|
42
44
|
- `ai`: inspect or install this packaged Skill.
|
|
43
45
|
|
|
44
46
|
## Safe inspection pattern
|