@sendbird/cli 0.0.0-stage → 0.4.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/.claude-plugin/plugin.json +11 -0
- package/.codex-plugin/plugin.json +39 -0
- package/LICENSE +13 -0
- package/NOTICE.md +1105 -0
- package/PROVENANCE.md +15 -0
- package/README.md +44 -2
- package/dist/acl.d.ts +2 -0
- package/dist/acl.js +12 -0
- package/dist/acl.js.map +1 -0
- package/dist/billing-plan.d.ts +68 -0
- package/dist/billing-plan.js +235 -0
- package/dist/billing-plan.js.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +20 -0
- package/dist/browser.js.map +1 -0
- package/dist/calls-catalog.d.ts +79 -0
- package/dist/calls-catalog.js +322 -0
- package/dist/calls-catalog.js.map +1 -0
- package/dist/calls-credits.d.ts +31 -0
- package/dist/calls-credits.js +224 -0
- package/dist/calls-credits.js.map +1 -0
- package/dist/calls-trial.d.ts +45 -0
- package/dist/calls-trial.js +77 -0
- package/dist/calls-trial.js.map +1 -0
- package/dist/calls.d.ts +10 -0
- package/dist/calls.js +470 -0
- package/dist/calls.js.map +1 -0
- package/dist/catalog.d.ts +67 -0
- package/dist/catalog.js +784 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +502 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +86 -0
- package/dist/core.js +531 -0
- package/dist/core.js.map +1 -0
- package/dist/diagnostics.d.ts +206 -0
- package/dist/diagnostics.js +664 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/discovery.d.ts +703 -0
- package/dist/discovery.js +846 -0
- package/dist/discovery.js.map +1 -0
- package/dist/docs.d.ts +31 -0
- package/dist/docs.js +139 -0
- package/dist/docs.js.map +1 -0
- package/dist/features.d.ts +131 -0
- package/dist/features.js +227 -0
- package/dist/features.js.map +1 -0
- package/dist/generated-api.d.ts +116 -0
- package/dist/generated-api.js +118 -0
- package/dist/generated-api.js.map +1 -0
- package/dist/human-format.d.ts +40 -0
- package/dist/human-format.js +205 -0
- package/dist/human-format.js.map +1 -0
- package/dist/human-list.d.ts +2 -0
- package/dist/human-list.js +368 -0
- package/dist/human-list.js.map +1 -0
- package/dist/human-output.d.ts +10 -0
- package/dist/human-output.js +1351 -0
- package/dist/human-output.js.map +1 -0
- package/dist/human-pro.d.ts +3 -0
- package/dist/human-pro.js +201 -0
- package/dist/human-pro.js.map +1 -0
- package/dist/human-progress.d.ts +5 -0
- package/dist/human-progress.js +49 -0
- package/dist/human-progress.js.map +1 -0
- package/dist/local.d.ts +42 -0
- package/dist/local.js +707 -0
- package/dist/local.js.map +1 -0
- package/dist/oauth-callback.d.ts +3 -0
- package/dist/oauth-callback.js +66 -0
- package/dist/oauth-callback.js.map +1 -0
- package/dist/oauth-lock.d.ts +6 -0
- package/dist/oauth-lock.js +212 -0
- package/dist/oauth-lock.js.map +1 -0
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +493 -0
- package/dist/oauth.js.map +1 -0
- package/dist/presentation.d.ts +208 -0
- package/dist/presentation.js +156 -0
- package/dist/presentation.js.map +1 -0
- package/dist/pro-api-catalog.d.ts +24 -0
- package/dist/pro-api-catalog.js +619 -0
- package/dist/pro-api-catalog.js.map +1 -0
- package/dist/pro-api.d.ts +10 -0
- package/dist/pro-api.js +338 -0
- package/dist/pro-api.js.map +1 -0
- package/dist/project.d.ts +34 -0
- package/dist/project.js +62 -0
- package/dist/project.js.map +1 -0
- package/dist/recovery.d.ts +41 -0
- package/dist/recovery.js +417 -0
- package/dist/recovery.js.map +1 -0
- package/dist/runner.d.ts +47 -0
- package/dist/runner.js +603 -0
- package/dist/runner.js.map +1 -0
- package/dist/sdk-catalog.d.ts +69 -0
- package/dist/sdk-catalog.js +527 -0
- package/dist/sdk-catalog.js.map +1 -0
- package/dist/sdk-config.d.ts +19 -0
- package/dist/sdk-config.js +85 -0
- package/dist/sdk-config.js.map +1 -0
- package/dist/sdk-dto.d.ts +244 -0
- package/dist/sdk-dto.js +98 -0
- package/dist/sdk-dto.js.map +1 -0
- package/dist/sdk-limits.d.ts +2 -0
- package/dist/sdk-limits.js +3 -0
- package/dist/sdk-limits.js.map +1 -0
- package/dist/sdk-node-file-reader.d.ts +6 -0
- package/dist/sdk-node-file-reader.js +38 -0
- package/dist/sdk-node-file-reader.js.map +1 -0
- package/dist/sdk-pro-catalog.d.ts +22 -0
- package/dist/sdk-pro-catalog.js +234 -0
- package/dist/sdk-pro-catalog.js.map +1 -0
- package/dist/sdk-pro-runtime.d.ts +31 -0
- package/dist/sdk-pro-runtime.js +293 -0
- package/dist/sdk-pro-runtime.js.map +1 -0
- package/dist/sdk-query.d.ts +10 -0
- package/dist/sdk-query.js +29 -0
- package/dist/sdk-query.js.map +1 -0
- package/dist/sdk-runtime.d.ts +15 -0
- package/dist/sdk-runtime.js +685 -0
- package/dist/sdk-runtime.js.map +1 -0
- package/dist/sdk-worker.d.ts +1 -0
- package/dist/sdk-worker.js +26 -0
- package/dist/sdk-worker.js.map +1 -0
- package/dist/sdk.d.ts +29 -0
- package/dist/sdk.js +346 -0
- package/dist/sdk.js.map +1 -0
- package/dist/search-engine.d.ts +17 -0
- package/dist/search-engine.js +54 -0
- package/dist/search-engine.js.map +1 -0
- package/dist/search-intents.d.ts +2 -0
- package/dist/search-intents.js +106 -0
- package/dist/search-intents.js.map +1 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +58 -0
- package/dist/search.js.map +1 -0
- package/dist/secret-output.d.ts +2 -0
- package/dist/secret-output.js +108 -0
- package/dist/secret-output.js.map +1 -0
- package/dist/shell.d.ts +3 -0
- package/dist/shell.js +14 -0
- package/dist/shell.js.map +1 -0
- package/dist/skills.d.ts +210 -0
- package/dist/skills.js +374 -0
- package/dist/skills.js.map +1 -0
- package/dist/value-semantics.d.ts +70 -0
- package/dist/value-semantics.js +173 -0
- package/dist/value-semantics.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/windows.d.ts +4 -0
- package/dist/windows.js +136 -0
- package/dist/windows.js.map +1 -0
- package/dist/workflows.d.ts +228 -0
- package/dist/workflows.js +1207 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +90 -4
- package/plugin.json +25 -0
- package/references/agent-guide.json +199 -0
- package/references/commands.md +247 -0
- package/references/dashboard-inventory.json +14796 -0
- package/references/docs-index.json +5329 -0
- package/references/sdk-commands.json +15065 -0
- package/references/workflows.json +1246 -0
- package/skills/sendbird/SKILL.md +96 -0
- package/skills/sendbird/agents/openai.yaml +4 -0
- package/skills/sendbird/references/build-chat-app.md +50 -0
- package/skills/sendbird/references/client-guide.md +34 -0
- package/skills/sendbird/references/client-plugin-bridge.md +77 -0
- package/skills/sendbird/references/client-plugin-install.md +39 -0
- package/skills/sendbird/references/operational-checks.md +35 -0
- package/skills/sendbird/references/pro-capabilities.md +52 -0
- package/skills/sendbird/scripts/client-guide.mjs +283 -0
- package/skills/sendbird/scripts/client-plugin-bridge.mjs +703 -0
- package/skills/sendbird-setup/SKILL.md +18 -0
- package/skills/sendbird-setup/agents/openai.yaml +4 -0
- package/skills/sendbird-setup/references/onboarding.md +73 -0
- package/skills/sendbird-setup/scripts/install-cli.mjs +447 -0
- package/skills/sendbird-setup/scripts/npm-run.mjs +34 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { permissionMatches } from "./acl.js";
|
|
2
|
+
export { permissionMatches } from "./acl.js";
|
|
3
|
+
import { proPermission } from "./pro-api.js";
|
|
4
|
+
/** Read-only evidence collection. No SDK connection, Chat user token read, or mutation probe. */
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { readProject } from "./project.js";
|
|
9
|
+
import { sdkConfigSchema } from "./sdk-config.js";
|
|
10
|
+
import { CliError, encodeSegment, profile, request, } from "./core.js";
|
|
11
|
+
import { definitions } from "./catalog.js";
|
|
12
|
+
import { sdkDefinitions } from "./sdk-catalog.js";
|
|
13
|
+
import { checkAppOrg, execute, context, transport, verify, } from "./runner.js";
|
|
14
|
+
const status = z.enum(["allowed", "denied", "unknown"]);
|
|
15
|
+
const checkSchema = z.object({
|
|
16
|
+
id: z.string(),
|
|
17
|
+
status,
|
|
18
|
+
source: z.string(),
|
|
19
|
+
reason: z.string(),
|
|
20
|
+
});
|
|
21
|
+
const observed = z.boolean().nullable();
|
|
22
|
+
export const doctorSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
ok: z.literal(true),
|
|
25
|
+
node: z.string(),
|
|
26
|
+
profile: z.string(),
|
|
27
|
+
environment: z.enum(["staging", "prod"]),
|
|
28
|
+
org_id: z.string().optional(),
|
|
29
|
+
app_id: z.string().optional(),
|
|
30
|
+
role: z.unknown().optional(),
|
|
31
|
+
application: z
|
|
32
|
+
.object({
|
|
33
|
+
name: z.string().nullable(),
|
|
34
|
+
region: z.string().nullable(),
|
|
35
|
+
family: z.enum(["standard", "delight"]).nullable(),
|
|
36
|
+
})
|
|
37
|
+
.nullable(),
|
|
38
|
+
checks: z.array(checkSchema),
|
|
39
|
+
assessment: z
|
|
40
|
+
.object({
|
|
41
|
+
command: z.string(),
|
|
42
|
+
status,
|
|
43
|
+
mutation_attempted: z.literal(false),
|
|
44
|
+
input_validated: z.literal(false),
|
|
45
|
+
resource_id: z.string().nullable(),
|
|
46
|
+
channel_type: z.string().nullable(),
|
|
47
|
+
actor_user_id: z.string().nullable(),
|
|
48
|
+
note: z.string(),
|
|
49
|
+
})
|
|
50
|
+
.nullable(),
|
|
51
|
+
subscription: z.object({
|
|
52
|
+
status: z.enum(["observed", "unavailable", "scope_unverified"]),
|
|
53
|
+
name: z.string().nullable(),
|
|
54
|
+
display_name: z.string().nullable(),
|
|
55
|
+
type: z.string().nullable(),
|
|
56
|
+
is_self_service_pro: observed,
|
|
57
|
+
}),
|
|
58
|
+
features: z.array(z.object({
|
|
59
|
+
feature: z.string(),
|
|
60
|
+
entitlement: observed,
|
|
61
|
+
reported_enabled: observed,
|
|
62
|
+
detail_snapshot: observed,
|
|
63
|
+
activation: z.enum([
|
|
64
|
+
"reported_enabled",
|
|
65
|
+
"reported_disabled",
|
|
66
|
+
"conflicting",
|
|
67
|
+
"unknown",
|
|
68
|
+
]),
|
|
69
|
+
runtime_verified: z.literal(false),
|
|
70
|
+
})),
|
|
71
|
+
limits: z.array(z.object({
|
|
72
|
+
metric: z.string(),
|
|
73
|
+
included_units: z.number().nullable(),
|
|
74
|
+
configured_hard_limit: z.number().nullable(),
|
|
75
|
+
source: z.string(),
|
|
76
|
+
})),
|
|
77
|
+
app_capacity: z.object({
|
|
78
|
+
count: z.number().int().nonnegative().nullable(),
|
|
79
|
+
maximum: z.number().int().nonnegative().nullable(),
|
|
80
|
+
source: z.string(),
|
|
81
|
+
}),
|
|
82
|
+
regions: z.array(z.object({
|
|
83
|
+
id: z.string(),
|
|
84
|
+
family: z.enum(["standard", "delight"]),
|
|
85
|
+
available: observed,
|
|
86
|
+
chat: observed,
|
|
87
|
+
calls: observed,
|
|
88
|
+
})),
|
|
89
|
+
actor: z
|
|
90
|
+
.object({
|
|
91
|
+
user_id: z.string(),
|
|
92
|
+
active: observed,
|
|
93
|
+
membership: z.enum([
|
|
94
|
+
"joined",
|
|
95
|
+
"invited",
|
|
96
|
+
"absent",
|
|
97
|
+
"unknown",
|
|
98
|
+
"not_applicable",
|
|
99
|
+
]),
|
|
100
|
+
membership_complete: z.boolean(),
|
|
101
|
+
})
|
|
102
|
+
.nullable(),
|
|
103
|
+
next: z.array(z.string()),
|
|
104
|
+
})
|
|
105
|
+
.strict();
|
|
106
|
+
const row = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
107
|
+
const text = (v) => (typeof v === "string" ? v : null);
|
|
108
|
+
const bool = (v) => (typeof v === "boolean" ? v : null);
|
|
109
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
110
|
+
const integer = (v) => Number.isSafeInteger(v) && Number(v) >= 0 ? Number(v) : null;
|
|
111
|
+
const sourceRoot = "Gate reference ACL and explicit CLI safety prerequisites; server remains authoritative";
|
|
112
|
+
export function commandPermissions(command, type) {
|
|
113
|
+
const proDefinition = definitions.find((d) => d.name === command);
|
|
114
|
+
const proRule = proDefinition && proPermission(proDefinition, type);
|
|
115
|
+
if (proRule)
|
|
116
|
+
return [proRule];
|
|
117
|
+
if (command === "org create")
|
|
118
|
+
return []; // Authenticated onboarding; no existing-org ACL.
|
|
119
|
+
if (command === "app create")
|
|
120
|
+
return ["organization.applications.all"];
|
|
121
|
+
if (command === "app feature set" ||
|
|
122
|
+
command === "app rename" ||
|
|
123
|
+
command === "app auth-policy set" ||
|
|
124
|
+
/^calls (settings .+ set|push (create|update|delete))$/.test(command))
|
|
125
|
+
return ["application.settings.all"];
|
|
126
|
+
if (command.startsWith("chat user ")) {
|
|
127
|
+
if (command === "chat user token get")
|
|
128
|
+
return ["application.users.all", "calls.studio.all"];
|
|
129
|
+
if (command === "chat user get")
|
|
130
|
+
return [
|
|
131
|
+
"application.users.view",
|
|
132
|
+
"moderation.openChannel.view",
|
|
133
|
+
"moderation.groupChannel.view",
|
|
134
|
+
"calls.studio.all",
|
|
135
|
+
"notifications.userInsights.view",
|
|
136
|
+
];
|
|
137
|
+
if (command === "chat user list" || command === "chat user search")
|
|
138
|
+
return ["application.users.view", "notifications.campaign.view"];
|
|
139
|
+
if (/ (get|list|search)$/.test(command))
|
|
140
|
+
return ["application.users.view"];
|
|
141
|
+
if ([
|
|
142
|
+
"chat user delete",
|
|
143
|
+
"chat user activate",
|
|
144
|
+
"chat user deactivate",
|
|
145
|
+
].includes(command))
|
|
146
|
+
return ["application.users.all"];
|
|
147
|
+
return ["application.users.all", "calls.studio.all"];
|
|
148
|
+
}
|
|
149
|
+
if (command.startsWith("chat channel ") &&
|
|
150
|
+
!sdkDefinitions.some((d) => d.name === command)) {
|
|
151
|
+
if (!["open", "group", "supergroup"].includes(type ?? ""))
|
|
152
|
+
return null;
|
|
153
|
+
const name = type === "open"
|
|
154
|
+
? "openChannel"
|
|
155
|
+
: type === "group"
|
|
156
|
+
? "groupChannel"
|
|
157
|
+
: "supergroupChannel";
|
|
158
|
+
return [
|
|
159
|
+
`moderation.${name}.${/ (get|list|search)$/.test(command) ? "view" : "all"}`,
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
if (/^billing plan (list|check|change)$/.test(command))
|
|
163
|
+
return ["organization.general.all"];
|
|
164
|
+
// SDK authorization is not inherited from the Dashboard role. This is only
|
|
165
|
+
// the stricter Gate prerequisite for the CLI's memory-only user-token read.
|
|
166
|
+
if (sdkDefinitions.some((d) => d.name === command))
|
|
167
|
+
return ["application.users.all", "calls.studio.all"];
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const featureKeys = [
|
|
171
|
+
["supergroup", "supergroup", "super_group_channel"],
|
|
172
|
+
["poll", "poll", "poll"],
|
|
173
|
+
["delivery_receipt", "delivery_receipt", "delivery_receipt"],
|
|
174
|
+
["reactions", "reactions", "reactions"],
|
|
175
|
+
["translation_tools", "translation_tools", "auto_trans"],
|
|
176
|
+
["image_moderation", "image_moderation", "image_moderation"],
|
|
177
|
+
["profanity_filter", "profanity_filter", "profanity_filter"],
|
|
178
|
+
["announcement", "announcement", "announcement"],
|
|
179
|
+
["message_search", "message_search_index", "message_search_v3"],
|
|
180
|
+
];
|
|
181
|
+
export function featureEvidence(plan, detail, enabled) {
|
|
182
|
+
return featureKeys.map(([feature, key, legacy]) => {
|
|
183
|
+
let entitlement = bool(row(plan[key]).enabled);
|
|
184
|
+
if (feature === "message_search") {
|
|
185
|
+
const query = bool(row(plan.message_search_query).enabled);
|
|
186
|
+
entitlement =
|
|
187
|
+
entitlement === false || query === false
|
|
188
|
+
? false
|
|
189
|
+
: entitlement === true && query === true
|
|
190
|
+
? true
|
|
191
|
+
: null;
|
|
192
|
+
}
|
|
193
|
+
const a = bool(enabled[feature]), b = bool(detail[legacy]);
|
|
194
|
+
return {
|
|
195
|
+
feature,
|
|
196
|
+
entitlement,
|
|
197
|
+
reported_enabled: a,
|
|
198
|
+
detail_snapshot: b,
|
|
199
|
+
activation: a !== null && b !== null && a !== b
|
|
200
|
+
? "conflicting"
|
|
201
|
+
: a === true
|
|
202
|
+
? "reported_enabled"
|
|
203
|
+
: a === false
|
|
204
|
+
? "reported_disabled"
|
|
205
|
+
: "unknown",
|
|
206
|
+
runtime_verified: false,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** Read-only comparison of ./sendbird.json and ./sdk-config.json in the working directory with the diagnosed app. */
|
|
211
|
+
export function projectContextChecks(cwd, appId, env) {
|
|
212
|
+
const out = [];
|
|
213
|
+
if (existsSync(join(cwd, "sendbird.json")))
|
|
214
|
+
try {
|
|
215
|
+
// existsSync passed, so readProject returns the parsed marker or throws.
|
|
216
|
+
const project = readProject(cwd);
|
|
217
|
+
if (appId && project.app_id !== appId)
|
|
218
|
+
out.push({
|
|
219
|
+
id: "project_marker",
|
|
220
|
+
status: "unknown",
|
|
221
|
+
source: "./sendbird.json",
|
|
222
|
+
reason: `The repository marker binds app ${project.app_id}, not the diagnosed app ${appId}; commands run with --project target the marker's app.`,
|
|
223
|
+
});
|
|
224
|
+
else
|
|
225
|
+
out.push({
|
|
226
|
+
id: "project_marker",
|
|
227
|
+
status: appId ? "allowed" : "unknown",
|
|
228
|
+
source: "./sendbird.json",
|
|
229
|
+
reason: appId
|
|
230
|
+
? `The repository marker binds the diagnosed app ${project.app_id}.`
|
|
231
|
+
: `The repository marker binds app ${project.app_id}; no app was selected for comparison.`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
out.push({
|
|
236
|
+
id: "project_marker",
|
|
237
|
+
status: "unknown",
|
|
238
|
+
source: "./sendbird.json",
|
|
239
|
+
reason: e.message,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (existsSync(join(cwd, "sdk-config.json")))
|
|
243
|
+
try {
|
|
244
|
+
const parsed = sdkConfigSchema.safeParse(JSON.parse(readFileSync(join(cwd, "sdk-config.json"), "utf8")));
|
|
245
|
+
if (!parsed.success)
|
|
246
|
+
out.push({
|
|
247
|
+
id: "project_sdk_config",
|
|
248
|
+
status: "unknown",
|
|
249
|
+
source: "./sdk-config.json",
|
|
250
|
+
reason: "File is not an unmodified app sdk-config output; compare it manually with app sdk-config --raw.",
|
|
251
|
+
});
|
|
252
|
+
else if ((appId && parsed.data.app_id !== appId) ||
|
|
253
|
+
parsed.data.environment !== env)
|
|
254
|
+
out.push({
|
|
255
|
+
id: "project_sdk_config",
|
|
256
|
+
status: "unknown",
|
|
257
|
+
source: "./sdk-config.json",
|
|
258
|
+
reason: `Explicit SDK config binds app ${parsed.data.app_id} (${parsed.data.environment}, ${parsed.data.api_host}); SDK commands using --sdk-config-file connect there, not to app ${appId ?? "(unselected)"} on ${env}.`,
|
|
259
|
+
});
|
|
260
|
+
else
|
|
261
|
+
out.push({
|
|
262
|
+
id: "project_sdk_config",
|
|
263
|
+
status: appId ? "allowed" : "unknown",
|
|
264
|
+
source: "./sdk-config.json",
|
|
265
|
+
reason: appId
|
|
266
|
+
? `Explicit SDK config matches the diagnosed app (${parsed.data.api_host}).`
|
|
267
|
+
: "SDK config is present, but no app was selected for comparison.",
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
out.push({
|
|
272
|
+
id: "project_sdk_config",
|
|
273
|
+
status: "unknown",
|
|
274
|
+
source: "./sdk-config.json",
|
|
275
|
+
reason: "File could not be parsed as JSON.",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
export async function diagnose(opts, fields = {}, injected, cwd = process.cwd()) {
|
|
281
|
+
const command = fields.command?.replace(/^sendbird\s+/, "").trim();
|
|
282
|
+
const d = command
|
|
283
|
+
? [...definitions, ...sdkDefinitions].find((d) => d.name === command)
|
|
284
|
+
: undefined;
|
|
285
|
+
if (command && !d)
|
|
286
|
+
throw new CliError("E_INPUT", "Unknown diagnostic command; use sendbird commands --compact", 2);
|
|
287
|
+
if (fields.type && !["open", "group", "supergroup"].includes(fields.type))
|
|
288
|
+
throw new CliError("E_INPUT", "--type must be open, group or supergroup", 2);
|
|
289
|
+
for (const [key, value] of Object.entries(fields))
|
|
290
|
+
if (value !== undefined &&
|
|
291
|
+
(typeof value !== "string" ||
|
|
292
|
+
value.length > 2048 ||
|
|
293
|
+
/[\x00-\x1f\x7f]/.test(value)))
|
|
294
|
+
throw new CliError("E_INPUT", `Invalid diagnostic ${key}`, 2);
|
|
295
|
+
const selected = context(opts), p = profile(selected.profile), t = injected ?? transport(selected);
|
|
296
|
+
if (t.env !== p.env)
|
|
297
|
+
throw new CliError("E_CONTEXT", "Diagnostic transport environment does not match profile", 5);
|
|
298
|
+
const who = await verify(t);
|
|
299
|
+
const organizations = who.organizations ?? (who.organization ? [who.organization] : []);
|
|
300
|
+
const sole = typeof who.organization?.uid === "string"
|
|
301
|
+
? who.organization.uid
|
|
302
|
+
: organizations.length === 1
|
|
303
|
+
? text(organizations[0].uid)
|
|
304
|
+
: undefined;
|
|
305
|
+
const orgId = selected.orgId ?? sole ?? null, appId = selected.appId ?? null;
|
|
306
|
+
const permissions = Array.isArray(row(who.role).permissions) &&
|
|
307
|
+
row(who.role).permissions instanceof Array
|
|
308
|
+
? row(who.role).permissions.filter((v) => typeof v === "string")
|
|
309
|
+
: null;
|
|
310
|
+
const checks = [];
|
|
311
|
+
const add = (id, s, source, reason) => checks.push({ id, status: s, source, reason });
|
|
312
|
+
const read = async (path, app, query) => {
|
|
313
|
+
try {
|
|
314
|
+
return {
|
|
315
|
+
ok: true,
|
|
316
|
+
data: row(await request(t, { method: "GET", path, appId: app, query })),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
code: e instanceof CliError ? e.code : "E_INTERNAL",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
let org = {}, app = {}, plan = {}, enabled = {};
|
|
327
|
+
add("authentication", "allowed", "GET /dashboard/auth/verify2/", "Authenticated for the selected environment.");
|
|
328
|
+
const orgMember = orgId && organizations.some((o) => o.uid === orgId);
|
|
329
|
+
if (orgId && !orgMember)
|
|
330
|
+
add("organization", "denied", "GET /dashboard/auth/verify2/", "Selected organization was not returned for this account.");
|
|
331
|
+
else if (orgId) {
|
|
332
|
+
const r = await read(`/dashboard_api/organizations/${encodeSegment(orgId)}/`);
|
|
333
|
+
if (r.ok && r.data.uid === orgId) {
|
|
334
|
+
org = r.data;
|
|
335
|
+
add("organization", "allowed", "Organization detail", "Selected organization identity verified.");
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
add("organization", r.ok ? "unknown" : r.code === "E_PERMISSION" ? "denied" : "unknown", "Organization detail", r.ok
|
|
339
|
+
? "Response has a different or missing organization UID."
|
|
340
|
+
: `Organization read failed: ${r.code}.`);
|
|
341
|
+
}
|
|
342
|
+
else
|
|
343
|
+
add("organization", "unknown", "Selected context", "Select an accessible organization.");
|
|
344
|
+
if (appId && orgMember && org.uid === orgId) {
|
|
345
|
+
try {
|
|
346
|
+
app = await checkAppOrg(t, appId, orgId);
|
|
347
|
+
add("application", "allowed", "GET application get_detail and organization binding", "Application belongs to the selected organization.");
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
add("application", e instanceof CliError &&
|
|
351
|
+
["E_PERMISSION", "E_CONTEXT", "E_NOT_FOUND"].includes(e.code)
|
|
352
|
+
? "denied"
|
|
353
|
+
: "unknown", "Application identity/access", `Cannot verify selected app: ${e instanceof CliError ? e.code : "E_INTERNAL"}.`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else if (appId)
|
|
357
|
+
add("application", "unknown", "Selected context", "Verify organization access before inspecting the app.");
|
|
358
|
+
const result = {
|
|
359
|
+
ok: true,
|
|
360
|
+
node: process.version,
|
|
361
|
+
profile: p.name,
|
|
362
|
+
environment: p.env,
|
|
363
|
+
org_id: orgId ?? undefined,
|
|
364
|
+
app_id: appId ?? undefined,
|
|
365
|
+
role: who.role,
|
|
366
|
+
application: app.app_id === appId
|
|
367
|
+
? {
|
|
368
|
+
name: text(app.app_name),
|
|
369
|
+
region: text(app.region),
|
|
370
|
+
family: typeof app.is_delight_region === "boolean"
|
|
371
|
+
? app.is_delight_region
|
|
372
|
+
? "delight"
|
|
373
|
+
: "standard"
|
|
374
|
+
: null,
|
|
375
|
+
}
|
|
376
|
+
: null,
|
|
377
|
+
checks,
|
|
378
|
+
assessment: null,
|
|
379
|
+
subscription: {
|
|
380
|
+
status: "scope_unverified",
|
|
381
|
+
name: null,
|
|
382
|
+
display_name: null,
|
|
383
|
+
type: null,
|
|
384
|
+
is_self_service_pro: null,
|
|
385
|
+
},
|
|
386
|
+
features: [],
|
|
387
|
+
limits: [],
|
|
388
|
+
app_capacity: {
|
|
389
|
+
count: null,
|
|
390
|
+
maximum: integer(org.max_application_count),
|
|
391
|
+
source: "Organization max_application_count and account-scoped application list count",
|
|
392
|
+
},
|
|
393
|
+
regions: [],
|
|
394
|
+
actor: null,
|
|
395
|
+
next: [],
|
|
396
|
+
};
|
|
397
|
+
// Customer project files in the working directory are read-only, non-secret evidence.
|
|
398
|
+
for (const c of projectContextChecks(cwd, appId, p.env))
|
|
399
|
+
checks.push(c);
|
|
400
|
+
if (checks.some((c) => c.id.startsWith("project_") && c.status === "unknown"))
|
|
401
|
+
result.next.push("Compare project files with the intended app/environment before explicitly selecting --project or --sdk-config-file. These local files do not establish command denial or execution access.");
|
|
402
|
+
if (p.env === "staging" && appId)
|
|
403
|
+
result.next.push("Client code for this staging app must use its api_host/ws_host from app sdk-config --raw (customApiHost/customWebSocketHost); default SDK hosts reach production only.");
|
|
404
|
+
if (d?.method === "SDK")
|
|
405
|
+
result.next.push("For a failure reported from a customer project, compare its README, sendbird.json and any --sdk-config-file (app_id, api_host, ws_host) with app sdk-config before changing auth or moderation settings.");
|
|
406
|
+
if (orgId === sole && org.uid === orgId) {
|
|
407
|
+
const [sub, apps] = await Promise.all([
|
|
408
|
+
read("/dashboard_api/billing/subscriptions/info/", undefined, {
|
|
409
|
+
product: "CHAT",
|
|
410
|
+
}),
|
|
411
|
+
read("/dashboard/applications/", undefined, { limit: 1, offset: 0 }),
|
|
412
|
+
]);
|
|
413
|
+
if (sub.ok) {
|
|
414
|
+
const current = row(sub.data.current_subscription);
|
|
415
|
+
plan = row(current.plan);
|
|
416
|
+
const name = text(current.subscription_name), kind = text(current.subscription_type);
|
|
417
|
+
result.subscription = {
|
|
418
|
+
status: "observed",
|
|
419
|
+
name,
|
|
420
|
+
display_name: text(current.display_name),
|
|
421
|
+
type: kind,
|
|
422
|
+
is_self_service_pro: name === null || kind === null
|
|
423
|
+
? null
|
|
424
|
+
: kind !== "SELF_SERVE"
|
|
425
|
+
? false
|
|
426
|
+
: /^plan_b_/.test(name)
|
|
427
|
+
? true
|
|
428
|
+
: /^(plan_a_|developer$|free$|free_trial$)/.test(name)
|
|
429
|
+
? false
|
|
430
|
+
: null,
|
|
431
|
+
};
|
|
432
|
+
add("subscription", name ? "allowed" : "unknown", "GET subscriptions/info?product=CHAT", name
|
|
433
|
+
? "Organization Chat subscription observed; app.plan is not the organization tier."
|
|
434
|
+
: "No current subscription identity was returned.");
|
|
435
|
+
result.limits = [
|
|
436
|
+
"mau",
|
|
437
|
+
"pc",
|
|
438
|
+
"message_retention_period",
|
|
439
|
+
"file_storage",
|
|
440
|
+
"upload_traffic",
|
|
441
|
+
].map((metric) => ({
|
|
442
|
+
metric,
|
|
443
|
+
included_units: num(row(plan[metric]).purchased_units),
|
|
444
|
+
configured_hard_limit: num(row(plan[metric]).hard_limit),
|
|
445
|
+
source: `current_subscription.plan.${metric}`,
|
|
446
|
+
}));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
result.subscription.status = "unavailable";
|
|
450
|
+
add("subscription", "unknown", "GET subscriptions/info?product=CHAT", `Subscription read failed: ${sub.code}; this does not prove that the feature is unavailable.`);
|
|
451
|
+
}
|
|
452
|
+
if (apps.ok && Array.isArray(apps.data.results)) {
|
|
453
|
+
const count = integer(apps.data.count);
|
|
454
|
+
result.app_capacity.count =
|
|
455
|
+
count !== null && count >= apps.data.results.length ? count : null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else
|
|
459
|
+
add("subscription", "unknown", "Billing account scope", "Cannot bind account-scoped billing to the selected organization; skipped.");
|
|
460
|
+
for (const [key, family] of [
|
|
461
|
+
["regions", "standard"],
|
|
462
|
+
["delight_regions", "delight"],
|
|
463
|
+
])
|
|
464
|
+
for (const [id, value] of Object.entries(row(org[key]))) {
|
|
465
|
+
const r = row(value), features = row(r.features), a = bool(r.allowed), b = bool(r.allow_app_creation);
|
|
466
|
+
result.regions.push({
|
|
467
|
+
id,
|
|
468
|
+
family,
|
|
469
|
+
available: a === false || b === false
|
|
470
|
+
? false
|
|
471
|
+
: a === true && b === true
|
|
472
|
+
? true
|
|
473
|
+
: null,
|
|
474
|
+
chat: bool(features.chat),
|
|
475
|
+
calls: bool(features.calls),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
if (app.app_id === appId) {
|
|
479
|
+
const f = await read(`/dashboard_api/applications/${encodeSegment(appId)}/enabled_features/`);
|
|
480
|
+
if (f.ok)
|
|
481
|
+
enabled = f.data;
|
|
482
|
+
else
|
|
483
|
+
add("feature_read", "unknown", "GET application enabled_features", `Feature evidence unavailable: ${f.code}; not a denial of the requested operation.`);
|
|
484
|
+
result.features = featureEvidence(plan, row(app.current_premium_features), enabled);
|
|
485
|
+
}
|
|
486
|
+
if (command && d) {
|
|
487
|
+
const sdk = d.method === "SDK";
|
|
488
|
+
const required = commandPermissions(command, fields.type);
|
|
489
|
+
const usable = required?.filter((permission) => !permission.startsWith("calls.") || org.is_videochat_enabled === true);
|
|
490
|
+
add("implementation", "allowed", "Installed command catalog", "Command is implemented in this CLI build.");
|
|
491
|
+
if (required === null || permissions === null)
|
|
492
|
+
add("role", "unknown", sourceRoot, "No complete reference permission rule or role permission list is available.");
|
|
493
|
+
else if (required.length === 0)
|
|
494
|
+
add("role", "allowed", sourceRoot, "No existing-organization permission is required by this reference rule.");
|
|
495
|
+
else
|
|
496
|
+
add("role", usable?.some((req) => permissions.some((g) => permissionMatches(g, req)))
|
|
497
|
+
? "allowed"
|
|
498
|
+
: "denied", sourceRoot, `Required any: ${required.join(", ")}. ${sdk ? "This is the CLI OAuth token-read prerequisite, not SDK user authorization." : "Additional server restrictions may apply."}`);
|
|
499
|
+
if (d.scope === "app" && !app.app_id)
|
|
500
|
+
add("target", "unknown", "Selected context", "Select and verify an application before executing this command.");
|
|
501
|
+
if (command === "app create") {
|
|
502
|
+
const { count, maximum } = result.app_capacity;
|
|
503
|
+
add("app_capacity", count === null || maximum === null
|
|
504
|
+
? "unknown"
|
|
505
|
+
: count >= maximum
|
|
506
|
+
? "denied"
|
|
507
|
+
: "allowed", result.app_capacity.source, count === null || maximum === null
|
|
508
|
+
? "App capacity is not known."
|
|
509
|
+
: `${count} apps observed; configured maximum ${maximum}. This is a snapshot, not a reservation.`);
|
|
510
|
+
if (org.is_chat_paused === true)
|
|
511
|
+
add("chat_paused", "denied", "Organization detail", "Organization reports Chat paused.");
|
|
512
|
+
const region = fields.region ?? selected.region;
|
|
513
|
+
const candidates = result.regions.filter((r) => r.id === region && r.family === "standard");
|
|
514
|
+
add("region", !region || candidates.length !== 1 || candidates[0].available === null
|
|
515
|
+
? "unknown"
|
|
516
|
+
: candidates[0].available
|
|
517
|
+
? "allowed"
|
|
518
|
+
: "denied", "Organization regions", region
|
|
519
|
+
? `Standard region ${region} is ${candidates[0]?.available === true ? "available" : "not confirmed available"}. Delight selection and Calls activation need their own execution checks.`
|
|
520
|
+
: "No intended creation region supplied.");
|
|
521
|
+
}
|
|
522
|
+
const needed = [];
|
|
523
|
+
if (fields.type === "supergroup")
|
|
524
|
+
needed.push("supergroup");
|
|
525
|
+
if (command.includes("poll"))
|
|
526
|
+
needed.push("poll");
|
|
527
|
+
if (command.endsWith("translate"))
|
|
528
|
+
needed.push("translation_tools");
|
|
529
|
+
for (const key of needed) {
|
|
530
|
+
const f = result.features.find((f) => f.feature === key);
|
|
531
|
+
add(`feature:${key}`, "unknown", "Subscription + enabled_features + get_detail", f
|
|
532
|
+
? `Plan entitlement=${f.entitlement}; enabled API=${f.reported_enabled}; detail=${f.detail_snapshot}. Runtime not tested. Do not gate this operation on a false snapshot alone.`
|
|
533
|
+
: "No feature evidence is available.");
|
|
534
|
+
}
|
|
535
|
+
if (sdk) {
|
|
536
|
+
if (!fields.asUser)
|
|
537
|
+
add("sdk_actor", "unknown", "Diagnostic input", "Pass --as-user to inspect the actor. Doctor never connects or creates a Chat user.");
|
|
538
|
+
else if (app.app_id === appId) {
|
|
539
|
+
const r = await read(`/platform/v3/users/${encodeSegment(fields.asUser)}`, appId, { with_access_token: false });
|
|
540
|
+
result.actor = {
|
|
541
|
+
user_id: fields.asUser,
|
|
542
|
+
active: null,
|
|
543
|
+
membership: fields.type === "open" ? "not_applicable" : "unknown",
|
|
544
|
+
membership_complete: false,
|
|
545
|
+
};
|
|
546
|
+
if (r.ok && r.data.user_id === fields.asUser) {
|
|
547
|
+
result.actor.active = bool(r.data.is_active);
|
|
548
|
+
add("sdk_actor", result.actor.active === false ? "denied" : "allowed", "GET user without access token", "Actor identity observed. User token presence and SDK authentication are not checked.");
|
|
549
|
+
}
|
|
550
|
+
else
|
|
551
|
+
add("sdk_actor", !r.ok && r.code === "E_NOT_FOUND" ? "denied" : "unknown", "GET user without access token", r.ok
|
|
552
|
+
? "Actor identity response mismatch."
|
|
553
|
+
: `Actor read unavailable: ${r.code}. A forbidden diagnostic read is not proof of SDK denial.`);
|
|
554
|
+
if (fields.id && ["group", "supergroup"].includes(fields.type ?? "")) {
|
|
555
|
+
const kind = fields.type === "supergroup"
|
|
556
|
+
? "supergroup_channels"
|
|
557
|
+
: "group_channels";
|
|
558
|
+
const detail = await read(`/platform/v3/${kind}/${encodeSegment(fields.id)}`, appId);
|
|
559
|
+
if (detail.ok) {
|
|
560
|
+
const completeIdentity = typeof detail.data.channel_url === "string" &&
|
|
561
|
+
typeof detail.data.is_super === "boolean";
|
|
562
|
+
if (!completeIdentity)
|
|
563
|
+
add("channel_type", "unknown", "GET channel", "Channel response has missing or malformed identity/type fields; this is not proof of access denial.");
|
|
564
|
+
else if (detail.data.channel_url !== fields.id ||
|
|
565
|
+
detail.data.is_super !== (fields.type === "supergroup"))
|
|
566
|
+
add("channel_type", "denied", "GET channel", "The channel identity/type does not match the requested target.");
|
|
567
|
+
}
|
|
568
|
+
let token;
|
|
569
|
+
let found;
|
|
570
|
+
let complete = false;
|
|
571
|
+
const seen = new Set();
|
|
572
|
+
for (let page = 0; page < 5; page++) {
|
|
573
|
+
const members = await read(`/platform/v3/${kind}/${encodeSegment(fields.id)}/members`, appId, { limit: 100, ...(token ? { token } : {}) });
|
|
574
|
+
if (!members.ok || !Array.isArray(members.data.members))
|
|
575
|
+
break;
|
|
576
|
+
found = members.data.members
|
|
577
|
+
.map(row)
|
|
578
|
+
.find((m) => m.user_id === fields.asUser);
|
|
579
|
+
const next = text(members.data.next);
|
|
580
|
+
if (members.data.next !== "" && !next)
|
|
581
|
+
break;
|
|
582
|
+
if (!next) {
|
|
583
|
+
complete = true;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
if (found)
|
|
587
|
+
break;
|
|
588
|
+
if (seen.has(next))
|
|
589
|
+
break;
|
|
590
|
+
seen.add(next);
|
|
591
|
+
token = next;
|
|
592
|
+
}
|
|
593
|
+
result.actor.membership =
|
|
594
|
+
found?.state === "joined"
|
|
595
|
+
? "joined"
|
|
596
|
+
: found?.state === "invited"
|
|
597
|
+
? "invited"
|
|
598
|
+
: found
|
|
599
|
+
? "unknown"
|
|
600
|
+
: complete
|
|
601
|
+
? "absent"
|
|
602
|
+
: "unknown";
|
|
603
|
+
result.actor.membership_complete = complete;
|
|
604
|
+
add("membership", found?.state === "joined"
|
|
605
|
+
? "allowed"
|
|
606
|
+
: /^chat message (send|file|reply|update|delete)/.test(command) &&
|
|
607
|
+
(found?.state === "invited" || (!found && complete))
|
|
608
|
+
? "denied"
|
|
609
|
+
: "unknown", "Bounded Dashboard member pages", `Actor membership: ${result.actor.membership}. ${complete ? "Reached final page." : "Not a complete membership inventory."} Join/accept/leave/moderation have different SDK rules; no membership was changed.`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
add("sdk_authorization", "unknown", "SDK runtime", "No SDK connection or Chat user token read was performed. Sender/operator permissions and runtime feature support remain server decisions.");
|
|
613
|
+
}
|
|
614
|
+
let observedRead;
|
|
615
|
+
if (d.method === "GET" &&
|
|
616
|
+
!d.secret &&
|
|
617
|
+
(!d.scope || d.scope !== "app" || app.app_id === appId)) {
|
|
618
|
+
try {
|
|
619
|
+
// Reuse the real parser, target binding and response contract, for GETs
|
|
620
|
+
// only. Credential-bearing GETs and SDK connections are never probed.
|
|
621
|
+
await execute(d, {
|
|
622
|
+
params: {
|
|
623
|
+
...(fields.id ? { id: fields.id } : {}),
|
|
624
|
+
...(fields.type ? { type: fields.type } : {}),
|
|
625
|
+
},
|
|
626
|
+
}, selected, t);
|
|
627
|
+
observedRead = "allowed";
|
|
628
|
+
add("operation_read", "allowed", "Requested GET command", "The read completed with the real command contract. Reference ACL hints do not override this server result.");
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
observedRead =
|
|
632
|
+
e instanceof CliError &&
|
|
633
|
+
["E_PERMISSION", "E_CONTEXT", "E_NOT_FOUND"].includes(e.code)
|
|
634
|
+
? "denied"
|
|
635
|
+
: "unknown";
|
|
636
|
+
add("operation_read", observedRead, "Requested GET command", `Read did not complete: ${e instanceof CliError ? e.code : "E_INTERNAL"}. Missing input, response mismatch and read failure remain distinct from feature availability.`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const mutation = d.method === "SDK"
|
|
640
|
+
? Boolean(d.mutates)
|
|
641
|
+
: d.method !== "GET";
|
|
642
|
+
if (mutation)
|
|
643
|
+
add("mutation_validation", "unknown", "Read-only preflight", "No mutation was attempted. Full payload, uniqueness, concurrency and final server authorization are not verified.");
|
|
644
|
+
result.assessment = {
|
|
645
|
+
command,
|
|
646
|
+
status: observedRead ??
|
|
647
|
+
(checks.some((c) => !c.id.startsWith("project_") && c.status === "denied")
|
|
648
|
+
? "denied"
|
|
649
|
+
: "unknown"),
|
|
650
|
+
mutation_attempted: false,
|
|
651
|
+
input_validated: false,
|
|
652
|
+
resource_id: fields.id ?? null,
|
|
653
|
+
channel_type: fields.type ?? null,
|
|
654
|
+
actor_user_id: fields.asUser ?? null,
|
|
655
|
+
note: "Read-only evidence, not execution authorization. A successful diagnosis does not guarantee that the requested operation will succeed.",
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (!appId)
|
|
659
|
+
result.next.push("Select an application before diagnosing an app command.");
|
|
660
|
+
if (result.features.some((f) => f.activation === "conflicting"))
|
|
661
|
+
result.next.push("Feature sources disagree; compare sources and verify actual operation in an authorized test scenario.");
|
|
662
|
+
return doctorSchema.parse(result);
|
|
663
|
+
}
|
|
664
|
+
//# sourceMappingURL=diagnostics.js.map
|