@sendbird/cli 0.0.0-stage → 0.3.0-alpha.1

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.
Files changed (182) hide show
  1. package/.claude-plugin/plugin.json +11 -0
  2. package/.codex-plugin/plugin.json +33 -0
  3. package/LICENSE +13 -0
  4. package/NOTICE.md +1105 -0
  5. package/PROVENANCE.md +15 -0
  6. package/README.md +457 -2
  7. package/dist/acl.d.ts +2 -0
  8. package/dist/acl.js +12 -0
  9. package/dist/acl.js.map +1 -0
  10. package/dist/billing-plan.d.ts +68 -0
  11. package/dist/billing-plan.js +235 -0
  12. package/dist/billing-plan.js.map +1 -0
  13. package/dist/browser.d.ts +1 -0
  14. package/dist/browser.js +20 -0
  15. package/dist/browser.js.map +1 -0
  16. package/dist/calls-catalog.d.ts +79 -0
  17. package/dist/calls-catalog.js +322 -0
  18. package/dist/calls-catalog.js.map +1 -0
  19. package/dist/calls-credits.d.ts +31 -0
  20. package/dist/calls-credits.js +224 -0
  21. package/dist/calls-credits.js.map +1 -0
  22. package/dist/calls-trial.d.ts +45 -0
  23. package/dist/calls-trial.js +77 -0
  24. package/dist/calls-trial.js.map +1 -0
  25. package/dist/calls.d.ts +10 -0
  26. package/dist/calls.js +470 -0
  27. package/dist/calls.js.map +1 -0
  28. package/dist/catalog.d.ts +67 -0
  29. package/dist/catalog.js +784 -0
  30. package/dist/catalog.js.map +1 -0
  31. package/dist/cli.d.ts +6 -0
  32. package/dist/cli.js +502 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/core.d.ts +86 -0
  35. package/dist/core.js +531 -0
  36. package/dist/core.js.map +1 -0
  37. package/dist/diagnostics.d.ts +206 -0
  38. package/dist/diagnostics.js +664 -0
  39. package/dist/diagnostics.js.map +1 -0
  40. package/dist/discovery.d.ts +700 -0
  41. package/dist/discovery.js +838 -0
  42. package/dist/discovery.js.map +1 -0
  43. package/dist/docs.d.ts +31 -0
  44. package/dist/docs.js +137 -0
  45. package/dist/docs.js.map +1 -0
  46. package/dist/features.d.ts +131 -0
  47. package/dist/features.js +227 -0
  48. package/dist/features.js.map +1 -0
  49. package/dist/generated-api.d.ts +116 -0
  50. package/dist/generated-api.js +118 -0
  51. package/dist/generated-api.js.map +1 -0
  52. package/dist/human-format.d.ts +40 -0
  53. package/dist/human-format.js +205 -0
  54. package/dist/human-format.js.map +1 -0
  55. package/dist/human-list.d.ts +2 -0
  56. package/dist/human-list.js +368 -0
  57. package/dist/human-list.js.map +1 -0
  58. package/dist/human-output.d.ts +10 -0
  59. package/dist/human-output.js +1351 -0
  60. package/dist/human-output.js.map +1 -0
  61. package/dist/human-pro.d.ts +3 -0
  62. package/dist/human-pro.js +201 -0
  63. package/dist/human-pro.js.map +1 -0
  64. package/dist/human-progress.d.ts +5 -0
  65. package/dist/human-progress.js +49 -0
  66. package/dist/human-progress.js.map +1 -0
  67. package/dist/local.d.ts +42 -0
  68. package/dist/local.js +707 -0
  69. package/dist/local.js.map +1 -0
  70. package/dist/oauth-callback.d.ts +3 -0
  71. package/dist/oauth-callback.js +66 -0
  72. package/dist/oauth-callback.js.map +1 -0
  73. package/dist/oauth-lock.d.ts +6 -0
  74. package/dist/oauth-lock.js +212 -0
  75. package/dist/oauth-lock.js.map +1 -0
  76. package/dist/oauth.d.ts +32 -0
  77. package/dist/oauth.js +493 -0
  78. package/dist/oauth.js.map +1 -0
  79. package/dist/presentation.d.ts +208 -0
  80. package/dist/presentation.js +156 -0
  81. package/dist/presentation.js.map +1 -0
  82. package/dist/pro-api-catalog.d.ts +24 -0
  83. package/dist/pro-api-catalog.js +619 -0
  84. package/dist/pro-api-catalog.js.map +1 -0
  85. package/dist/pro-api.d.ts +10 -0
  86. package/dist/pro-api.js +338 -0
  87. package/dist/pro-api.js.map +1 -0
  88. package/dist/project.d.ts +34 -0
  89. package/dist/project.js +62 -0
  90. package/dist/project.js.map +1 -0
  91. package/dist/recovery.d.ts +41 -0
  92. package/dist/recovery.js +417 -0
  93. package/dist/recovery.js.map +1 -0
  94. package/dist/runner.d.ts +47 -0
  95. package/dist/runner.js +603 -0
  96. package/dist/runner.js.map +1 -0
  97. package/dist/sdk-catalog.d.ts +69 -0
  98. package/dist/sdk-catalog.js +527 -0
  99. package/dist/sdk-catalog.js.map +1 -0
  100. package/dist/sdk-config.d.ts +19 -0
  101. package/dist/sdk-config.js +85 -0
  102. package/dist/sdk-config.js.map +1 -0
  103. package/dist/sdk-dto.d.ts +244 -0
  104. package/dist/sdk-dto.js +98 -0
  105. package/dist/sdk-dto.js.map +1 -0
  106. package/dist/sdk-limits.d.ts +2 -0
  107. package/dist/sdk-limits.js +3 -0
  108. package/dist/sdk-limits.js.map +1 -0
  109. package/dist/sdk-node-file-reader.d.ts +6 -0
  110. package/dist/sdk-node-file-reader.js +38 -0
  111. package/dist/sdk-node-file-reader.js.map +1 -0
  112. package/dist/sdk-pro-catalog.d.ts +22 -0
  113. package/dist/sdk-pro-catalog.js +234 -0
  114. package/dist/sdk-pro-catalog.js.map +1 -0
  115. package/dist/sdk-pro-runtime.d.ts +31 -0
  116. package/dist/sdk-pro-runtime.js +293 -0
  117. package/dist/sdk-pro-runtime.js.map +1 -0
  118. package/dist/sdk-query.d.ts +10 -0
  119. package/dist/sdk-query.js +29 -0
  120. package/dist/sdk-query.js.map +1 -0
  121. package/dist/sdk-runtime.d.ts +15 -0
  122. package/dist/sdk-runtime.js +685 -0
  123. package/dist/sdk-runtime.js.map +1 -0
  124. package/dist/sdk-worker.d.ts +1 -0
  125. package/dist/sdk-worker.js +26 -0
  126. package/dist/sdk-worker.js.map +1 -0
  127. package/dist/sdk.d.ts +29 -0
  128. package/dist/sdk.js +346 -0
  129. package/dist/sdk.js.map +1 -0
  130. package/dist/search-engine.d.ts +17 -0
  131. package/dist/search-engine.js +54 -0
  132. package/dist/search-engine.js.map +1 -0
  133. package/dist/search-intents.d.ts +2 -0
  134. package/dist/search-intents.js +98 -0
  135. package/dist/search-intents.js.map +1 -0
  136. package/dist/search.d.ts +7 -0
  137. package/dist/search.js +58 -0
  138. package/dist/search.js.map +1 -0
  139. package/dist/secret-output.d.ts +2 -0
  140. package/dist/secret-output.js +108 -0
  141. package/dist/secret-output.js.map +1 -0
  142. package/dist/shell.d.ts +3 -0
  143. package/dist/shell.js +14 -0
  144. package/dist/shell.js.map +1 -0
  145. package/dist/skills.d.ts +210 -0
  146. package/dist/skills.js +374 -0
  147. package/dist/skills.js.map +1 -0
  148. package/dist/value-semantics.d.ts +70 -0
  149. package/dist/value-semantics.js +173 -0
  150. package/dist/value-semantics.js.map +1 -0
  151. package/dist/version.d.ts +1 -0
  152. package/dist/version.js +3 -0
  153. package/dist/version.js.map +1 -0
  154. package/dist/windows.d.ts +4 -0
  155. package/dist/windows.js +136 -0
  156. package/dist/windows.js.map +1 -0
  157. package/dist/workflows.d.ts +228 -0
  158. package/dist/workflows.js +1207 -0
  159. package/dist/workflows.js.map +1 -0
  160. package/package.json +86 -4
  161. package/plugin.json +22 -0
  162. package/references/agent-guide.json +196 -0
  163. package/references/commands.md +247 -0
  164. package/references/dashboard-inventory.json +14796 -0
  165. package/references/docs-index.json +2709 -0
  166. package/references/sdk-commands.json +15065 -0
  167. package/references/workflows.json +1246 -0
  168. package/skills/sendbird/SKILL.md +96 -0
  169. package/skills/sendbird/agents/openai.yaml +4 -0
  170. package/skills/sendbird/references/build-chat-app.md +49 -0
  171. package/skills/sendbird/references/client-guide.md +34 -0
  172. package/skills/sendbird/references/client-plugin-bridge.md +77 -0
  173. package/skills/sendbird/references/client-plugin-install.md +39 -0
  174. package/skills/sendbird/references/operational-checks.md +35 -0
  175. package/skills/sendbird/references/pro-capabilities.md +52 -0
  176. package/skills/sendbird/scripts/client-guide.mjs +283 -0
  177. package/skills/sendbird/scripts/client-plugin-bridge.mjs +703 -0
  178. package/skills/sendbird-setup/SKILL.md +18 -0
  179. package/skills/sendbird-setup/agents/openai.yaml +4 -0
  180. package/skills/sendbird-setup/references/onboarding.md +73 -0
  181. package/skills/sendbird-setup/scripts/install-cli.mjs +447 -0
  182. 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