@supacloud/admin 0.10.2 → 0.12.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 +2 -0
- package/dist/index.js +522 -15
- package/dist/sshcrypto-vd2k5hq9.node +0 -0
- package/package.json +1 -1
- package/dist/sshcrypto-8m50vnmb.node +0 -0
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ npx @supacloud/admin project create --name my-app --domain example.com \
|
|
|
80
80
|
--env_file /secure/path/.env.project-credentials.test --environment test
|
|
81
81
|
npx @supacloud/admin project list
|
|
82
82
|
npx @supacloud/admin project services --ref abc123
|
|
83
|
+
npx @supacloud/admin project runtime_snapshot --ref abc123
|
|
83
84
|
npx @supacloud/admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
84
85
|
```
|
|
85
86
|
|
|
@@ -167,6 +168,7 @@ Project commands owned by this CLI:
|
|
|
167
168
|
- `project restart`
|
|
168
169
|
- `project update_settings`
|
|
169
170
|
- `project services` — read-only project service inventory
|
|
171
|
+
- `project runtime_snapshot` — strict read-only runtime revision and PostgREST attestation snapshot
|
|
170
172
|
- `project service_control` — constrained project service lifecycle control
|
|
171
173
|
|
|
172
174
|
`project create` never prints project credentials. Pass an absolute
|
package/dist/index.js
CHANGED
|
@@ -4759,7 +4759,7 @@ var require_utils = __commonJS((exports, module) => {
|
|
|
4759
4759
|
|
|
4760
4760
|
// node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
|
|
4761
4761
|
var require_sshcrypto = __commonJS((exports, module) => {
|
|
4762
|
-
module.exports = __require("./sshcrypto-
|
|
4762
|
+
module.exports = __require("./sshcrypto-vd2k5hq9.node");
|
|
4763
4763
|
});
|
|
4764
4764
|
|
|
4765
4765
|
// node_modules/ssh2/lib/protocol/crypto/poly1305.js
|
|
@@ -25788,7 +25788,17 @@ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selecti
|
|
|
25788
25788
|
// src/shared/execution-policy.ts
|
|
25789
25789
|
var ACTION_POLICY = {
|
|
25790
25790
|
project: {
|
|
25791
|
-
read: [
|
|
25791
|
+
read: [
|
|
25792
|
+
"list",
|
|
25793
|
+
"get",
|
|
25794
|
+
"settings",
|
|
25795
|
+
"api_keys",
|
|
25796
|
+
"health",
|
|
25797
|
+
"logs",
|
|
25798
|
+
"tasks",
|
|
25799
|
+
"services",
|
|
25800
|
+
"runtime_snapshot"
|
|
25801
|
+
],
|
|
25792
25802
|
write: ["create", "delete", "pause", "restore", "restart", "update_settings"]
|
|
25793
25803
|
},
|
|
25794
25804
|
platform: {
|
|
@@ -26023,6 +26033,14 @@ function transportFailure(error) {
|
|
|
26023
26033
|
transportError: true
|
|
26024
26034
|
};
|
|
26025
26035
|
}
|
|
26036
|
+
function responseBodyFailure(status) {
|
|
26037
|
+
return {
|
|
26038
|
+
ok: false,
|
|
26039
|
+
status,
|
|
26040
|
+
data: { error: "Invalid Response", code: "INVALID_RESPONSE" },
|
|
26041
|
+
responseError: true
|
|
26042
|
+
};
|
|
26043
|
+
}
|
|
26026
26044
|
function validatedPostTimeout(options) {
|
|
26027
26045
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
26028
26046
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_POST_TIMEOUT_MS) {
|
|
@@ -26030,12 +26048,90 @@ function validatedPostTimeout(options) {
|
|
|
26030
26048
|
}
|
|
26031
26049
|
return timeoutMs;
|
|
26032
26050
|
}
|
|
26051
|
+
function validatedGetResponseLimit(options) {
|
|
26052
|
+
const maxBytes = options.maxResponseBytes;
|
|
26053
|
+
if (maxBytes === undefined)
|
|
26054
|
+
return;
|
|
26055
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
26056
|
+
throw new RangeError("HTTP response limit must be a positive safe integer");
|
|
26057
|
+
}
|
|
26058
|
+
return maxBytes;
|
|
26059
|
+
}
|
|
26060
|
+
function validatedStrictJsonLimit(options) {
|
|
26061
|
+
const maxBytes = options.maxJsonBytes;
|
|
26062
|
+
if (maxBytes === undefined)
|
|
26063
|
+
return;
|
|
26064
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
26065
|
+
throw new RangeError("HTTP JSON response byte limit must be a positive safe integer");
|
|
26066
|
+
}
|
|
26067
|
+
return maxBytes;
|
|
26068
|
+
}
|
|
26069
|
+
function responseExceedsDeclaredLimit(response, maxBytes) {
|
|
26070
|
+
const contentLength = response.headers.get("content-length");
|
|
26071
|
+
return contentLength !== null && /^\d+$/u.test(contentLength) && Number(contentLength) > maxBytes;
|
|
26072
|
+
}
|
|
26073
|
+
function joinedResponseBytes(chunks, totalBytes) {
|
|
26074
|
+
const responseBytes = new Uint8Array(totalBytes);
|
|
26075
|
+
let offset = 0;
|
|
26076
|
+
for (const chunk of chunks) {
|
|
26077
|
+
responseBytes.set(chunk, offset);
|
|
26078
|
+
offset += chunk.byteLength;
|
|
26079
|
+
}
|
|
26080
|
+
return responseBytes;
|
|
26081
|
+
}
|
|
26082
|
+
async function boundedResponseBytes(response, maxBytes) {
|
|
26083
|
+
if (responseExceedsDeclaredLimit(response, maxBytes)) {
|
|
26084
|
+
response.body?.cancel().catch(() => {
|
|
26085
|
+
return;
|
|
26086
|
+
});
|
|
26087
|
+
return null;
|
|
26088
|
+
}
|
|
26089
|
+
if (!response.body)
|
|
26090
|
+
return new Uint8Array;
|
|
26091
|
+
const reader = response.body.getReader();
|
|
26092
|
+
const chunks = [];
|
|
26093
|
+
let totalBytes = 0;
|
|
26094
|
+
while (true) {
|
|
26095
|
+
const { done, value } = await reader.read();
|
|
26096
|
+
if (done)
|
|
26097
|
+
return joinedResponseBytes(chunks, totalBytes);
|
|
26098
|
+
totalBytes += value.byteLength;
|
|
26099
|
+
if (totalBytes > maxBytes) {
|
|
26100
|
+
reader.cancel().catch(() => {
|
|
26101
|
+
return;
|
|
26102
|
+
});
|
|
26103
|
+
return null;
|
|
26104
|
+
}
|
|
26105
|
+
chunks.push(value);
|
|
26106
|
+
}
|
|
26107
|
+
}
|
|
26108
|
+
async function boundedResponseJson(response, maxBytes) {
|
|
26109
|
+
const responseBytes = await boundedResponseBytes(response, maxBytes);
|
|
26110
|
+
if (responseBytes === null)
|
|
26111
|
+
return null;
|
|
26112
|
+
try {
|
|
26113
|
+
const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
|
|
26114
|
+
return JSON.parse(responseText);
|
|
26115
|
+
} catch (error) {
|
|
26116
|
+
if (error instanceof SyntaxError || error instanceof TypeError)
|
|
26117
|
+
return null;
|
|
26118
|
+
throw error;
|
|
26119
|
+
}
|
|
26120
|
+
}
|
|
26121
|
+
async function strictBoundedResponseJson(response, maxBytes) {
|
|
26122
|
+
const responseBytes = await boundedResponseBytes(response, maxBytes);
|
|
26123
|
+
if (responseBytes === null)
|
|
26124
|
+
throw new Error("HTTP JSON response exceeded its byte limit");
|
|
26125
|
+
const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
|
|
26126
|
+
return JSON.parse(responseText);
|
|
26127
|
+
}
|
|
26033
26128
|
async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_TIMEOUT) {
|
|
26034
26129
|
const controller = new AbortController;
|
|
26035
26130
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
26036
26131
|
try {
|
|
26037
26132
|
return await fetch(url, {
|
|
26038
26133
|
...options,
|
|
26134
|
+
redirect: "error",
|
|
26039
26135
|
signal: controller.signal
|
|
26040
26136
|
});
|
|
26041
26137
|
} finally {
|
|
@@ -26078,17 +26174,35 @@ class HttpTransport {
|
|
|
26078
26174
|
"Content-Type": "application/json"
|
|
26079
26175
|
};
|
|
26080
26176
|
}
|
|
26081
|
-
async get(path) {
|
|
26177
|
+
async get(path, options = {}) {
|
|
26178
|
+
const maxResponseBytes = validatedGetResponseLimit(options);
|
|
26179
|
+
const maxJsonBytes = validatedStrictJsonLimit(options);
|
|
26180
|
+
if (maxResponseBytes !== undefined && maxJsonBytes !== undefined) {
|
|
26181
|
+
throw new RangeError("HTTP response limit options are mutually exclusive");
|
|
26182
|
+
}
|
|
26183
|
+
let response;
|
|
26082
26184
|
try {
|
|
26083
|
-
|
|
26185
|
+
response = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
26084
26186
|
method: "GET",
|
|
26085
26187
|
headers: this.headers()
|
|
26086
26188
|
});
|
|
26087
|
-
const data = await res.json().catch(() => null);
|
|
26088
|
-
return { ok: res.ok, status: res.status, data };
|
|
26089
26189
|
} catch (error) {
|
|
26090
26190
|
return transportFailure(error);
|
|
26091
26191
|
}
|
|
26192
|
+
if (maxJsonBytes !== undefined) {
|
|
26193
|
+
try {
|
|
26194
|
+
const data2 = await strictBoundedResponseJson(response, maxJsonBytes);
|
|
26195
|
+
return { ok: response.ok, status: response.status, data: data2 };
|
|
26196
|
+
} catch {
|
|
26197
|
+
return responseBodyFailure(response.status);
|
|
26198
|
+
}
|
|
26199
|
+
}
|
|
26200
|
+
if (maxResponseBytes !== undefined) {
|
|
26201
|
+
const data2 = await boundedResponseJson(response, maxResponseBytes);
|
|
26202
|
+
return { ok: response.ok, status: response.status, data: data2 };
|
|
26203
|
+
}
|
|
26204
|
+
const data = await response.json().catch(() => null);
|
|
26205
|
+
return { ok: response.ok, status: response.status, data };
|
|
26092
26206
|
}
|
|
26093
26207
|
async post(path, body, options) {
|
|
26094
26208
|
const timeoutMs = validatedPostTimeout(options);
|
|
@@ -29717,6 +29831,375 @@ function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expec
|
|
|
29717
29831
|
return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
|
|
29718
29832
|
}
|
|
29719
29833
|
|
|
29834
|
+
// ../cli/src/shared/tools/project-read-projection.ts
|
|
29835
|
+
var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
|
|
29836
|
+
var PROJECT_REF_PATTERN = /^[a-z0-9-]{1,20}$/;
|
|
29837
|
+
var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
29838
|
+
var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
29839
|
+
var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
29840
|
+
var DNS_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
|
|
29841
|
+
var DATABASE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
29842
|
+
var MAX_PROJECTS = 1e4;
|
|
29843
|
+
var PROJECT_SUMMARY_KEYS = new Set([
|
|
29844
|
+
"id",
|
|
29845
|
+
"ref",
|
|
29846
|
+
"organization_id",
|
|
29847
|
+
"organization_slug",
|
|
29848
|
+
"name",
|
|
29849
|
+
"region",
|
|
29850
|
+
"created_at",
|
|
29851
|
+
"status"
|
|
29852
|
+
]);
|
|
29853
|
+
var PROJECT_DETAILS_KEYS = new Set([
|
|
29854
|
+
...PROJECT_SUMMARY_KEYS,
|
|
29855
|
+
"database",
|
|
29856
|
+
"api",
|
|
29857
|
+
"studio",
|
|
29858
|
+
"config",
|
|
29859
|
+
"anon_key",
|
|
29860
|
+
"services"
|
|
29861
|
+
]);
|
|
29862
|
+
var PROJECT_DATABASE_KEYS = new Set([
|
|
29863
|
+
"host",
|
|
29864
|
+
"version",
|
|
29865
|
+
"postgres_engine",
|
|
29866
|
+
"release_channel"
|
|
29867
|
+
]);
|
|
29868
|
+
var PROJECT_ENDPOINT_KEYS = new Set(["url"]);
|
|
29869
|
+
function plainRecord(candidate) {
|
|
29870
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
29871
|
+
return null;
|
|
29872
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
29873
|
+
return prototype === Object.prototype || prototype === null ? candidate : null;
|
|
29874
|
+
}
|
|
29875
|
+
function hasOnlyKeys(record, allowedKeys) {
|
|
29876
|
+
return Object.keys(record).every((key) => allowedKeys.has(key));
|
|
29877
|
+
}
|
|
29878
|
+
function hasWellFormedUnicode(text) {
|
|
29879
|
+
for (let index = 0;index < text.length; index++) {
|
|
29880
|
+
const codeUnit = text.charCodeAt(index);
|
|
29881
|
+
if (codeUnit >= 55296 && codeUnit <= 56319) {
|
|
29882
|
+
if (index + 1 >= text.length)
|
|
29883
|
+
return false;
|
|
29884
|
+
const lowSurrogate = text.charCodeAt(index + 1);
|
|
29885
|
+
if (lowSurrogate < 56320 || lowSurrogate > 57343)
|
|
29886
|
+
return false;
|
|
29887
|
+
index++;
|
|
29888
|
+
} else if (codeUnit >= 56320 && codeUnit <= 57343) {
|
|
29889
|
+
return false;
|
|
29890
|
+
}
|
|
29891
|
+
}
|
|
29892
|
+
return true;
|
|
29893
|
+
}
|
|
29894
|
+
function boundedText(candidate, maxLength) {
|
|
29895
|
+
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
|
|
29896
|
+
}
|
|
29897
|
+
function matchingText(candidate, maxLength, pattern) {
|
|
29898
|
+
const candidateText = boundedText(candidate, maxLength);
|
|
29899
|
+
return candidateText && pattern.test(candidateText) ? candidateText : null;
|
|
29900
|
+
}
|
|
29901
|
+
function canonicalTimestamp(candidate) {
|
|
29902
|
+
const timestamp = boundedText(candidate, 64);
|
|
29903
|
+
if (!timestamp)
|
|
29904
|
+
return null;
|
|
29905
|
+
const milliseconds = Date.parse(timestamp);
|
|
29906
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === timestamp ? timestamp : null;
|
|
29907
|
+
}
|
|
29908
|
+
function projectedSummary(project) {
|
|
29909
|
+
const summary = {
|
|
29910
|
+
id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
29911
|
+
ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN),
|
|
29912
|
+
organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
29913
|
+
organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
|
|
29914
|
+
name: boundedText(project.name, 100),
|
|
29915
|
+
region: matchingText(project.region, 64, REGION_PATTERN),
|
|
29916
|
+
created_at: canonicalTimestamp(project.created_at),
|
|
29917
|
+
status: matchingText(project.status, 64, STATUS_PATTERN)
|
|
29918
|
+
};
|
|
29919
|
+
return Object.values(summary).every((field) => field !== null) ? summary : null;
|
|
29920
|
+
}
|
|
29921
|
+
function projectSummary(candidate) {
|
|
29922
|
+
const project = plainRecord(candidate);
|
|
29923
|
+
return project && hasOnlyKeys(project, PROJECT_SUMMARY_KEYS) ? projectedSummary(project) : null;
|
|
29924
|
+
}
|
|
29925
|
+
function databaseHost(candidate) {
|
|
29926
|
+
const host = boundedText(candidate, 255);
|
|
29927
|
+
if (!host)
|
|
29928
|
+
return null;
|
|
29929
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
29930
|
+
try {
|
|
29931
|
+
const parsedHost = new URL(`http://${host}`);
|
|
29932
|
+
return parsedHost.host === host ? host : null;
|
|
29933
|
+
} catch (error) {
|
|
29934
|
+
if (error instanceof TypeError)
|
|
29935
|
+
return null;
|
|
29936
|
+
throw error;
|
|
29937
|
+
}
|
|
29938
|
+
}
|
|
29939
|
+
const ipv4Parts = host.split(".");
|
|
29940
|
+
if (ipv4Parts.length === 4 && ipv4Parts.every((part) => /^\d{1,3}$/u.test(part))) {
|
|
29941
|
+
return ipv4Parts.every((part) => Number(part) <= 255) ? host : null;
|
|
29942
|
+
}
|
|
29943
|
+
return ipv4Parts.every((label) => DNS_LABEL_PATTERN.test(label)) ? host : null;
|
|
29944
|
+
}
|
|
29945
|
+
function projectDatabase(candidate) {
|
|
29946
|
+
const database = plainRecord(candidate);
|
|
29947
|
+
if (!database || !hasOnlyKeys(database, PROJECT_DATABASE_KEYS))
|
|
29948
|
+
return null;
|
|
29949
|
+
const host = databaseHost(database.host);
|
|
29950
|
+
const version = matchingText(database.version, 64, DATABASE_VERSION_PATTERN);
|
|
29951
|
+
const postgresEngine = matchingText(database.postgres_engine, 64, DATABASE_VERSION_PATTERN);
|
|
29952
|
+
const releaseChannel = matchingText(database.release_channel, 64, DATABASE_VERSION_PATTERN);
|
|
29953
|
+
return host && version && postgresEngine && releaseChannel ? { host, version, postgres_engine: postgresEngine, release_channel: releaseChannel } : null;
|
|
29954
|
+
}
|
|
29955
|
+
function rawUrlHasNoPath(candidate) {
|
|
29956
|
+
if (candidate.trim() !== candidate || candidate.includes("\\"))
|
|
29957
|
+
return false;
|
|
29958
|
+
const schemeEnd = candidate.indexOf("://");
|
|
29959
|
+
const pathStart = candidate.indexOf("/", schemeEnd + 3);
|
|
29960
|
+
return pathStart === -1;
|
|
29961
|
+
}
|
|
29962
|
+
function projectEndpoint(candidate) {
|
|
29963
|
+
const endpoint = plainRecord(candidate);
|
|
29964
|
+
if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
|
|
29965
|
+
return null;
|
|
29966
|
+
const endpointUrl = boundedText(endpoint.url, 2048);
|
|
29967
|
+
if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
|
|
29968
|
+
return null;
|
|
29969
|
+
try {
|
|
29970
|
+
const url = new URL(endpointUrl);
|
|
29971
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
29972
|
+
return null;
|
|
29973
|
+
return { url: url.origin };
|
|
29974
|
+
} catch (error) {
|
|
29975
|
+
if (error instanceof TypeError)
|
|
29976
|
+
return null;
|
|
29977
|
+
throw error;
|
|
29978
|
+
}
|
|
29979
|
+
}
|
|
29980
|
+
function discardedDetailFieldsAreValid(project) {
|
|
29981
|
+
if (project.config !== undefined && plainRecord(project.config) === null)
|
|
29982
|
+
return false;
|
|
29983
|
+
if (project.anon_key !== undefined && boundedText(project.anon_key, 16384) === null)
|
|
29984
|
+
return false;
|
|
29985
|
+
return project.services === undefined || Array.isArray(project.services);
|
|
29986
|
+
}
|
|
29987
|
+
function projectDetails(candidate, expectedRef) {
|
|
29988
|
+
const project = plainRecord(candidate);
|
|
29989
|
+
if (!project || !hasOnlyKeys(project, PROJECT_DETAILS_KEYS))
|
|
29990
|
+
return null;
|
|
29991
|
+
const summary = projectedSummary(project);
|
|
29992
|
+
const database = projectDatabase(project.database);
|
|
29993
|
+
const api = project.api === undefined ? undefined : projectEndpoint(project.api);
|
|
29994
|
+
const studio = project.studio === undefined ? undefined : projectEndpoint(project.studio);
|
|
29995
|
+
if (!summary || summary.ref !== expectedRef || !database || !discardedDetailFieldsAreValid(project) || project.api !== undefined && !api || project.studio !== undefined && !studio)
|
|
29996
|
+
return null;
|
|
29997
|
+
return {
|
|
29998
|
+
...summary,
|
|
29999
|
+
database,
|
|
30000
|
+
...api ? { api } : {},
|
|
30001
|
+
...studio ? { studio } : {}
|
|
30002
|
+
};
|
|
30003
|
+
}
|
|
30004
|
+
function payloadWithinLimit(candidate) {
|
|
30005
|
+
try {
|
|
30006
|
+
const serializedPayload = JSON.stringify(candidate);
|
|
30007
|
+
return serializedPayload !== undefined && new TextEncoder().encode(serializedPayload).byteLength <= PROJECT_READ_RESPONSE_MAX_BYTES;
|
|
30008
|
+
} catch {
|
|
30009
|
+
return false;
|
|
30010
|
+
}
|
|
30011
|
+
}
|
|
30012
|
+
function safeProjectList(candidate) {
|
|
30013
|
+
if (!payloadWithinLimit(candidate) || !Array.isArray(candidate) || candidate.length > MAX_PROJECTS)
|
|
30014
|
+
return null;
|
|
30015
|
+
const safeProjects = [];
|
|
30016
|
+
const ids = new Set;
|
|
30017
|
+
const refs = new Set;
|
|
30018
|
+
for (const projectCandidate of candidate) {
|
|
30019
|
+
const project = projectSummary(projectCandidate);
|
|
30020
|
+
if (!project || ids.has(project.id) || refs.has(project.ref))
|
|
30021
|
+
return null;
|
|
30022
|
+
ids.add(project.id);
|
|
30023
|
+
refs.add(project.ref);
|
|
30024
|
+
safeProjects.push(project);
|
|
30025
|
+
}
|
|
30026
|
+
return safeProjects;
|
|
30027
|
+
}
|
|
30028
|
+
function validHttpStatus(status) {
|
|
30029
|
+
return Number.isSafeInteger(status) && status >= 100 && status <= 599;
|
|
30030
|
+
}
|
|
30031
|
+
function successfulResponse(response) {
|
|
30032
|
+
return response.ok === true && validHttpStatus(response.status) && response.status >= 200 && response.status <= 299;
|
|
30033
|
+
}
|
|
30034
|
+
function failedResult(message) {
|
|
30035
|
+
return { text: `❌ ${message}`, isError: true };
|
|
30036
|
+
}
|
|
30037
|
+
function failedHttpResult(label, status) {
|
|
30038
|
+
return failedResult(validHttpStatus(status) ? `${label} request failed (${status})` : `${label} request failed`);
|
|
30039
|
+
}
|
|
30040
|
+
function successfulResult(payload) {
|
|
30041
|
+
return { text: JSON.stringify(payload, null, 2), isError: false };
|
|
30042
|
+
}
|
|
30043
|
+
function projectListRead(response) {
|
|
30044
|
+
if (!successfulResponse(response))
|
|
30045
|
+
return failedHttpResult("Project list", response.status);
|
|
30046
|
+
const projects = safeProjectList(response.data);
|
|
30047
|
+
return projects ? successfulResult(projects) : failedResult("Invalid project list response");
|
|
30048
|
+
}
|
|
30049
|
+
function projectGetRead(response, expectedRef) {
|
|
30050
|
+
if (!successfulResponse(response))
|
|
30051
|
+
return failedHttpResult("Project get", response.status);
|
|
30052
|
+
if (!payloadWithinLimit(response.data))
|
|
30053
|
+
return failedResult("Invalid project response");
|
|
30054
|
+
const project = projectDetails(response.data, expectedRef);
|
|
30055
|
+
return project ? successfulResult(project) : failedResult("Invalid project response");
|
|
30056
|
+
}
|
|
30057
|
+
// src/shared/tools/project-runtime-snapshot.ts
|
|
30058
|
+
var RUNTIME_SNAPSHOT_SCHEMA = "supacloud.runtime-snapshot.v1";
|
|
30059
|
+
var ATTESTED_REVISION_PATTERN = /^hmac-sha256:[a-f0-9]{64}$/;
|
|
30060
|
+
var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
|
|
30061
|
+
var SNAPSHOT_KEYS = ["schema", "project_ref", "captured_at", "secrets", "postgrest"];
|
|
30062
|
+
var SECRETS_KEYS = [
|
|
30063
|
+
"desired_revision",
|
|
30064
|
+
"loaded_revision",
|
|
30065
|
+
"load_state",
|
|
30066
|
+
"load_source",
|
|
30067
|
+
"matches_desired",
|
|
30068
|
+
"loaded_at"
|
|
30069
|
+
];
|
|
30070
|
+
var POSTGREST_KEYS = [
|
|
30071
|
+
"desired_revision",
|
|
30072
|
+
"loaded_revision",
|
|
30073
|
+
"attestation_state",
|
|
30074
|
+
"matches_desired",
|
|
30075
|
+
"desired",
|
|
30076
|
+
"actual",
|
|
30077
|
+
"health",
|
|
30078
|
+
"port",
|
|
30079
|
+
"unit",
|
|
30080
|
+
"loaded_at"
|
|
30081
|
+
];
|
|
30082
|
+
var SECRET_LOAD_STATES = ["current", "stale", "not_loaded", "unverified", "unreachable"];
|
|
30083
|
+
var SECRET_LOAD_SOURCES = ["management_api", "stale_cache", "file_fallback"];
|
|
30084
|
+
var POSTGREST_ATTESTATION_STATES = [
|
|
30085
|
+
"loaded",
|
|
30086
|
+
"stale",
|
|
30087
|
+
"drifted",
|
|
30088
|
+
"unverified_legacy",
|
|
30089
|
+
"stopped",
|
|
30090
|
+
"unreachable"
|
|
30091
|
+
];
|
|
30092
|
+
var POSTGREST_DESIRED_STATES = ["running", "stopped"];
|
|
30093
|
+
var POSTGREST_ACTUAL_STATES = ["running", "stopped", "starting", "error"];
|
|
30094
|
+
var POSTGREST_HEALTH_STATES = ["healthy", "unhealthy", "unknown"];
|
|
30095
|
+
function isRecord2(candidate) {
|
|
30096
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
|
|
30097
|
+
}
|
|
30098
|
+
function hasExactKeys(candidate, expectedKeys) {
|
|
30099
|
+
const actualKeys = Object.keys(candidate).sort();
|
|
30100
|
+
const sortedExpectedKeys = [...expectedKeys].sort();
|
|
30101
|
+
return actualKeys.length === sortedExpectedKeys.length && actualKeys.every((key, index) => key === sortedExpectedKeys[index]);
|
|
30102
|
+
}
|
|
30103
|
+
function isEnumMember(candidate, members) {
|
|
30104
|
+
return typeof candidate === "string" && members.includes(candidate);
|
|
30105
|
+
}
|
|
30106
|
+
function isIsoTimestampOrNull(candidate) {
|
|
30107
|
+
if (candidate === null)
|
|
30108
|
+
return true;
|
|
30109
|
+
if (typeof candidate !== "string")
|
|
30110
|
+
return false;
|
|
30111
|
+
const timestamp = Date.parse(candidate);
|
|
30112
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === candidate;
|
|
30113
|
+
}
|
|
30114
|
+
function isRevisionOrNull(candidate) {
|
|
30115
|
+
return candidate === null || typeof candidate === "string" && ATTESTED_REVISION_PATTERN.test(candidate);
|
|
30116
|
+
}
|
|
30117
|
+
function isBooleanOrNull(candidate) {
|
|
30118
|
+
return candidate === null || typeof candidate === "boolean";
|
|
30119
|
+
}
|
|
30120
|
+
function isUnloadedSecretsState(candidate) {
|
|
30121
|
+
return candidate.loaded_revision === null && candidate.load_source === null && candidate.matches_desired === null && candidate.loaded_at === null;
|
|
30122
|
+
}
|
|
30123
|
+
function isLoadedSecretsState(candidate) {
|
|
30124
|
+
if (candidate.load_state === "current") {
|
|
30125
|
+
return candidate.loaded_revision === candidate.desired_revision && candidate.load_source === "management_api" && candidate.matches_desired === true && candidate.loaded_at !== null;
|
|
30126
|
+
}
|
|
30127
|
+
return candidate.load_state === "stale" && candidate.loaded_revision !== null && candidate.loaded_revision !== candidate.desired_revision && candidate.load_source === "management_api" && candidate.matches_desired === false && candidate.loaded_at !== null;
|
|
30128
|
+
}
|
|
30129
|
+
function isUnverifiedSecretsState(candidate) {
|
|
30130
|
+
if (candidate.load_state !== "unverified" || candidate.matches_desired !== null || candidate.loaded_at === null)
|
|
30131
|
+
return false;
|
|
30132
|
+
if (candidate.loaded_revision === candidate.desired_revision) {
|
|
30133
|
+
return candidate.load_source === "management_api";
|
|
30134
|
+
}
|
|
30135
|
+
return candidate.loaded_revision === null && candidate.load_source !== null;
|
|
30136
|
+
}
|
|
30137
|
+
function hasValidSecretsState(candidate) {
|
|
30138
|
+
if (candidate.load_state === "not_loaded" || candidate.load_state === "unreachable") {
|
|
30139
|
+
return isUnloadedSecretsState(candidate);
|
|
30140
|
+
}
|
|
30141
|
+
return isLoadedSecretsState(candidate) || isUnverifiedSecretsState(candidate);
|
|
30142
|
+
}
|
|
30143
|
+
function isRuntimeSecretsSnapshot(payload) {
|
|
30144
|
+
if (!isRecord2(payload) || !hasExactKeys(payload, SECRETS_KEYS))
|
|
30145
|
+
return false;
|
|
30146
|
+
if (!isRevisionOrNull(payload.loaded_revision) || typeof payload.desired_revision !== "string" || !ATTESTED_REVISION_PATTERN.test(payload.desired_revision) || !isEnumMember(payload.load_state, SECRET_LOAD_STATES) || !(payload.load_source === null || isEnumMember(payload.load_source, SECRET_LOAD_SOURCES)) || !isBooleanOrNull(payload.matches_desired) || !isIsoTimestampOrNull(payload.loaded_at))
|
|
30147
|
+
return false;
|
|
30148
|
+
return hasValidSecretsState(payload);
|
|
30149
|
+
}
|
|
30150
|
+
function hasConsistentRevisionMatch(candidate) {
|
|
30151
|
+
if (candidate.loaded_revision === null)
|
|
30152
|
+
return candidate.matches_desired === null;
|
|
30153
|
+
return candidate.matches_desired === (candidate.loaded_revision === candidate.desired_revision);
|
|
30154
|
+
}
|
|
30155
|
+
function hasActivePostgrestProjection(candidate) {
|
|
30156
|
+
return candidate.actual === "running" && candidate.health === "healthy" || candidate.actual === "error" && candidate.health === "unhealthy";
|
|
30157
|
+
}
|
|
30158
|
+
function hasValidPostgrestState(candidate) {
|
|
30159
|
+
if (!hasConsistentRevisionMatch(candidate))
|
|
30160
|
+
return false;
|
|
30161
|
+
if (candidate.attestation_state === "loaded") {
|
|
30162
|
+
return candidate.matches_desired === true && candidate.actual === "running" && candidate.health === "healthy" && candidate.loaded_at !== null;
|
|
30163
|
+
}
|
|
30164
|
+
if (candidate.attestation_state === "stale") {
|
|
30165
|
+
return candidate.loaded_revision !== null && candidate.matches_desired === false && candidate.loaded_at !== null && hasActivePostgrestProjection(candidate);
|
|
30166
|
+
}
|
|
30167
|
+
if (candidate.attestation_state === "unverified_legacy")
|
|
30168
|
+
return candidate.loaded_revision === null;
|
|
30169
|
+
if (candidate.attestation_state === "stopped") {
|
|
30170
|
+
return candidate.loaded_revision === null && candidate.actual === "stopped" && candidate.health === "unknown" && candidate.loaded_at === null;
|
|
30171
|
+
}
|
|
30172
|
+
if (candidate.attestation_state === "unreachable")
|
|
30173
|
+
return candidate.loaded_revision === null;
|
|
30174
|
+
return candidate.attestation_state === "drifted";
|
|
30175
|
+
}
|
|
30176
|
+
function isPostgrestRuntimeSnapshot(payload, projectRef) {
|
|
30177
|
+
if (!isRecord2(payload) || !hasExactKeys(payload, POSTGREST_KEYS))
|
|
30178
|
+
return false;
|
|
30179
|
+
if (typeof payload.desired_revision !== "string" || !ATTESTED_REVISION_PATTERN.test(payload.desired_revision) || !isRevisionOrNull(payload.loaded_revision) || !isEnumMember(payload.attestation_state, POSTGREST_ATTESTATION_STATES) || !isBooleanOrNull(payload.matches_desired) || !isEnumMember(payload.desired, POSTGREST_DESIRED_STATES) || !isEnumMember(payload.actual, POSTGREST_ACTUAL_STATES) || !isEnumMember(payload.health, POSTGREST_HEALTH_STATES) || !Number.isSafeInteger(payload.port) || Number(payload.port) < 1 || Number(payload.port) > 65535 || payload.unit !== `supacloud-pgrst@${projectRef}` || !isIsoTimestampOrNull(payload.loaded_at))
|
|
30180
|
+
return false;
|
|
30181
|
+
return hasValidPostgrestState(payload);
|
|
30182
|
+
}
|
|
30183
|
+
function sanitizedSnapshot(snapshot) {
|
|
30184
|
+
return {
|
|
30185
|
+
schema: snapshot.schema,
|
|
30186
|
+
project_ref: snapshot.project_ref,
|
|
30187
|
+
captured_at: snapshot.captured_at,
|
|
30188
|
+
secrets: { ...snapshot.secrets },
|
|
30189
|
+
postgrest: { ...snapshot.postgrest }
|
|
30190
|
+
};
|
|
30191
|
+
}
|
|
30192
|
+
function hasCausalLoadTimestamps(snapshot) {
|
|
30193
|
+
const capturedAt = Date.parse(snapshot.captured_at);
|
|
30194
|
+
return [snapshot.secrets.loaded_at, snapshot.postgrest.loaded_at].every((loadedAt) => loadedAt === null || Date.parse(loadedAt) <= capturedAt);
|
|
30195
|
+
}
|
|
30196
|
+
function parseProjectRuntimeSnapshot(payload, requestedProjectRef2) {
|
|
30197
|
+
if (!SAFE_PROJECT_REF2.test(requestedProjectRef2) || !isRecord2(payload) || !hasExactKeys(payload, SNAPSHOT_KEYS) || payload.schema !== RUNTIME_SNAPSHOT_SCHEMA || payload.project_ref !== requestedProjectRef2 || !isIsoTimestampOrNull(payload.captured_at) || payload.captured_at === null || !isRuntimeSecretsSnapshot(payload.secrets) || !isPostgrestRuntimeSnapshot(payload.postgrest, requestedProjectRef2))
|
|
30198
|
+
return null;
|
|
30199
|
+
const snapshot = payload;
|
|
30200
|
+
return hasCausalLoadTimestamps(snapshot) ? sanitizedSnapshot(snapshot) : null;
|
|
30201
|
+
}
|
|
30202
|
+
|
|
29720
30203
|
// src/shared/tools/project-cli-tools.ts
|
|
29721
30204
|
var PROJECT_SERVICE_NAMES = [
|
|
29722
30205
|
"postgrest",
|
|
@@ -29748,9 +30231,10 @@ var STUDIO_PROJECT_SERVICE_STATUSES = [
|
|
|
29748
30231
|
];
|
|
29749
30232
|
var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
|
|
29750
30233
|
var AUTH_SERVICE_HOST_SUFFIX = "-auth";
|
|
29751
|
-
var
|
|
30234
|
+
var SAFE_PROJECT_REF3 = /^[a-z0-9-]{1,20}$/;
|
|
29752
30235
|
var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
|
|
29753
30236
|
var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
|
|
30237
|
+
var MAX_RUNTIME_SNAPSHOT_BYTES = 64 * 1024;
|
|
29754
30238
|
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
29755
30239
|
var PROJECT_CREATE_OPERATION = "project.create";
|
|
29756
30240
|
var PROJECT_ENVIRONMENTS = ["test", "production"];
|
|
@@ -29766,6 +30250,10 @@ var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
|
|
|
29766
30250
|
function projectToolResponse(text) {
|
|
29767
30251
|
return { content: [{ type: "text", text }] };
|
|
29768
30252
|
}
|
|
30253
|
+
function projectReadResponse(readResult) {
|
|
30254
|
+
const response = projectToolResponse(readResult.text);
|
|
30255
|
+
return readResult.isError ? { ...response, isError: true } : response;
|
|
30256
|
+
}
|
|
29769
30257
|
function failedProjectServiceResponse(message) {
|
|
29770
30258
|
return {
|
|
29771
30259
|
content: [{ type: "text", text: `❌ ${message}` }],
|
|
@@ -29960,7 +30448,7 @@ function projectServiceStatusOutput(status) {
|
|
|
29960
30448
|
function projectServicesResponse(projectRef, response) {
|
|
29961
30449
|
if (!response.ok)
|
|
29962
30450
|
return failedProjectServiceHttpResponse(response);
|
|
29963
|
-
if (!
|
|
30451
|
+
if (!SAFE_PROJECT_REF3.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
|
|
29964
30452
|
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
29965
30453
|
}
|
|
29966
30454
|
if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
|
|
@@ -29973,6 +30461,15 @@ function projectServicesResponse(projectRef, response) {
|
|
|
29973
30461
|
const services = response.data.map(projectServiceStatusOutput);
|
|
29974
30462
|
return projectToolResponse(JSON.stringify({ project_ref: projectRef, services }, null, 2));
|
|
29975
30463
|
}
|
|
30464
|
+
function projectRuntimeSnapshotResponse(projectRef, response) {
|
|
30465
|
+
if (response.responseError) {
|
|
30466
|
+
return failedProjectServiceResponse("Project runtime snapshot response is invalid");
|
|
30467
|
+
}
|
|
30468
|
+
if (!response.ok)
|
|
30469
|
+
return failedProjectServiceHttpResponse(response);
|
|
30470
|
+
const snapshot = parseProjectRuntimeSnapshot(response.data, projectRef);
|
|
30471
|
+
return snapshot ? projectToolResponse(JSON.stringify(snapshot, null, 2)) : failedProjectServiceResponse("Project runtime snapshot response is invalid");
|
|
30472
|
+
}
|
|
29976
30473
|
function supportsProjectServiceAction(service, action) {
|
|
29977
30474
|
return SUPPORTED_PROJECT_SERVICE_ACTIONS[service].includes(action);
|
|
29978
30475
|
}
|
|
@@ -30046,7 +30543,7 @@ function resolveRef(refFromArgs, defaultRef) {
|
|
|
30046
30543
|
}
|
|
30047
30544
|
function registerAdminProjectCliTools(server, http, options = {}) {
|
|
30048
30545
|
const fileOperations = options.projectEnvFileOperations;
|
|
30049
|
-
server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, service_control", {
|
|
30546
|
+
server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, runtime_snapshot, service_control", {
|
|
30050
30547
|
action: withDescription(stringEnum([
|
|
30051
30548
|
"list",
|
|
30052
30549
|
"create",
|
|
@@ -30062,6 +30559,7 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30062
30559
|
"logs",
|
|
30063
30560
|
"tasks",
|
|
30064
30561
|
"services",
|
|
30562
|
+
"runtime_snapshot",
|
|
30065
30563
|
"service_control"
|
|
30066
30564
|
]), "Action to perform"),
|
|
30067
30565
|
ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list' and 'create')"),
|
|
@@ -30098,8 +30596,9 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30098
30596
|
let text;
|
|
30099
30597
|
switch (action) {
|
|
30100
30598
|
case "list":
|
|
30101
|
-
|
|
30102
|
-
|
|
30599
|
+
return projectReadResponse(projectListRead(await http.get("/v1/projects", {
|
|
30600
|
+
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
30601
|
+
})));
|
|
30103
30602
|
case "create": {
|
|
30104
30603
|
if (!name)
|
|
30105
30604
|
throw new Error("'name' is required for create");
|
|
@@ -30137,9 +30636,12 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30137
30636
|
createRequest.credential_delivery = "response";
|
|
30138
30637
|
return projectCreateResponse(await http.post("/v1/projects", createRequest), preparedEnvFile, { projectName: name, apiOrigin: boundApiOrigin }, fileOperations);
|
|
30139
30638
|
}
|
|
30140
|
-
case "get":
|
|
30141
|
-
|
|
30142
|
-
|
|
30639
|
+
case "get": {
|
|
30640
|
+
const resolvedRef = resolveRef(ref);
|
|
30641
|
+
return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
|
|
30642
|
+
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
30643
|
+
}), resolvedRef));
|
|
30644
|
+
}
|
|
30143
30645
|
case "delete": {
|
|
30144
30646
|
const resolvedRef = resolveRef(ref);
|
|
30145
30647
|
text = simple(await http.delete(`/v1/projects/${resolvedRef}`), `Project ${resolvedRef} deleted`);
|
|
@@ -30191,6 +30693,10 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30191
30693
|
const resolvedRef = resolveRef(ref);
|
|
30192
30694
|
return projectServicesResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/services`));
|
|
30193
30695
|
}
|
|
30696
|
+
case "runtime_snapshot": {
|
|
30697
|
+
const resolvedRef = resolveRef(ref);
|
|
30698
|
+
return projectRuntimeSnapshotResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/runtime-snapshot`, { maxJsonBytes: MAX_RUNTIME_SNAPSHOT_BYTES }));
|
|
30699
|
+
}
|
|
30194
30700
|
case "service_control": {
|
|
30195
30701
|
const resolvedRef = resolveRef(ref);
|
|
30196
30702
|
if (!service)
|
|
@@ -30543,7 +31049,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
30543
31049
|
// package.json
|
|
30544
31050
|
var package_default = {
|
|
30545
31051
|
name: "@supacloud/admin",
|
|
30546
|
-
version: "0.
|
|
31052
|
+
version: "0.12.0",
|
|
30547
31053
|
description: "Platform administration CLI for SupaCloud operators",
|
|
30548
31054
|
type: "module",
|
|
30549
31055
|
main: "./dist/index.js",
|
|
@@ -30675,6 +31181,7 @@ EXAMPLES
|
|
|
30675
31181
|
supacloud-admin project create --name my-app --domain example.com --env_file /secure/path/.env.project-credentials.test --environment test
|
|
30676
31182
|
supacloud-admin project list
|
|
30677
31183
|
supacloud-admin project services --ref abc123
|
|
31184
|
+
supacloud-admin project runtime_snapshot --ref abc123
|
|
30678
31185
|
supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
30679
31186
|
supacloud-admin platform metrics
|
|
30680
31187
|
supacloud-admin gateway routes --ref abc123
|
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|