@sentry/junior 0.5.0 → 0.7.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.
@@ -0,0 +1,522 @@
1
+ // src/chat/plugins/manifest.ts
2
+ import { z } from "zod";
3
+ import { parse as parseYaml } from "yaml";
4
+ var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
5
+ var SHORT_CAPABILITY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
6
+ var SHORT_CONFIG_KEY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
7
+ var AUTH_TOKEN_ENV_RE = /^[A-Z][A-Z0-9_]*$/;
8
+ var API_DOMAIN_RE = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
9
+ var RUNTIME_POSTINSTALL_CMD_RE = /^[A-Za-z0-9._/-]+$/;
10
+ var RESERVED_AUTHORIZE_PARAM_KEYS = /* @__PURE__ */ new Set([
11
+ "client_id",
12
+ "scope",
13
+ "state",
14
+ "redirect_uri",
15
+ "response_type"
16
+ ]);
17
+ var FORBIDDEN_API_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
18
+ var FORBIDDEN_TOKEN_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
19
+ var trimmedString = z.string().transform((value) => value.trim());
20
+ var nonEmptyTrimmedString = trimmedString.pipe(
21
+ z.string().min(1, { error: "must be a non-empty string" })
22
+ );
23
+ var envVarString = nonEmptyTrimmedString.refine(
24
+ (value) => AUTH_TOKEN_ENV_RE.test(value),
25
+ {
26
+ error: "must be an uppercase env var name"
27
+ }
28
+ );
29
+ var httpsUrlString = nonEmptyTrimmedString.superRefine((value, ctx) => {
30
+ let parsed;
31
+ try {
32
+ parsed = new URL(value);
33
+ } catch {
34
+ ctx.addIssue({
35
+ code: z.ZodIssueCode.custom,
36
+ message: "must be a valid URL"
37
+ });
38
+ return;
39
+ }
40
+ if (parsed.protocol !== "https:") {
41
+ ctx.addIssue({
42
+ code: z.ZodIssueCode.custom,
43
+ message: "must use https"
44
+ });
45
+ }
46
+ });
47
+ var stringMapSchema = z.record(z.string(), z.unknown()).transform((record, ctx) => {
48
+ const entries = Object.entries(record);
49
+ const result = {};
50
+ const seen = /* @__PURE__ */ new Set();
51
+ for (const [rawKey, rawValue] of entries) {
52
+ const key = rawKey.trim();
53
+ if (!key) {
54
+ ctx.addIssue({
55
+ code: z.ZodIssueCode.custom,
56
+ message: "keys must be non-empty strings"
57
+ });
58
+ return z.NEVER;
59
+ }
60
+ if (typeof rawValue !== "string" || !rawValue.trim()) {
61
+ ctx.addIssue({
62
+ code: z.ZodIssueCode.custom,
63
+ message: `${key} must be a non-empty string`
64
+ });
65
+ return z.NEVER;
66
+ }
67
+ const normalizedKey = key.toLowerCase();
68
+ if (seen.has(normalizedKey)) {
69
+ ctx.addIssue({
70
+ code: z.ZodIssueCode.custom,
71
+ message: `${key} is duplicated`
72
+ });
73
+ return z.NEVER;
74
+ }
75
+ seen.add(normalizedKey);
76
+ result[key] = rawValue.trim();
77
+ }
78
+ return Object.keys(result).length > 0 ? result : void 0;
79
+ });
80
+ var apiDomainsSchema = z.array(z.unknown()).min(1, {
81
+ error: "must be a non-empty array of strings"
82
+ }).transform((domains, ctx) => {
83
+ return domains.map((rawDomain) => {
84
+ const domain = typeof rawDomain === "string" ? rawDomain.trim().toLowerCase() : "";
85
+ if (!domain) {
86
+ ctx.addIssue({
87
+ code: z.ZodIssueCode.custom,
88
+ message: "entries must be non-empty strings"
89
+ });
90
+ return z.NEVER;
91
+ }
92
+ if (!API_DOMAIN_RE.test(domain)) {
93
+ ctx.addIssue({
94
+ code: z.ZodIssueCode.custom,
95
+ message: "entries must be valid domain names"
96
+ });
97
+ return z.NEVER;
98
+ }
99
+ return domain;
100
+ });
101
+ });
102
+ var baseCredentialsSchema = z.object({
103
+ "api-domains": apiDomainsSchema,
104
+ "api-headers": stringMapSchema.optional(),
105
+ "auth-token-env": envVarString,
106
+ "auth-token-placeholder": nonEmptyTrimmedString.optional()
107
+ }).passthrough();
108
+ var oauthBearerCredentialsSchema = baseCredentialsSchema.extend({
109
+ type: z.literal("oauth-bearer")
110
+ });
111
+ var githubAppCredentialsSchema = baseCredentialsSchema.extend({
112
+ type: z.literal("github-app"),
113
+ "app-id-env": envVarString,
114
+ "private-key-env": envVarString,
115
+ "installation-id-env": envVarString
116
+ });
117
+ var runtimeDependencyEntrySchema = z.object({
118
+ type: z.enum(["npm", "system"]),
119
+ package: z.string().optional(),
120
+ version: z.string().optional(),
121
+ url: z.string().optional(),
122
+ sha256: z.string().optional()
123
+ }).passthrough();
124
+ var runtimePostinstallCommandSourceSchema = z.object({
125
+ cmd: nonEmptyTrimmedString,
126
+ args: z.array(z.string(), {
127
+ error: "args must be an array of strings when provided"
128
+ }).optional(),
129
+ sudo: z.boolean({
130
+ error: "sudo must be a boolean when provided"
131
+ }).optional()
132
+ }).passthrough();
133
+ var oauthSourceSchema = z.object({
134
+ "client-id-env": envVarString,
135
+ "client-secret-env": envVarString,
136
+ "authorize-endpoint": httpsUrlString,
137
+ "token-endpoint": httpsUrlString,
138
+ scope: nonEmptyTrimmedString.optional(),
139
+ "authorize-params": stringMapSchema.optional(),
140
+ "token-extra-headers": stringMapSchema.optional(),
141
+ "token-auth-method": nonEmptyTrimmedString.refine((value) => value === "body" || value === "basic", {
142
+ error: 'must be "body" or "basic"'
143
+ }).optional()
144
+ }).passthrough();
145
+ var targetSourceSchema = z.object({
146
+ type: z.literal("repo", {
147
+ error: 'type must be "repo"'
148
+ }),
149
+ "config-key": nonEmptyTrimmedString
150
+ }).passthrough();
151
+ var manifestSourceSchema = z.object({
152
+ name: z.string().refine((value) => PLUGIN_NAME_RE.test(value), {
153
+ error: "invalid"
154
+ }),
155
+ description: nonEmptyTrimmedString,
156
+ capabilities: z.array(z.string(), {
157
+ error: "must be an array when provided"
158
+ }).optional(),
159
+ "config-keys": z.array(z.string(), {
160
+ error: "must be an array when provided"
161
+ }).optional(),
162
+ credentials: z.record(z.string(), z.unknown(), {
163
+ error: "must be an object when provided"
164
+ }).optional(),
165
+ "runtime-dependencies": z.array(z.unknown(), {
166
+ error: "must be an array"
167
+ }).optional(),
168
+ "runtime-postinstall": z.array(z.unknown(), {
169
+ error: "must be an array"
170
+ }).optional(),
171
+ oauth: z.record(z.string(), z.unknown(), {
172
+ error: "must be an object"
173
+ }).optional(),
174
+ target: z.record(z.string(), z.unknown(), {
175
+ error: "must be an object"
176
+ }).optional()
177
+ }).passthrough();
178
+ function formatPath(path) {
179
+ return path.map((segment) => String(segment)).join(".");
180
+ }
181
+ function issueMessage(error, prefix) {
182
+ const issue = error.issues[0];
183
+ if (!issue) {
184
+ return prefix;
185
+ }
186
+ const suffix = formatPath(issue.path);
187
+ return suffix ? `${prefix}.${suffix} ${issue.message}` : `${prefix} ${issue.message}`;
188
+ }
189
+ function normalizeStringMap(value, prefix, options = {}) {
190
+ if (!value) {
191
+ return void 0;
192
+ }
193
+ for (const key of Object.keys(value)) {
194
+ const normalizedKey = key.toLowerCase();
195
+ if (options.reservedKeys?.has(normalizedKey)) {
196
+ throw new Error(`${prefix}.${key} is reserved by the runtime`);
197
+ }
198
+ if (options.forbiddenKeys?.has(normalizedKey)) {
199
+ throw new Error(`${prefix}.${key} is not allowed`);
200
+ }
201
+ }
202
+ return value;
203
+ }
204
+ function normalizeCredentials(data, name) {
205
+ const schema = data.type === "oauth-bearer" ? oauthBearerCredentialsSchema : data.type === "github-app" ? githubAppCredentialsSchema : void 0;
206
+ if (!schema) {
207
+ throw new Error(
208
+ `Plugin ${name} has unsupported credentials.type: "${String(data.type)}"`
209
+ );
210
+ }
211
+ const result = schema.safeParse(data);
212
+ if (!result.success) {
213
+ throw new Error(issueMessage(result.error, `Plugin ${name} credentials`));
214
+ }
215
+ if (result.data.type === "oauth-bearer") {
216
+ return {
217
+ type: "oauth-bearer",
218
+ apiDomains: result.data["api-domains"],
219
+ ...result.data["api-headers"] ? {
220
+ apiHeaders: normalizeStringMap(
221
+ result.data["api-headers"],
222
+ `Plugin ${name} credentials.api-headers`,
223
+ { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
224
+ )
225
+ } : {},
226
+ authTokenEnv: result.data["auth-token-env"],
227
+ ...result.data["auth-token-placeholder"] ? { authTokenPlaceholder: result.data["auth-token-placeholder"] } : {}
228
+ };
229
+ }
230
+ return {
231
+ type: "github-app",
232
+ apiDomains: result.data["api-domains"],
233
+ ...result.data["api-headers"] ? {
234
+ apiHeaders: normalizeStringMap(
235
+ result.data["api-headers"],
236
+ `Plugin ${name} credentials.api-headers`,
237
+ { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
238
+ )
239
+ } : {},
240
+ authTokenEnv: result.data["auth-token-env"],
241
+ ...result.data["auth-token-placeholder"] ? { authTokenPlaceholder: result.data["auth-token-placeholder"] } : {},
242
+ appIdEnv: result.data["app-id-env"],
243
+ privateKeyEnv: result.data["private-key-env"],
244
+ installationIdEnv: result.data["installation-id-env"]
245
+ };
246
+ }
247
+ function normalizeRuntimeDependencies(entries, name) {
248
+ const parsed = [];
249
+ const seen = /* @__PURE__ */ new Set();
250
+ for (const entry of entries) {
251
+ const result = runtimeDependencyEntrySchema.safeParse(entry);
252
+ if (!result.success) {
253
+ throw new Error(
254
+ issueMessage(
255
+ result.error,
256
+ `Plugin ${name} runtime-dependencies entries`
257
+ )
258
+ );
259
+ }
260
+ const record = result.data;
261
+ const packageName = typeof record.package === "string" ? record.package.trim() : "";
262
+ const packageUrl = typeof record.url === "string" ? record.url.trim() : "";
263
+ const version = record.version;
264
+ const sha256 = record.sha256;
265
+ if (record.type === "npm") {
266
+ if (!packageName) {
267
+ throw new Error(
268
+ `Plugin ${name} runtime dependency package must be a non-empty string`
269
+ );
270
+ }
271
+ if (record.url !== void 0 || sha256 !== void 0) {
272
+ throw new Error(
273
+ `Plugin ${name} npm runtime dependencies must only include package/version fields`
274
+ );
275
+ }
276
+ const normalizedVersion = typeof version === "string" ? version.trim() : "latest";
277
+ if (!normalizedVersion) {
278
+ throw new Error(
279
+ `Plugin ${name} runtime dependency version must be a non-empty string when provided`
280
+ );
281
+ }
282
+ const dedupeKey2 = `${record.type}:${packageName}:${normalizedVersion}`;
283
+ if (seen.has(dedupeKey2)) {
284
+ continue;
285
+ }
286
+ seen.add(dedupeKey2);
287
+ parsed.push({
288
+ type: "npm",
289
+ package: packageName,
290
+ version: normalizedVersion
291
+ });
292
+ continue;
293
+ }
294
+ if (version !== void 0) {
295
+ throw new Error(
296
+ `Plugin ${name} system runtime dependencies must not include a version`
297
+ );
298
+ }
299
+ if (packageName && packageUrl) {
300
+ throw new Error(
301
+ `Plugin ${name} system runtime dependencies must specify either package or url, not both`
302
+ );
303
+ }
304
+ if (!packageName && !packageUrl) {
305
+ throw new Error(
306
+ `Plugin ${name} system runtime dependencies must specify package or url`
307
+ );
308
+ }
309
+ if (packageName) {
310
+ if (sha256 !== void 0) {
311
+ throw new Error(
312
+ `Plugin ${name} system runtime dependency package entries must not include sha256`
313
+ );
314
+ }
315
+ const dedupeKey2 = `${record.type}:package:${packageName}`;
316
+ if (seen.has(dedupeKey2)) {
317
+ continue;
318
+ }
319
+ seen.add(dedupeKey2);
320
+ parsed.push({
321
+ type: "system",
322
+ package: packageName
323
+ });
324
+ continue;
325
+ }
326
+ if (!/^https:\/\//i.test(packageUrl)) {
327
+ throw new Error(
328
+ `Plugin ${name} system runtime dependency url must be an https URL`
329
+ );
330
+ }
331
+ const normalizedSha256 = typeof sha256 === "string" ? sha256.trim().toLowerCase() : "";
332
+ if (!/^[a-f0-9]{64}$/.test(normalizedSha256)) {
333
+ throw new Error(
334
+ `Plugin ${name} system runtime dependency url entries must include a valid sha256`
335
+ );
336
+ }
337
+ const dedupeKey = `${record.type}:url:${packageUrl}:${normalizedSha256}`;
338
+ if (seen.has(dedupeKey)) {
339
+ continue;
340
+ }
341
+ seen.add(dedupeKey);
342
+ parsed.push({
343
+ type: "system",
344
+ url: packageUrl,
345
+ sha256: normalizedSha256
346
+ });
347
+ }
348
+ return parsed.length > 0 ? parsed : void 0;
349
+ }
350
+ function normalizeRuntimePostinstall(commands, name) {
351
+ const parsed = [];
352
+ for (const command of commands) {
353
+ const result = runtimePostinstallCommandSourceSchema.safeParse(command);
354
+ if (!result.success) {
355
+ throw new Error(
356
+ issueMessage(result.error, `Plugin ${name} runtime-postinstall`)
357
+ );
358
+ }
359
+ if (!RUNTIME_POSTINSTALL_CMD_RE.test(result.data.cmd)) {
360
+ throw new Error(
361
+ `Plugin ${name} runtime-postinstall cmd must be a single executable token (letters, digits, ., _, /, -)`
362
+ );
363
+ }
364
+ const normalizedArgs = result.data.args?.map((arg) => arg.trim()).filter((arg) => arg.length > 0);
365
+ parsed.push({
366
+ cmd: result.data.cmd,
367
+ ...normalizedArgs && normalizedArgs.length > 0 ? { args: normalizedArgs } : {},
368
+ ...typeof result.data.sudo === "boolean" ? { sudo: result.data.sudo } : {}
369
+ });
370
+ }
371
+ return parsed.length > 0 ? parsed : void 0;
372
+ }
373
+ function parsePluginManifest(raw, dir) {
374
+ let parsedYaml;
375
+ try {
376
+ parsedYaml = parseYaml(raw);
377
+ } catch (error) {
378
+ throw new Error(
379
+ `Invalid plugin manifest in ${dir}: ${error instanceof Error ? error.message : String(error)}`
380
+ );
381
+ }
382
+ if (!parsedYaml || typeof parsedYaml !== "object" || Array.isArray(parsedYaml)) {
383
+ throw new Error(`Invalid plugin manifest in ${dir}: expected an object`);
384
+ }
385
+ const sourceResult = manifestSourceSchema.safeParse(parsedYaml);
386
+ if (!sourceResult.success) {
387
+ const issue = sourceResult.error.issues[0];
388
+ const path = formatPath(issue?.path ?? []);
389
+ if (path === "name") {
390
+ throw new Error(
391
+ `Invalid plugin name in ${dir}: "${parsedYaml.name}"`
392
+ );
393
+ }
394
+ if (path === "description") {
395
+ throw new Error(`Invalid plugin description in ${dir}`);
396
+ }
397
+ if (path === "capabilities") {
398
+ throw new Error(
399
+ `Plugin ${parsedYaml.name ?? "unknown"} capabilities must be an array when provided`
400
+ );
401
+ }
402
+ if (path === "config-keys") {
403
+ throw new Error(
404
+ `Plugin ${parsedYaml.name ?? "unknown"} config-keys must be an array when provided`
405
+ );
406
+ }
407
+ if (path === "credentials") {
408
+ throw new Error(
409
+ `Plugin ${parsedYaml.name ?? "unknown"} credentials must be an object when provided`
410
+ );
411
+ }
412
+ if (path === "runtime-dependencies") {
413
+ throw new Error(
414
+ `Plugin ${parsedYaml.name ?? "unknown"} runtime-dependencies must be an array`
415
+ );
416
+ }
417
+ if (path === "runtime-postinstall") {
418
+ throw new Error(
419
+ `Plugin ${parsedYaml.name ?? "unknown"} runtime-postinstall must be an array`
420
+ );
421
+ }
422
+ if (path === "oauth") {
423
+ throw new Error(
424
+ `Plugin ${parsedYaml.name ?? "unknown"} oauth must be an object`
425
+ );
426
+ }
427
+ if (path === "target") {
428
+ throw new Error(
429
+ `Plugin ${parsedYaml.name ?? "unknown"} target must be an object`
430
+ );
431
+ }
432
+ throw new Error(issue?.message ?? `Invalid plugin manifest in ${dir}`);
433
+ }
434
+ const data = sourceResult.data;
435
+ const capabilities = (data.capabilities ?? []).map((cap) => {
436
+ if (!SHORT_CAPABILITY_RE.test(cap)) {
437
+ throw new Error(
438
+ `Invalid capability token "${cap}" in plugin ${data.name}`
439
+ );
440
+ }
441
+ return `${data.name}.${cap}`;
442
+ });
443
+ const configKeys = (data["config-keys"] ?? []).map((key) => {
444
+ if (!SHORT_CONFIG_KEY_RE.test(key)) {
445
+ throw new Error(`Invalid config key "${key}" in plugin ${data.name}`);
446
+ }
447
+ return `${data.name}.${key}`;
448
+ });
449
+ const credentials = data.credentials ? normalizeCredentials(data.credentials, data.name) : void 0;
450
+ const runtimeDependencies = data["runtime-dependencies"] ? normalizeRuntimeDependencies(data["runtime-dependencies"], data.name) : void 0;
451
+ const runtimePostinstall = data["runtime-postinstall"] ? normalizeRuntimePostinstall(data["runtime-postinstall"], data.name) : void 0;
452
+ const manifest = {
453
+ name: data.name,
454
+ description: data.description,
455
+ capabilities,
456
+ configKeys,
457
+ ...credentials ? { credentials } : {},
458
+ ...runtimeDependencies ? { runtimeDependencies } : {},
459
+ ...runtimePostinstall ? { runtimePostinstall } : {}
460
+ };
461
+ if (data.oauth) {
462
+ if (!credentials) {
463
+ throw new Error(`Plugin ${data.name} oauth requires credentials`);
464
+ }
465
+ if (credentials.type !== "oauth-bearer") {
466
+ throw new Error(
467
+ `Plugin ${data.name} oauth requires credentials.type "oauth-bearer"`
468
+ );
469
+ }
470
+ const result = oauthSourceSchema.safeParse(data.oauth);
471
+ if (!result.success) {
472
+ throw new Error(issueMessage(result.error, `Plugin ${data.name} oauth`));
473
+ }
474
+ const authorizeParams = result.data["authorize-params"] ? normalizeStringMap(
475
+ result.data["authorize-params"],
476
+ `Plugin ${data.name} oauth.authorize-params`,
477
+ {
478
+ reservedKeys: RESERVED_AUTHORIZE_PARAM_KEYS
479
+ }
480
+ ) : void 0;
481
+ const tokenExtraHeaders = result.data["token-extra-headers"] ? normalizeStringMap(
482
+ result.data["token-extra-headers"],
483
+ `Plugin ${data.name} oauth.token-extra-headers`,
484
+ {
485
+ forbiddenKeys: FORBIDDEN_TOKEN_HEADER_NAMES
486
+ }
487
+ ) : void 0;
488
+ manifest.oauth = {
489
+ clientIdEnv: result.data["client-id-env"],
490
+ clientSecretEnv: result.data["client-secret-env"],
491
+ authorizeEndpoint: result.data["authorize-endpoint"],
492
+ tokenEndpoint: result.data["token-endpoint"],
493
+ ...result.data.scope ? { scope: result.data.scope } : {},
494
+ ...authorizeParams ? { authorizeParams } : {},
495
+ ...result.data["token-auth-method"] ? { tokenAuthMethod: result.data["token-auth-method"] } : {},
496
+ ...tokenExtraHeaders ? { tokenExtraHeaders } : {}
497
+ };
498
+ }
499
+ if (data.target) {
500
+ const result = targetSourceSchema.safeParse(data.target);
501
+ if (!result.success) {
502
+ throw new Error(issueMessage(result.error, `Plugin ${data.name} target`));
503
+ }
504
+ if (!SHORT_CONFIG_KEY_RE.test(result.data["config-key"])) {
505
+ throw new Error(
506
+ `Plugin ${data.name} target.config-key "${result.data["config-key"]}" is invalid`
507
+ );
508
+ }
509
+ const qualifiedKey = `${data.name}.${result.data["config-key"]}`;
510
+ if (!configKeys.includes(qualifiedKey)) {
511
+ throw new Error(
512
+ `Plugin ${data.name} target.config-key "${result.data["config-key"]}" must be listed in config-keys`
513
+ );
514
+ }
515
+ manifest.target = { type: "repo", configKey: qualifiedKey };
516
+ }
517
+ return manifest;
518
+ }
519
+
520
+ export {
521
+ parsePluginManifest
522
+ };
@@ -0,0 +1,8 @@
1
+ interface ValidationIo {
2
+ info: (line: string) => void;
3
+ warn: (line: string) => void;
4
+ error: (line: string) => void;
5
+ }
6
+ declare function runCheck(rootDir?: string, io?: ValidationIo): Promise<void>;
7
+
8
+ export { type ValidationIo, runCheck };