@supacloud/admin 0.8.2 → 0.9.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 +19 -0
- package/dist/index.js +176 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ npx @supacloud/admin ssh versions
|
|
|
31
31
|
npx @supacloud/admin ssh diagnose
|
|
32
32
|
npx @supacloud/admin project create --name my-app --domain example.com
|
|
33
33
|
npx @supacloud/admin project list
|
|
34
|
+
npx @supacloud/admin project services --ref abc123
|
|
35
|
+
npx @supacloud/admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
34
36
|
```
|
|
35
37
|
|
|
36
38
|
`ssh versions` emits JSON with `schema_version: 1` and fixed
|
|
@@ -106,6 +108,23 @@ Project commands owned by this CLI:
|
|
|
106
108
|
- `project restore`
|
|
107
109
|
- `project restart`
|
|
108
110
|
- `project update_settings`
|
|
111
|
+
- `project services` — read-only project service inventory
|
|
112
|
+
- `project service_control` — constrained project service lifecycle control
|
|
113
|
+
|
|
114
|
+
Service control accepts canonical service names only. `postgrest` supports
|
|
115
|
+
`start`, `stop`, `restart`, `pause`, `resume`, and `status`; `gotrue`, `storage`,
|
|
116
|
+
`postgresql`, `realtime`, and `gateway` support `start`, `stop`, and `restart`.
|
|
117
|
+
The command calls only the Management API's existing
|
|
118
|
+
`/v1/projects/{ref}/services` routes. A non-2xx response, a `success: false`
|
|
119
|
+
receipt, or a response that does not match the requested service and action
|
|
120
|
+
exits non-zero. Successful inventory and control responses are emitted as JSON
|
|
121
|
+
with `project_ref` for strict read-back.
|
|
122
|
+
|
|
123
|
+
The Management API remains authoritative for SupAuth ownership. Controlling
|
|
124
|
+
GoTrue on a shared-auth project fails with `AUTH_RUNTIME_MANAGED_BY_OWNER`;
|
|
125
|
+
the CLI does not redirect the operation to the owner project. Supply
|
|
126
|
+
`SUPACLOUD_API_TOKEN` through the environment only; service-control commands do
|
|
127
|
+
not accept credential flags.
|
|
109
128
|
|
|
110
129
|
Gateway / Caddy commands (config is injected via the Caddy JSON Admin API; requires admin privileges):
|
|
111
130
|
|
package/dist/index.js
CHANGED
|
@@ -28678,6 +28678,150 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status})`;
|
|
|
28678
28678
|
}
|
|
28679
28679
|
|
|
28680
28680
|
// src/shared/tools/project-cli-tools.ts
|
|
28681
|
+
var PROJECT_SERVICE_NAMES = [
|
|
28682
|
+
"postgrest",
|
|
28683
|
+
"gotrue",
|
|
28684
|
+
"storage",
|
|
28685
|
+
"postgresql",
|
|
28686
|
+
"realtime",
|
|
28687
|
+
"gateway"
|
|
28688
|
+
];
|
|
28689
|
+
var PROJECT_SERVICE_CONTROL_ACTIONS = [
|
|
28690
|
+
"start",
|
|
28691
|
+
"stop",
|
|
28692
|
+
"restart",
|
|
28693
|
+
"pause",
|
|
28694
|
+
"resume",
|
|
28695
|
+
"status"
|
|
28696
|
+
];
|
|
28697
|
+
var STUDIO_PROJECT_SERVICE_NAMES = [
|
|
28698
|
+
"db",
|
|
28699
|
+
"rest",
|
|
28700
|
+
"auth",
|
|
28701
|
+
"realtime",
|
|
28702
|
+
"storage"
|
|
28703
|
+
];
|
|
28704
|
+
var STUDIO_PROJECT_SERVICE_STATUSES = [
|
|
28705
|
+
"ACTIVE_HEALTHY",
|
|
28706
|
+
"COMING_UP",
|
|
28707
|
+
"UNHEALTHY"
|
|
28708
|
+
];
|
|
28709
|
+
var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
|
|
28710
|
+
var AUTH_SERVICE_HOST_SUFFIX = "-auth";
|
|
28711
|
+
var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
|
|
28712
|
+
var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
|
|
28713
|
+
var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
|
|
28714
|
+
var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
|
|
28715
|
+
postgrest: ["start", "stop", "restart", "pause", "resume", "status"],
|
|
28716
|
+
gotrue: ["start", "stop", "restart"],
|
|
28717
|
+
storage: ["start", "stop", "restart"],
|
|
28718
|
+
postgresql: ["start", "stop", "restart"],
|
|
28719
|
+
realtime: ["start", "stop", "restart"],
|
|
28720
|
+
gateway: ["start", "stop", "restart"]
|
|
28721
|
+
};
|
|
28722
|
+
function projectToolResponse(text) {
|
|
28723
|
+
return { content: [{ type: "text", text }] };
|
|
28724
|
+
}
|
|
28725
|
+
function failedProjectServiceResponse(message) {
|
|
28726
|
+
return {
|
|
28727
|
+
content: [{ type: "text", text: `❌ ${message}` }],
|
|
28728
|
+
isError: true
|
|
28729
|
+
};
|
|
28730
|
+
}
|
|
28731
|
+
function failedProjectServiceHttpResponse(response) {
|
|
28732
|
+
if (response.status === 409 && isRecordPayload(response.data) && response.data.code === AUTH_RUNTIME_MANAGED_BY_OWNER && typeof response.data.authority_project_ref === "string" && SAFE_AUTHORITY_PROJECT_REF.test(response.data.authority_project_ref)) {
|
|
28733
|
+
const ownerBoundary = {
|
|
28734
|
+
status: response.status,
|
|
28735
|
+
code: AUTH_RUNTIME_MANAGED_BY_OWNER,
|
|
28736
|
+
authority_project_ref: response.data.authority_project_ref
|
|
28737
|
+
};
|
|
28738
|
+
return failedProjectServiceResponse(JSON.stringify(ownerBoundary));
|
|
28739
|
+
}
|
|
28740
|
+
return failedProjectServiceResponse(`Failed (${response.status})`);
|
|
28741
|
+
}
|
|
28742
|
+
function isRecordPayload(payload) {
|
|
28743
|
+
return typeof payload === "object" && payload !== null && !Array.isArray(payload);
|
|
28744
|
+
}
|
|
28745
|
+
function isStudioProjectServiceName(name) {
|
|
28746
|
+
return typeof name === "string" && STUDIO_PROJECT_SERVICE_NAMES.some((allowedName) => name === allowedName);
|
|
28747
|
+
}
|
|
28748
|
+
function hasValidStudioServiceHealth(serviceId, status, healthy) {
|
|
28749
|
+
if (status === "INACTIVE")
|
|
28750
|
+
return serviceId === "auth" && healthy === false;
|
|
28751
|
+
const knownStatus = STUDIO_PROJECT_SERVICE_STATUSES.some((allowedStatus) => status === allowedStatus);
|
|
28752
|
+
return knownStatus && healthy === (status === "ACTIVE_HEALTHY");
|
|
28753
|
+
}
|
|
28754
|
+
function hasValidStudioServiceHost(serviceId, projectRef, hostIds) {
|
|
28755
|
+
if (!Array.isArray(hostIds) || hostIds.length !== 1 || typeof hostIds[0] !== "string")
|
|
28756
|
+
return false;
|
|
28757
|
+
if (serviceId === "auth") {
|
|
28758
|
+
if (!hostIds[0].endsWith(AUTH_SERVICE_HOST_SUFFIX))
|
|
28759
|
+
return false;
|
|
28760
|
+
return SAFE_AUTHORITY_PROJECT_REF.test(hostIds[0].slice(0, -AUTH_SERVICE_HOST_SUFFIX.length));
|
|
28761
|
+
}
|
|
28762
|
+
return hostIds[0] === `${projectRef}-${serviceId}`;
|
|
28763
|
+
}
|
|
28764
|
+
function isProjectServiceStatus(payload, projectRef) {
|
|
28765
|
+
if (!isRecordPayload(payload))
|
|
28766
|
+
return false;
|
|
28767
|
+
if (!isStudioProjectServiceName(payload.id) || payload.name !== payload.id)
|
|
28768
|
+
return false;
|
|
28769
|
+
return hasValidStudioServiceHealth(payload.id, payload.status, payload.healthy) && hasValidStudioServiceHost(payload.id, projectRef, payload.service_host_ids);
|
|
28770
|
+
}
|
|
28771
|
+
function projectServiceStatusOutput(status) {
|
|
28772
|
+
return {
|
|
28773
|
+
id: status.id,
|
|
28774
|
+
name: status.name,
|
|
28775
|
+
status: status.status,
|
|
28776
|
+
healthy: status.healthy,
|
|
28777
|
+
service_host_ids: [status.service_host_ids[0]]
|
|
28778
|
+
};
|
|
28779
|
+
}
|
|
28780
|
+
function projectServicesResponse(projectRef, response) {
|
|
28781
|
+
if (!response.ok)
|
|
28782
|
+
return failedProjectServiceHttpResponse(response);
|
|
28783
|
+
if (!SAFE_PROJECT_REF2.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
|
|
28784
|
+
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
28785
|
+
}
|
|
28786
|
+
if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
|
|
28787
|
+
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
28788
|
+
}
|
|
28789
|
+
const serviceIds = new Set(response.data.map((service) => service.id));
|
|
28790
|
+
if (serviceIds.size !== STUDIO_PROJECT_SERVICE_NAMES.length) {
|
|
28791
|
+
return failedProjectServiceResponse("Project service inventory response is invalid");
|
|
28792
|
+
}
|
|
28793
|
+
const services = response.data.map(projectServiceStatusOutput);
|
|
28794
|
+
return projectToolResponse(JSON.stringify({ project_ref: projectRef, services }, null, 2));
|
|
28795
|
+
}
|
|
28796
|
+
function supportsProjectServiceAction(service, action) {
|
|
28797
|
+
return SUPPORTED_PROJECT_SERVICE_ACTIONS[service].includes(action);
|
|
28798
|
+
}
|
|
28799
|
+
function projectServiceReceiptError(receipt, requestedService, requestedAction) {
|
|
28800
|
+
if (!isRecordPayload(receipt))
|
|
28801
|
+
return "Project service control response is invalid";
|
|
28802
|
+
if (receipt.service !== requestedService || receipt.action !== requestedAction) {
|
|
28803
|
+
return "Project service control response does not match the request";
|
|
28804
|
+
}
|
|
28805
|
+
if (receipt.success === false)
|
|
28806
|
+
return "Project service control failed";
|
|
28807
|
+
if (receipt.success !== true || typeof receipt.message !== "string" || receipt.message.length > MAX_SERVICE_CONTROL_MESSAGE_LENGTH) {
|
|
28808
|
+
return "Project service control response is invalid";
|
|
28809
|
+
}
|
|
28810
|
+
return null;
|
|
28811
|
+
}
|
|
28812
|
+
function projectServiceControlResponse(projectRef, requestedService, requestedAction, response) {
|
|
28813
|
+
if (!response.ok)
|
|
28814
|
+
return failedProjectServiceHttpResponse(response);
|
|
28815
|
+
const receiptError = projectServiceReceiptError(response.data, requestedService, requestedAction);
|
|
28816
|
+
if (receiptError)
|
|
28817
|
+
return failedProjectServiceResponse(receiptError);
|
|
28818
|
+
return projectToolResponse(JSON.stringify({
|
|
28819
|
+
project_ref: projectRef,
|
|
28820
|
+
service: requestedService,
|
|
28821
|
+
action: requestedAction,
|
|
28822
|
+
success: true
|
|
28823
|
+
}, null, 2));
|
|
28824
|
+
}
|
|
28681
28825
|
var formatTasks = (data) => {
|
|
28682
28826
|
if (!Array.isArray(data))
|
|
28683
28827
|
return JSON.stringify(data, null, 2);
|
|
@@ -28721,7 +28865,7 @@ function resolveRef(refFromArgs, defaultRef) {
|
|
|
28721
28865
|
return ref;
|
|
28722
28866
|
}
|
|
28723
28867
|
function registerAdminProjectCliTools(server, http) {
|
|
28724
|
-
server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks", {
|
|
28868
|
+
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", {
|
|
28725
28869
|
action: withDescription(stringEnum([
|
|
28726
28870
|
"list",
|
|
28727
28871
|
"create",
|
|
@@ -28735,9 +28879,11 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28735
28879
|
"api_keys",
|
|
28736
28880
|
"health",
|
|
28737
28881
|
"logs",
|
|
28738
|
-
"tasks"
|
|
28882
|
+
"tasks",
|
|
28883
|
+
"services",
|
|
28884
|
+
"service_control"
|
|
28739
28885
|
]), "Action to perform"),
|
|
28740
|
-
ref: optional(Type.String(), "Project ref (required for most actions except 'list' and 'create')"),
|
|
28886
|
+
ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list' and 'create')"),
|
|
28741
28887
|
name: optional(Type.String(), "[create] Project name"),
|
|
28742
28888
|
region: optional(Type.String(), "[create] Region (default: local)"),
|
|
28743
28889
|
organization_id: optional(Type.String(), "[create] Organization ID"),
|
|
@@ -28746,7 +28892,9 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28746
28892
|
auth_domain: optional(Type.String(), "[create] Explicit Auth/OIDC domain"),
|
|
28747
28893
|
studio_domain: optional(Type.String(), "[create] Explicit Studio domain"),
|
|
28748
28894
|
settings: optional(Type.Record(Type.String(), Type.Unknown()), "[update_settings] Config fields to update"),
|
|
28749
|
-
log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service")
|
|
28895
|
+
log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service"),
|
|
28896
|
+
service: optional(stringEnum(PROJECT_SERVICE_NAMES), "[service_control] Canonical service name"),
|
|
28897
|
+
service_action: optional(stringEnum(PROJECT_SERVICE_CONTROL_ACTIONS), "[service_control] Supported action for the selected service")
|
|
28750
28898
|
}, async ({
|
|
28751
28899
|
action,
|
|
28752
28900
|
ref,
|
|
@@ -28758,7 +28906,9 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28758
28906
|
auth_domain,
|
|
28759
28907
|
studio_domain,
|
|
28760
28908
|
settings,
|
|
28761
|
-
log_type
|
|
28909
|
+
log_type,
|
|
28910
|
+
service,
|
|
28911
|
+
service_action
|
|
28762
28912
|
}) => {
|
|
28763
28913
|
let text;
|
|
28764
28914
|
switch (action) {
|
|
@@ -28834,6 +28984,24 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28834
28984
|
text = res.ok ? formatTasks(res.data) : `❌ Failed (${res.status})`;
|
|
28835
28985
|
break;
|
|
28836
28986
|
}
|
|
28987
|
+
case "services": {
|
|
28988
|
+
const resolvedRef = resolveRef(ref);
|
|
28989
|
+
return projectServicesResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/services`));
|
|
28990
|
+
}
|
|
28991
|
+
case "service_control": {
|
|
28992
|
+
const resolvedRef = resolveRef(ref);
|
|
28993
|
+
if (!service)
|
|
28994
|
+
throw new Error("'service' is required for service_control");
|
|
28995
|
+
if (!service_action)
|
|
28996
|
+
throw new Error("'service_action' is required for service_control");
|
|
28997
|
+
if (!supportsProjectServiceAction(service, service_action)) {
|
|
28998
|
+
throw new Error(`'${service_action}' is not supported for service '${service}'`);
|
|
28999
|
+
}
|
|
29000
|
+
const encodedRef = encodeURIComponent(resolvedRef);
|
|
29001
|
+
const encodedService = encodeURIComponent(service);
|
|
29002
|
+
const encodedAction = encodeURIComponent(service_action);
|
|
29003
|
+
return projectServiceControlResponse(resolvedRef, service, service_action, await http.post(`/v1/projects/${encodedRef}/services/${encodedService}/${encodedAction}`));
|
|
29004
|
+
}
|
|
28837
29005
|
default:
|
|
28838
29006
|
text = `❌ Unknown action: ${action}`;
|
|
28839
29007
|
}
|
|
@@ -29172,7 +29340,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
29172
29340
|
// package.json
|
|
29173
29341
|
var package_default = {
|
|
29174
29342
|
name: "@supacloud/admin",
|
|
29175
|
-
version: "0.
|
|
29343
|
+
version: "0.9.0",
|
|
29176
29344
|
description: "Platform administration CLI for SupaCloud operators",
|
|
29177
29345
|
type: "module",
|
|
29178
29346
|
main: "./dist/index.js",
|
|
@@ -29286,6 +29454,8 @@ EXAMPLES
|
|
|
29286
29454
|
supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
|
|
29287
29455
|
supacloud-admin project create --name my-app --domain example.com
|
|
29288
29456
|
supacloud-admin project list
|
|
29457
|
+
supacloud-admin project services --ref abc123
|
|
29458
|
+
supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
29289
29459
|
supacloud-admin platform metrics
|
|
29290
29460
|
supacloud-admin gateway routes --ref abc123
|
|
29291
29461
|
supacloud-admin gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
|