railcode 0.1.22 → 0.1.23
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 -0
- package/dist/connections.js +156 -0
- package/dist/index.js +996 -3
- package/dist/observability.js +220 -0
- package/dist/orgadmin.js +227 -0
- package/package.json +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Pure helpers behind `railcode logs` (observability streams) and
|
|
2
|
+
// `railcode analytics` (per-app pageviews). No I/O or process state — the command
|
|
3
|
+
// layer in index.ts does the HTTP and printing. Mirrors connectors.ts / db.ts.
|
|
4
|
+
import { formatTable } from "./db.js";
|
|
5
|
+
export class ObservabilityError extends Error {
|
|
6
|
+
}
|
|
7
|
+
function fmtTime(iso) {
|
|
8
|
+
if (!iso)
|
|
9
|
+
return "-";
|
|
10
|
+
return iso.slice(0, 16).replace("T", " ");
|
|
11
|
+
}
|
|
12
|
+
function num(value) {
|
|
13
|
+
return value === null || value === undefined ? "-" : String(value);
|
|
14
|
+
}
|
|
15
|
+
function str(value) {
|
|
16
|
+
return value === null || value === undefined ? "-" : String(value);
|
|
17
|
+
}
|
|
18
|
+
function refLabel(ref) {
|
|
19
|
+
if (!ref)
|
|
20
|
+
return "-";
|
|
21
|
+
return ref.email ?? ref.name ?? ref.uuid;
|
|
22
|
+
}
|
|
23
|
+
export const LOG_STREAMS = [
|
|
24
|
+
{
|
|
25
|
+
key: "connector",
|
|
26
|
+
aliases: ["connectors", "sql", "connection", "connections", "db"],
|
|
27
|
+
path: "/connections/logs",
|
|
28
|
+
label: "data-connection SQL queries",
|
|
29
|
+
capability: "connection:manage",
|
|
30
|
+
filters: ["app", "user", "connector", "status"],
|
|
31
|
+
statuses: ["success", "error"],
|
|
32
|
+
columns: ["request_id", "connector", "engine", "status", "ms", "rows", "user", "when"],
|
|
33
|
+
map: (r) => [
|
|
34
|
+
str(r.request_id),
|
|
35
|
+
str(r.connection_name),
|
|
36
|
+
str(r.engine),
|
|
37
|
+
str(r.status),
|
|
38
|
+
num(r.duration_ms),
|
|
39
|
+
num(r.rowcount),
|
|
40
|
+
refLabel(r.user),
|
|
41
|
+
fmtTime(r.created_at),
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "service-connector",
|
|
46
|
+
aliases: ["service-connectors", "sc", "proxy"],
|
|
47
|
+
path: "/service-connectors/logs",
|
|
48
|
+
label: "service-connector HTTP proxy calls",
|
|
49
|
+
capability: "service-connector:manage",
|
|
50
|
+
filters: ["app", "user", "connector", "status"],
|
|
51
|
+
statuses: ["success", "error"],
|
|
52
|
+
columns: ["request_id", "connector", "method", "path", "status", "resp", "ms", "when"],
|
|
53
|
+
map: (r) => [
|
|
54
|
+
str(r.request_id),
|
|
55
|
+
str(r.connector_name),
|
|
56
|
+
str(r.method),
|
|
57
|
+
str(r.path),
|
|
58
|
+
str(r.status),
|
|
59
|
+
num(r.response_status),
|
|
60
|
+
num(r.duration_ms),
|
|
61
|
+
fmtTime(r.created_at),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "llm",
|
|
66
|
+
aliases: ["ai"],
|
|
67
|
+
path: "/llm/logs",
|
|
68
|
+
label: "LLM gateway calls",
|
|
69
|
+
capability: "llm:manage",
|
|
70
|
+
filters: ["app", "user", "status", "source"],
|
|
71
|
+
statuses: ["success", "provider_error", "validation_error", "cancelled", "timeout"],
|
|
72
|
+
sources: ["app", "agent"],
|
|
73
|
+
columns: ["request_id", "provider", "model", "status", "in_tok", "out_tok", "ms", "when"],
|
|
74
|
+
map: (r) => [
|
|
75
|
+
str(r.request_id),
|
|
76
|
+
str(r.provider),
|
|
77
|
+
str(r.model),
|
|
78
|
+
str(r.status),
|
|
79
|
+
num(r.input_tokens),
|
|
80
|
+
num(r.output_tokens),
|
|
81
|
+
num(r.latency_ms),
|
|
82
|
+
fmtTime(r.created_at),
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "email",
|
|
87
|
+
aliases: ["emails", "mail"],
|
|
88
|
+
path: "/email/logs",
|
|
89
|
+
label: "email gateway sends",
|
|
90
|
+
capability: "email:manage",
|
|
91
|
+
filters: ["app", "user", "status"],
|
|
92
|
+
statuses: ["success", "provider_error", "validation_error", "suppressed"],
|
|
93
|
+
columns: ["request_id", "subject", "recipients", "status", "when"],
|
|
94
|
+
map: (r) => [
|
|
95
|
+
str(r.request_id),
|
|
96
|
+
str(r.subject),
|
|
97
|
+
num(r.recipient_count),
|
|
98
|
+
str(r.status),
|
|
99
|
+
fmtTime(r.created_at),
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: "agent",
|
|
104
|
+
aliases: ["agents", "runs"],
|
|
105
|
+
path: "/agents/runs",
|
|
106
|
+
label: "managed-agent runs",
|
|
107
|
+
capability: "agent:manage",
|
|
108
|
+
filters: ["agent", "app", "user", "status"],
|
|
109
|
+
statuses: ["running", "success", "failed", "cancelled", "limit_exceeded"],
|
|
110
|
+
columns: ["request_id", "agent", "status", "trigger", "started", "finished"],
|
|
111
|
+
map: (r) => [
|
|
112
|
+
str(r.request_id),
|
|
113
|
+
refLabel(r.agent),
|
|
114
|
+
str(r.status),
|
|
115
|
+
str(r.trigger_type),
|
|
116
|
+
fmtTime(r.started_at),
|
|
117
|
+
fmtTime(r.finished_at),
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
export function resolveLogStream(name) {
|
|
122
|
+
const needle = (name ?? "").trim().toLowerCase();
|
|
123
|
+
if (!needle) {
|
|
124
|
+
throw new ObservabilityError(`Pick a log stream: ${LOG_STREAMS.map((s) => s.key).join(", ")}.`);
|
|
125
|
+
}
|
|
126
|
+
const found = LOG_STREAMS.find((s) => s.key === needle || s.aliases.includes(needle));
|
|
127
|
+
if (!found) {
|
|
128
|
+
throw new ObservabilityError(`Unknown log stream "${name}". One of: ${LOG_STREAMS.map((s) => s.key).join(", ")}.`);
|
|
129
|
+
}
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
export function validateLogStatus(stream, value) {
|
|
133
|
+
const v = value.trim().toLowerCase();
|
|
134
|
+
if (!stream.statuses.includes(v)) {
|
|
135
|
+
throw new ObservabilityError(`--status for ${stream.key} must be one of: ${stream.statuses.join(", ")}.`);
|
|
136
|
+
}
|
|
137
|
+
return v;
|
|
138
|
+
}
|
|
139
|
+
export function validateLogSource(stream, value) {
|
|
140
|
+
const v = value.trim().toLowerCase();
|
|
141
|
+
if (!stream.sources || !stream.sources.includes(v)) {
|
|
142
|
+
throw new ObservabilityError(`--source is only valid for llm logs (${(stream.sources ?? []).join(", ") || "n/a"}).`);
|
|
143
|
+
}
|
|
144
|
+
return v;
|
|
145
|
+
}
|
|
146
|
+
export function parseLimit(value) {
|
|
147
|
+
if (value === undefined)
|
|
148
|
+
return undefined;
|
|
149
|
+
const n = Number(value);
|
|
150
|
+
if (!Number.isInteger(n) || n < 1 || n > 500) {
|
|
151
|
+
throw new ObservabilityError("--limit must be an integer between 1 and 500.");
|
|
152
|
+
}
|
|
153
|
+
return n;
|
|
154
|
+
}
|
|
155
|
+
// Build the `GET /logs` query string from resolved filter values (app/user/agent
|
|
156
|
+
// are already UUIDs). Only non-empty values are appended; order is stable for tests.
|
|
157
|
+
export function buildLogQuery(params) {
|
|
158
|
+
const qs = new URLSearchParams();
|
|
159
|
+
if (params.agent)
|
|
160
|
+
qs.set("agent", params.agent);
|
|
161
|
+
if (params.app)
|
|
162
|
+
qs.set("app", params.app);
|
|
163
|
+
if (params.user)
|
|
164
|
+
qs.set("user", params.user);
|
|
165
|
+
if (params.connector)
|
|
166
|
+
qs.set("connector", params.connector);
|
|
167
|
+
if (params.source)
|
|
168
|
+
qs.set("source", params.source);
|
|
169
|
+
if (params.status)
|
|
170
|
+
qs.set("status", params.status);
|
|
171
|
+
if (params.limit !== undefined)
|
|
172
|
+
qs.set("limit", String(params.limit));
|
|
173
|
+
const s = qs.toString();
|
|
174
|
+
return s ? `?${s}` : "";
|
|
175
|
+
}
|
|
176
|
+
export function logTable(stream, rows) {
|
|
177
|
+
return formatTable(stream.columns, rows.map((r) => stream.map(r)));
|
|
178
|
+
}
|
|
179
|
+
// ── Analytics (per-app pageviews) ───────────────────────────────────────────
|
|
180
|
+
export const ANALYTICS_RANGES = ["7d", "30d", "90d"];
|
|
181
|
+
export function parseRange(value) {
|
|
182
|
+
if (value === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
const v = value.trim().toLowerCase();
|
|
185
|
+
if (!ANALYTICS_RANGES.includes(v)) {
|
|
186
|
+
throw new ObservabilityError(`--range must be one of: ${ANALYTICS_RANGES.join(", ")}.`);
|
|
187
|
+
}
|
|
188
|
+
return v;
|
|
189
|
+
}
|
|
190
|
+
export function renderAnalytics(slug, data) {
|
|
191
|
+
const lines = [];
|
|
192
|
+
lines.push(`Analytics for ${slug} — ${data.range} (${data.from} → ${data.to})`);
|
|
193
|
+
lines.push(` views: ${data.totals.views} unique users: ${data.totals.uniques}`);
|
|
194
|
+
lines.push("");
|
|
195
|
+
if (data.daily.length > 0) {
|
|
196
|
+
lines.push("Daily");
|
|
197
|
+
lines.push(indent(formatTable(["day", "views", "uniques"], data.daily.map((d) => [d.day, String(d.views), String(d.uniques)]))));
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
if (data.top_paths.length > 0) {
|
|
201
|
+
lines.push("Top paths");
|
|
202
|
+
lines.push(indent(formatTable(["path", "views"], data.top_paths.map((p) => [p.path, String(p.views)]))));
|
|
203
|
+
lines.push("");
|
|
204
|
+
}
|
|
205
|
+
if (data.users.length > 0) {
|
|
206
|
+
lines.push("Top users");
|
|
207
|
+
lines.push(indent(formatTable(["user", "views"], data.users.map((u) => [u.display_name, String(u.views)]))));
|
|
208
|
+
lines.push("");
|
|
209
|
+
}
|
|
210
|
+
if (data.totals.views === 0) {
|
|
211
|
+
lines.push("No pageviews recorded in this window.");
|
|
212
|
+
}
|
|
213
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
214
|
+
}
|
|
215
|
+
function indent(block) {
|
|
216
|
+
return block
|
|
217
|
+
.split("\n")
|
|
218
|
+
.map((l) => ` ${l}`)
|
|
219
|
+
.join("\n");
|
|
220
|
+
}
|
package/dist/orgadmin.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Pure helpers behind the org-admin CLI surfaces — `railcode members`,
|
|
2
|
+
// `railcode roles`, and the access half of `railcode apps`. Kept free of any I/O
|
|
3
|
+
// or process state so they can be unit-tested directly (see test/orgadmin.test.mjs);
|
|
4
|
+
// the command layer in index.ts does the HTTP and printing. Mirrors connectors.ts.
|
|
5
|
+
import { formatTable } from "./db.js";
|
|
6
|
+
export class OrgAdminError extends Error {
|
|
7
|
+
}
|
|
8
|
+
// Trim an ISO 8601 timestamp to a compact "YYYY-MM-DD HH:MM" for table display.
|
|
9
|
+
export function fmtTime(iso) {
|
|
10
|
+
if (!iso)
|
|
11
|
+
return "-";
|
|
12
|
+
return iso.slice(0, 16).replace("T", " ");
|
|
13
|
+
}
|
|
14
|
+
function splitList(value) {
|
|
15
|
+
return value
|
|
16
|
+
.split(",")
|
|
17
|
+
.map((part) => part.trim())
|
|
18
|
+
.filter((part) => part.length > 0);
|
|
19
|
+
}
|
|
20
|
+
export function memberTable(members) {
|
|
21
|
+
return formatTable(["email", "name", "role", "verified"], members.map((m) => [m.email, m.name, m.role, m.email_verified ? "yes" : "no"]));
|
|
22
|
+
}
|
|
23
|
+
// Resolve a member reference — a UUID or an email (case-insensitive) — against a
|
|
24
|
+
// fetched member list. Throws OrgAdminError when it matches nothing/ambiguously.
|
|
25
|
+
export function resolveMemberRef(members, ref) {
|
|
26
|
+
const needle = ref.trim();
|
|
27
|
+
if (!needle)
|
|
28
|
+
throw new OrgAdminError("Expected a member (email or UUID).");
|
|
29
|
+
const byUuid = members.find((m) => m.uuid === needle);
|
|
30
|
+
if (byUuid)
|
|
31
|
+
return byUuid;
|
|
32
|
+
const lower = needle.toLowerCase();
|
|
33
|
+
const byEmail = members.filter((m) => m.email.toLowerCase() === lower);
|
|
34
|
+
if (byEmail.length === 1)
|
|
35
|
+
return byEmail[0];
|
|
36
|
+
if (byEmail.length > 1) {
|
|
37
|
+
throw new OrgAdminError(`Multiple members match "${ref}" — use the UUID.`);
|
|
38
|
+
}
|
|
39
|
+
throw new OrgAdminError(`No member matches "${ref}" (try an email or UUID).`);
|
|
40
|
+
}
|
|
41
|
+
// The two assignable system roles (the owner tier can't be assigned/reassigned via
|
|
42
|
+
// the API — it's the org creator, transferred through a separate flow).
|
|
43
|
+
export function parseAssignableRole(value) {
|
|
44
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
45
|
+
if (v === "admin" || v === "member")
|
|
46
|
+
return v;
|
|
47
|
+
throw new OrgAdminError('--role must be "admin" or "member".');
|
|
48
|
+
}
|
|
49
|
+
export function roleTable(roles) {
|
|
50
|
+
return formatTable(["name", "members", "description", "uuid"], roles.map((r) => [
|
|
51
|
+
r.name,
|
|
52
|
+
String(r.member_uuids.length),
|
|
53
|
+
r.description || "-",
|
|
54
|
+
r.uuid,
|
|
55
|
+
]));
|
|
56
|
+
}
|
|
57
|
+
// Resolve an org-role reference — a UUID or a name (case-insensitive) — against a
|
|
58
|
+
// fetched role list.
|
|
59
|
+
export function resolveRoleRef(roles, ref) {
|
|
60
|
+
const needle = ref.trim();
|
|
61
|
+
if (!needle)
|
|
62
|
+
throw new OrgAdminError("Expected a role (name or UUID).");
|
|
63
|
+
const byUuid = roles.find((r) => r.uuid === needle);
|
|
64
|
+
if (byUuid)
|
|
65
|
+
return byUuid;
|
|
66
|
+
const lower = needle.toLowerCase();
|
|
67
|
+
const byName = roles.filter((r) => r.name.toLowerCase() === lower);
|
|
68
|
+
if (byName.length === 1)
|
|
69
|
+
return byName[0];
|
|
70
|
+
if (byName.length > 1) {
|
|
71
|
+
throw new OrgAdminError(`Multiple roles named "${ref}" — use the UUID.`);
|
|
72
|
+
}
|
|
73
|
+
throw new OrgAdminError(`No role matches "${ref}" (try a name or UUID).`);
|
|
74
|
+
}
|
|
75
|
+
// How the subject label is resolved for display: org → "everyone"; role/user/app →
|
|
76
|
+
// looked up by uuid in the provided maps, else the raw type.
|
|
77
|
+
export function grantTable(grants, roleNames, userEmails, appSlugs = new Map()) {
|
|
78
|
+
return formatTable(["id", "subject", "action", "resource_type", "resource_id"], grants.map((g) => [
|
|
79
|
+
String(g.id),
|
|
80
|
+
grantSubjectLabel(g, roleNames, userEmails, appSlugs),
|
|
81
|
+
g.action,
|
|
82
|
+
g.resource_type,
|
|
83
|
+
g.resource_id,
|
|
84
|
+
]));
|
|
85
|
+
}
|
|
86
|
+
export function grantSubjectLabel(g, roleNames, userEmails, appSlugs = new Map()) {
|
|
87
|
+
if (g.subject_type === "org")
|
|
88
|
+
return "everyone";
|
|
89
|
+
if (!g.subject_uuid)
|
|
90
|
+
return g.subject_type;
|
|
91
|
+
if (g.subject_type === "role")
|
|
92
|
+
return `role:${roleNames.get(g.subject_uuid) ?? g.subject_uuid}`;
|
|
93
|
+
if (g.subject_type === "user")
|
|
94
|
+
return `user:${userEmails.get(g.subject_uuid) ?? g.subject_uuid}`;
|
|
95
|
+
if (g.subject_type === "app")
|
|
96
|
+
return `app:${appSlugs.get(g.subject_uuid) ?? g.subject_uuid}`;
|
|
97
|
+
return `${g.subject_type}:${g.subject_uuid}`;
|
|
98
|
+
}
|
|
99
|
+
export function parseGrantSubject(value) {
|
|
100
|
+
const raw = (value ?? "").trim();
|
|
101
|
+
if (!raw) {
|
|
102
|
+
throw new OrgAdminError("--subject is required: org | role:<name|uuid> | user:<email|uuid>.");
|
|
103
|
+
}
|
|
104
|
+
if (raw.toLowerCase() === "org" || raw.toLowerCase() === "everyone")
|
|
105
|
+
return { kind: "org" };
|
|
106
|
+
const idx = raw.indexOf(":");
|
|
107
|
+
if (idx === -1) {
|
|
108
|
+
throw new OrgAdminError(`Bad --subject "${value}". Use org | role:<name|uuid> | user:<email|uuid>.`);
|
|
109
|
+
}
|
|
110
|
+
const kind = raw.slice(0, idx).toLowerCase();
|
|
111
|
+
const ref = raw.slice(idx + 1).trim();
|
|
112
|
+
if (!ref)
|
|
113
|
+
throw new OrgAdminError(`--subject ${kind}: is missing a value.`);
|
|
114
|
+
if (kind === "role")
|
|
115
|
+
return { kind: "role", ref };
|
|
116
|
+
if (kind === "user")
|
|
117
|
+
return { kind: "user", ref };
|
|
118
|
+
throw new OrgAdminError(`Unknown --subject kind "${kind}". Use org, role: or user:.`);
|
|
119
|
+
}
|
|
120
|
+
// The grant resource types the management API accepts (the implied action is
|
|
121
|
+
// stored server-side). `app` grants are manifest-managed, not settable here.
|
|
122
|
+
export const GRANT_RESOURCE_TYPES = [
|
|
123
|
+
"app",
|
|
124
|
+
"llm",
|
|
125
|
+
"email",
|
|
126
|
+
"sc_endpoint",
|
|
127
|
+
"connector",
|
|
128
|
+
"saved_query",
|
|
129
|
+
"agent",
|
|
130
|
+
];
|
|
131
|
+
export function parseResourceType(value) {
|
|
132
|
+
const v = (value ?? "").trim();
|
|
133
|
+
if (!v) {
|
|
134
|
+
throw new OrgAdminError(`--resource is required (${GRANT_RESOURCE_TYPES.join(" | ")}).`);
|
|
135
|
+
}
|
|
136
|
+
if (!GRANT_RESOURCE_TYPES.includes(v)) {
|
|
137
|
+
throw new OrgAdminError(`Unknown --resource "${value}". One of: ${GRANT_RESOURCE_TYPES.join(", ")}.`);
|
|
138
|
+
}
|
|
139
|
+
return v;
|
|
140
|
+
}
|
|
141
|
+
export function parseResourceIds(value) {
|
|
142
|
+
const ids = value === undefined ? [] : splitList(value);
|
|
143
|
+
if (ids.length === 0) {
|
|
144
|
+
throw new OrgAdminError('--ids is required (comma-separated, e.g. "*" or a name).');
|
|
145
|
+
}
|
|
146
|
+
return ids;
|
|
147
|
+
}
|
|
148
|
+
export function renderEffective(eff, who) {
|
|
149
|
+
const lines = [`Effective access for ${who}`];
|
|
150
|
+
if (eff.admin) {
|
|
151
|
+
lines.push(" admin/owner tier — full access to everything (bypasses grants).");
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
const keys = Object.keys(eff.sections);
|
|
155
|
+
const hasContent = eff.apps.length > 0 ||
|
|
156
|
+
keys.some((k) => eff.sections[k].all || eff.sections[k].items.length > 0);
|
|
157
|
+
if (!hasContent) {
|
|
158
|
+
lines.push(" (no grants — org defaults only)");
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
for (const key of keys) {
|
|
162
|
+
const s = eff.sections[key];
|
|
163
|
+
if (s.all) {
|
|
164
|
+
lines.push(` ${key}: all${s.via ? ` (via ${s.via})` : ""}`);
|
|
165
|
+
}
|
|
166
|
+
else if (s.items.length > 0) {
|
|
167
|
+
// Skip empty sections — the server sends every section key even when the
|
|
168
|
+
// member has no grant there; printing "key: -" for each is just noise.
|
|
169
|
+
const items = s.items.map((i) => `${i.resource_id} (${i.via})`).join(", ");
|
|
170
|
+
lines.push(` ${key}: ${items}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (eff.apps.length > 0) {
|
|
174
|
+
lines.push(" apps:");
|
|
175
|
+
for (const a of eff.apps)
|
|
176
|
+
lines.push(` ${a.name} (${a.via})`);
|
|
177
|
+
}
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
export function appTable(apps) {
|
|
181
|
+
return formatTable(["slug", "name", "status", "access", "your_role", "manage"], apps.map((a) => [
|
|
182
|
+
a.app_slug,
|
|
183
|
+
a.name,
|
|
184
|
+
a.status,
|
|
185
|
+
a.access_mode,
|
|
186
|
+
a.your_role ?? "-",
|
|
187
|
+
a.can_manage ? "yes" : "no",
|
|
188
|
+
]));
|
|
189
|
+
}
|
|
190
|
+
// Resolve an app reference — a slug (preferred) or a UUID — against a fetched app
|
|
191
|
+
// list.
|
|
192
|
+
export function resolveAppRef(apps, ref) {
|
|
193
|
+
const needle = ref.trim();
|
|
194
|
+
if (!needle)
|
|
195
|
+
throw new OrgAdminError("Expected an app (slug or UUID).");
|
|
196
|
+
const bySlug = apps.find((a) => a.app_slug === needle);
|
|
197
|
+
if (bySlug)
|
|
198
|
+
return bySlug;
|
|
199
|
+
const byUuid = apps.find((a) => a.uuid === needle);
|
|
200
|
+
if (byUuid)
|
|
201
|
+
return byUuid;
|
|
202
|
+
throw new OrgAdminError(`No app matches "${ref}" (try a slug or UUID).`);
|
|
203
|
+
}
|
|
204
|
+
export const ACCESS_MODES = ["organization", "private", "restricted"];
|
|
205
|
+
export function parseAccessMode(value) {
|
|
206
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
207
|
+
if (ACCESS_MODES.includes(v)) {
|
|
208
|
+
return v;
|
|
209
|
+
}
|
|
210
|
+
throw new OrgAdminError(`--mode must be one of: ${ACCESS_MODES.join(", ")}.`);
|
|
211
|
+
}
|
|
212
|
+
export function renderAppAccess(slug, access) {
|
|
213
|
+
const lines = [`Access for ${slug}: ${access.mode}`];
|
|
214
|
+
if (access.grants.length === 0) {
|
|
215
|
+
lines.push(" (no per-user grants)");
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
}
|
|
218
|
+
lines.push(formatTable(["email", "name", "role"], access.grants.map((g) => [g.email, g.name, g.role]))
|
|
219
|
+
.split("\n")
|
|
220
|
+
.map((l) => ` ${l}`)
|
|
221
|
+
.join("\n"));
|
|
222
|
+
return lines.join("\n");
|
|
223
|
+
}
|
|
224
|
+
// Comma-separated member refs (emails/uuids) for `--members` in restricted mode.
|
|
225
|
+
export function parseMemberList(value) {
|
|
226
|
+
return value === undefined ? [] : splitList(value);
|
|
227
|
+
}
|