@axtary/adapters 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,1768 @@
1
+ import { verifyActionPass, } from "@axtary/actionpass";
2
+ import { createHash, createHmac } from "node:crypto";
3
+ import { readdir, readFile, stat } from "node:fs/promises";
4
+ import { basename, extname, isAbsolute, relative, resolve, sep } from "node:path";
5
+ export const GITHUB_REST_API_BASE_URL = "https://api.github.com";
6
+ export const GITHUB_REST_USER_AGENT = "axtary-local-proxy";
7
+ export const SLACK_WEB_API_BASE_URL = "https://slack.com/api";
8
+ export const LINEAR_GRAPHQL_API_URL = "https://api.linear.app/graphql";
9
+ export const AWS_STS_API_URL = "https://sts.amazonaws.com/";
10
+ export const GCP_CLOUD_RESOURCE_MANAGER_API_BASE_URL = "https://cloudresourcemanager.googleapis.com";
11
+ export const GCP_STORAGE_API_BASE_URL = "https://storage.googleapis.com/storage/v1";
12
+ export const DEFAULT_PROVIDER_RETRIES = 2;
13
+ export const CONNECTOR_CAPABILITY_REGISTRY = [
14
+ {
15
+ id: "github-pr-create",
16
+ provider: "github",
17
+ connector: "github-rest",
18
+ label: "Create pull request",
19
+ tool: "github.pull_requests.create",
20
+ operation: "write",
21
+ status: "supported",
22
+ supportedModes: ["fake", "rest"],
23
+ requiresActionPass: true,
24
+ defaultPolicy: "step_up",
25
+ payloadFields: ["title", "baseBranch", "filesChanged", "testsPassed", "touchesProduction"],
26
+ approvalTriggers: [
27
+ "protected path",
28
+ "production impact",
29
+ "base branch mismatch",
30
+ "missing tests",
31
+ "large file set",
32
+ ],
33
+ credentialBoundary: "GitHub token stays in the proxy/adapter; the agent receives an ActionPass-bound result.",
34
+ smokeCheck: "GET /user",
35
+ },
36
+ {
37
+ id: "github-content-read",
38
+ provider: "github",
39
+ connector: "github-rest",
40
+ label: "Read repository content",
41
+ tool: "github.contents.read",
42
+ operation: "read",
43
+ status: "supported",
44
+ supportedModes: ["fake", "rest"],
45
+ requiresActionPass: true,
46
+ defaultPolicy: "deny_until_scoped",
47
+ payloadFields: ["path", "ref"],
48
+ approvalTriggers: ["denied secret or production path"],
49
+ credentialBoundary: "Repository content is fetched by the adapter after policy checks deny protected paths.",
50
+ smokeCheck: "GET /user",
51
+ },
52
+ {
53
+ id: "github-branch-create",
54
+ provider: "github",
55
+ connector: "github-rest",
56
+ label: "Create branch",
57
+ tool: "github.branches.create",
58
+ operation: "write",
59
+ status: "supported",
60
+ supportedModes: ["fake", "rest"],
61
+ requiresActionPass: true,
62
+ defaultPolicy: "allow",
63
+ payloadFields: ["branch", "baseBranch", "sha", "allowExisting"],
64
+ approvalTriggers: ["base branch mismatch"],
65
+ credentialBoundary: "Branch creation runs through the proxy and is bound to the same ActionPass verification path.",
66
+ smokeCheck: "GET /user",
67
+ },
68
+ {
69
+ id: "github-content-write",
70
+ provider: "github",
71
+ connector: "github-rest",
72
+ label: "Write repository content",
73
+ tool: "github.contents.write",
74
+ operation: "write",
75
+ status: "supported",
76
+ supportedModes: ["fake", "rest"],
77
+ requiresActionPass: true,
78
+ defaultPolicy: "step_up",
79
+ payloadFields: ["path", "branch", "message", "sha", "content"],
80
+ approvalTriggers: ["protected path"],
81
+ credentialBoundary: "File content is never written until policy and ActionPass verification both pass.",
82
+ smokeCheck: "GET /user",
83
+ },
84
+ {
85
+ id: "slack-message-create",
86
+ provider: "slack",
87
+ connector: "slack-web",
88
+ label: "Post message",
89
+ tool: "slack.chat.postMessage",
90
+ operation: "external_message",
91
+ status: "supported",
92
+ supportedModes: ["fake", "web"],
93
+ requiresActionPass: true,
94
+ defaultPolicy: "step_up",
95
+ payloadFields: ["channel", "message", "text", "externalRecipients"],
96
+ approvalTriggers: ["external recipient"],
97
+ credentialBoundary: "Slack bot token stays adapter-side; exact outbound text is the approval payload.",
98
+ smokeCheck: "auth.test",
99
+ },
100
+ {
101
+ id: "linear-issue-read",
102
+ provider: "linear",
103
+ connector: "linear-graphql",
104
+ label: "Read issue",
105
+ tool: "linear.issues.read",
106
+ operation: "read",
107
+ status: "supported",
108
+ supportedModes: ["fake", "graphql"],
109
+ requiresActionPass: true,
110
+ defaultPolicy: "allow",
111
+ payloadFields: ["issueKey", "projectKey"],
112
+ approvalTriggers: [],
113
+ credentialBoundary: "Linear API key stays adapter-side; policy pins issue access by project key.",
114
+ smokeCheck: "viewer",
115
+ },
116
+ {
117
+ id: "linear-comment-create",
118
+ provider: "linear",
119
+ connector: "linear-graphql",
120
+ label: "Create issue comment",
121
+ tool: "linear.comments.create",
122
+ operation: "write",
123
+ status: "supported",
124
+ supportedModes: ["fake", "graphql"],
125
+ requiresActionPass: true,
126
+ defaultPolicy: "allow",
127
+ payloadFields: ["issueKey", "projectKey", "body"],
128
+ approvalTriggers: [],
129
+ credentialBoundary: "Linear comments execute only through proxy policy and ActionPass verification.",
130
+ smokeCheck: "viewer",
131
+ },
132
+ {
133
+ id: "linear-issue-update",
134
+ provider: "linear",
135
+ connector: "linear-graphql",
136
+ label: "Update issue",
137
+ tool: "linear.issues.update",
138
+ operation: "write",
139
+ status: "supported",
140
+ supportedModes: ["fake", "graphql"],
141
+ requiresActionPass: true,
142
+ defaultPolicy: "step_up",
143
+ payloadFields: ["issueKey", "projectKey", "status", "title", "description", "priority"],
144
+ approvalTriggers: ["protected status"],
145
+ credentialBoundary: "Issue mutations stay behind adapter credentials and project/status policy checks.",
146
+ smokeCheck: "viewer",
147
+ },
148
+ {
149
+ id: "jira-issue-read",
150
+ provider: "jira",
151
+ connector: "jira-fake",
152
+ label: "Read issue",
153
+ tool: "jira.issues.read",
154
+ operation: "read",
155
+ status: "supported",
156
+ supportedModes: ["fake"],
157
+ requiresActionPass: true,
158
+ defaultPolicy: "allow",
159
+ payloadFields: ["issueKey", "projectKey"],
160
+ approvalTriggers: [],
161
+ credentialBoundary: "Jira is currently fake/local in this repo; future credentials must remain adapter-side.",
162
+ smokeCheck: null,
163
+ },
164
+ {
165
+ id: "jira-comment-create",
166
+ provider: "jira",
167
+ connector: "jira-fake",
168
+ label: "Create issue comment",
169
+ tool: "jira.comments.create",
170
+ operation: "write",
171
+ status: "supported",
172
+ supportedModes: ["fake"],
173
+ requiresActionPass: true,
174
+ defaultPolicy: "allow",
175
+ payloadFields: ["issueKey", "projectKey", "body"],
176
+ approvalTriggers: [],
177
+ credentialBoundary: "Jira is currently fake/local in this repo; future credentials must remain adapter-side.",
178
+ smokeCheck: null,
179
+ },
180
+ {
181
+ id: "jira-issue-update",
182
+ provider: "jira",
183
+ connector: "jira-fake",
184
+ label: "Update issue",
185
+ tool: "jira.issues.update",
186
+ operation: "write",
187
+ status: "supported",
188
+ supportedModes: ["fake"],
189
+ requiresActionPass: true,
190
+ defaultPolicy: "step_up",
191
+ payloadFields: ["issueKey", "projectKey", "status"],
192
+ approvalTriggers: ["protected status"],
193
+ credentialBoundary: "Jira is currently fake/local in this repo; future credentials must remain adapter-side.",
194
+ smokeCheck: null,
195
+ },
196
+ {
197
+ id: "aws-identity-get",
198
+ provider: "aws",
199
+ connector: "aws-rest",
200
+ label: "Get caller identity",
201
+ tool: "aws.identity.get",
202
+ operation: "identity_read",
203
+ status: "supported",
204
+ supportedModes: ["rest"],
205
+ requiresActionPass: true,
206
+ defaultPolicy: "allow",
207
+ payloadFields: [],
208
+ approvalTriggers: [],
209
+ credentialBoundary: "AWS credentials are read from local env/config by the adapter; prefer temporary credentials.",
210
+ smokeCheck: "STS GetCallerIdentity",
211
+ },
212
+ {
213
+ id: "aws-s3-list",
214
+ provider: "aws",
215
+ connector: "aws-rest",
216
+ label: "List S3 objects",
217
+ tool: "aws.s3.objects.list",
218
+ operation: "read",
219
+ status: "supported",
220
+ supportedModes: ["rest"],
221
+ requiresActionPass: true,
222
+ defaultPolicy: "deny_until_scoped",
223
+ payloadFields: ["accountId", "region", "bucket", "prefix", "maxKeys"],
224
+ approvalTriggers: ["unscoped bucket", "denied prefix", "region mismatch"],
225
+ credentialBoundary: "S3 reads run adapter-side with scoped bucket/prefix policy and bounded result limits.",
226
+ smokeCheck: "STS GetCallerIdentity",
227
+ },
228
+ {
229
+ id: "gcp-project-get",
230
+ provider: "gcp",
231
+ connector: "gcp-rest",
232
+ label: "Get project",
233
+ tool: "gcp.projects.get",
234
+ operation: "identity_read",
235
+ status: "supported",
236
+ supportedModes: ["rest"],
237
+ requiresActionPass: true,
238
+ defaultPolicy: "deny_until_scoped",
239
+ payloadFields: ["projectId"],
240
+ approvalTriggers: ["unscoped project"],
241
+ credentialBoundary: "GCP access token stays adapter-side; prefer workload identity or impersonation later.",
242
+ smokeCheck: "Cloud Resource Manager project get",
243
+ },
244
+ {
245
+ id: "gcp-storage-list",
246
+ provider: "gcp",
247
+ connector: "gcp-rest",
248
+ label: "List storage objects",
249
+ tool: "gcp.storage.objects.list",
250
+ operation: "read",
251
+ status: "supported",
252
+ supportedModes: ["rest"],
253
+ requiresActionPass: true,
254
+ defaultPolicy: "deny_until_scoped",
255
+ payloadFields: ["bucket", "prefix", "maxResults"],
256
+ approvalTriggers: ["unscoped bucket", "denied prefix"],
257
+ credentialBoundary: "Storage reads run adapter-side with scoped bucket/prefix policy and bounded result limits.",
258
+ smokeCheck: "Cloud Resource Manager project get",
259
+ },
260
+ {
261
+ id: "docs-document-search",
262
+ provider: "docs",
263
+ connector: "local-docs",
264
+ label: "Search local documents",
265
+ tool: "docs.documents.search",
266
+ operation: "read",
267
+ status: "supported",
268
+ supportedModes: ["local"],
269
+ requiresActionPass: true,
270
+ defaultPolicy: "deny_until_scoped",
271
+ payloadFields: ["workspace", "query", "maxResults"],
272
+ approvalTriggers: ["workspace mismatch", "result limit"],
273
+ credentialBoundary: "Local document roots stay adapter-side; returned snippets are bounded and redacted.",
274
+ smokeCheck: "configured roots exist",
275
+ },
276
+ {
277
+ id: "docs-document-read",
278
+ provider: "docs",
279
+ connector: "local-docs",
280
+ label: "Read local document",
281
+ tool: "docs.documents.read",
282
+ operation: "read",
283
+ status: "supported",
284
+ supportedModes: ["local"],
285
+ requiresActionPass: true,
286
+ defaultPolicy: "deny_until_scoped",
287
+ payloadFields: ["workspace", "path", "maxBytes"],
288
+ approvalTriggers: ["workspace mismatch", "denied path", "byte limit"],
289
+ credentialBoundary: "Document reads are resolved under configured roots and redacted before returning to the agent.",
290
+ smokeCheck: "configured roots exist",
291
+ },
292
+ {
293
+ id: "mcp-tool-call",
294
+ provider: "mcp",
295
+ connector: "mcp-wrapper",
296
+ label: "Call MCP tool",
297
+ tool: "mcp.tool.call",
298
+ operation: "write",
299
+ status: "supported",
300
+ supportedModes: ["local-wrapper"],
301
+ requiresActionPass: true,
302
+ defaultPolicy: "deny_until_scoped",
303
+ payloadFields: ["toolName", "arguments"],
304
+ approvalTriggers: ["new tool definition", "definition hash drift"],
305
+ credentialBoundary: "MCP server credentials stay outside the agent; Axtary binds execution to server identity, schema version, and tool definition hash.",
306
+ smokeCheck: null,
307
+ },
308
+ {
309
+ id: "aws-iam-mutation",
310
+ provider: "aws",
311
+ connector: "aws-rest",
312
+ label: "Mutate IAM",
313
+ tool: "aws.iam.*",
314
+ operation: "identity_mutation",
315
+ status: "planned",
316
+ supportedModes: [],
317
+ requiresActionPass: true,
318
+ defaultPolicy: "deny",
319
+ payloadFields: ["accountId", "role", "policyArn", "principal"],
320
+ approvalTriggers: ["always"],
321
+ credentialBoundary: "Not executable yet; future support must use scoped temporary authority and reviewer permissions.",
322
+ smokeCheck: null,
323
+ },
324
+ {
325
+ id: "gcp-iam-mutation",
326
+ provider: "gcp",
327
+ connector: "gcp-rest",
328
+ label: "Mutate IAM",
329
+ tool: "gcp.iam.*",
330
+ operation: "identity_mutation",
331
+ status: "planned",
332
+ supportedModes: [],
333
+ requiresActionPass: true,
334
+ defaultPolicy: "deny",
335
+ payloadFields: ["projectId", "role", "member", "serviceAccount"],
336
+ approvalTriggers: ["always"],
337
+ credentialBoundary: "Not executable yet; future support must use scoped impersonation and reviewer permissions.",
338
+ smokeCheck: null,
339
+ },
340
+ ];
341
+ export function getConnectorCapabilities() {
342
+ return CONNECTOR_CAPABILITY_REGISTRY;
343
+ }
344
+ export function getConnectorCapability(tool) {
345
+ return (CONNECTOR_CAPABILITY_REGISTRY.find((capability) => capability.tool === tool) ??
346
+ null);
347
+ }
348
+ export function getConnectorCapabilitiesByProvider(provider) {
349
+ return CONNECTOR_CAPABILITY_REGISTRY.filter((capability) => capability.provider === provider);
350
+ }
351
+ export function createFakeAdapterState() {
352
+ return {
353
+ github: {
354
+ pullRequests: [],
355
+ contentReads: [],
356
+ contentWrites: [],
357
+ branches: [],
358
+ },
359
+ slack: {
360
+ messages: [],
361
+ },
362
+ issueTrackers: {
363
+ linear: {
364
+ issues: [
365
+ {
366
+ provider: "linear",
367
+ issueKey: "AXT-418",
368
+ projectKey: "AXT",
369
+ title: "Fix auth redirect bug",
370
+ status: "In Progress",
371
+ },
372
+ ],
373
+ comments: [],
374
+ },
375
+ jira: {
376
+ issues: [
377
+ {
378
+ provider: "jira",
379
+ issueKey: "AXT-772",
380
+ projectKey: "AXT",
381
+ title: "Document ActionPass audit flow",
382
+ status: "To Do",
383
+ },
384
+ ],
385
+ comments: [],
386
+ },
387
+ },
388
+ };
389
+ }
390
+ export function createFakeHandlers(state = createFakeAdapterState(), options = {}) {
391
+ const handlers = {
392
+ "github.pull_requests.create": createFakeGitHubPullRequestHandler(state),
393
+ "github.contents.read": createFakeGitHubContentReadHandler(state),
394
+ "github.contents.write": createFakeGitHubContentWriteHandler(state),
395
+ "github.branches.create": createFakeGitHubBranchCreateHandler(state),
396
+ "slack.chat.postMessage": createFakeSlackMessageHandler(state),
397
+ "linear.issues.read": createFakeIssueReadHandler(state, "linear"),
398
+ "linear.comments.create": createFakeIssueCommentHandler(state, "linear"),
399
+ "linear.issues.update": createFakeIssueUpdateHandler(state, "linear"),
400
+ "jira.issues.read": createFakeIssueReadHandler(state, "jira"),
401
+ "jira.comments.create": createFakeIssueCommentHandler(state, "jira"),
402
+ "jira.issues.update": createFakeIssueUpdateHandler(state, "jira"),
403
+ };
404
+ return options.verification
405
+ ? wrapHandlersWithActionPassVerification(handlers, options.verification)
406
+ : handlers;
407
+ }
408
+ export function createGitHubRestHandlers(config, options = {}) {
409
+ const handlers = {
410
+ "github.contents.read": createGitHubRestContentReadHandler(config),
411
+ "github.pull_requests.create": createGitHubRestPullRequestHandler(config),
412
+ "github.branches.create": createGitHubRestBranchCreateHandler(config),
413
+ "github.contents.write": createGitHubRestContentWriteHandler(config),
414
+ };
415
+ return options.verification
416
+ ? wrapHandlersWithActionPassVerification(handlers, options.verification)
417
+ : handlers;
418
+ }
419
+ export function createSlackWebHandlers(config, options = {}) {
420
+ const handlers = {
421
+ "slack.chat.postMessage": createSlackWebPostMessageHandler(config),
422
+ };
423
+ return options.verification
424
+ ? wrapHandlersWithActionPassVerification(handlers, options.verification)
425
+ : handlers;
426
+ }
427
+ export function createLinearGraphqlHandlers(config, options = {}) {
428
+ const handlers = {
429
+ "linear.issues.read": createLinearIssueReadHandler(config),
430
+ "linear.comments.create": createLinearCommentCreateHandler(config),
431
+ "linear.issues.update": createLinearIssueUpdateHandler(config),
432
+ };
433
+ return options.verification
434
+ ? wrapHandlersWithActionPassVerification(handlers, options.verification)
435
+ : handlers;
436
+ }
437
+ export function createAwsRestHandlers(config, options = {}) {
438
+ const handlers = {
439
+ "aws.identity.get": createAwsIdentityGetHandler(config),
440
+ "aws.s3.objects.list": createAwsS3ObjectsListHandler(config),
441
+ };
442
+ return options.verification
443
+ ? wrapHandlersWithActionPassVerification(handlers, options.verification)
444
+ : handlers;
445
+ }
446
+ export function createGcpRestHandlers(config, options = {}) {
447
+ const handlers = {
448
+ "gcp.projects.get": createGcpProjectGetHandler(config),
449
+ "gcp.storage.objects.list": createGcpStorageObjectsListHandler(config),
450
+ };
451
+ return options.verification
452
+ ? wrapHandlersWithActionPassVerification(handlers, options.verification)
453
+ : handlers;
454
+ }
455
+ export function createLocalDocsHandlers(config, options = {}) {
456
+ const handlers = {
457
+ "docs.documents.search": createLocalDocsSearchHandler(config),
458
+ "docs.documents.read": createLocalDocsReadHandler(config),
459
+ };
460
+ return options.verification
461
+ ? wrapHandlersWithActionPassVerification(handlers, options.verification)
462
+ : handlers;
463
+ }
464
+ export async function smokeLocalDocsRoots(config) {
465
+ const roots = normalizeDocsRoots(config);
466
+ for (const root of roots) {
467
+ const rootStat = await stat(root.absolutePath);
468
+ if (!rootStat.isDirectory()) {
469
+ throw new Error(`docs_root_not_directory:${root.configuredPath}`);
470
+ }
471
+ }
472
+ return {
473
+ provider: "docs",
474
+ ok: true,
475
+ summary: `${roots.length} root${roots.length === 1 ? "" : "s"} available`,
476
+ details: {
477
+ workspace: config.workspace ?? "local",
478
+ roots: roots.map((root) => root.configuredPath),
479
+ },
480
+ };
481
+ }
482
+ export async function smokeGitHubRestCredentials(config) {
483
+ const client = createGitHubRestClient(config);
484
+ const user = await client.request("/user", { method: "GET" });
485
+ return {
486
+ provider: "github",
487
+ ok: true,
488
+ summary: `logged in as ${user.login}`,
489
+ details: {
490
+ login: user.login,
491
+ id: user.id,
492
+ },
493
+ };
494
+ }
495
+ export async function smokeSlackWebCredentials(config) {
496
+ const client = createSlackWebClient(config);
497
+ const response = await client.request("auth.test", {});
498
+ return {
499
+ provider: "slack",
500
+ ok: true,
501
+ summary: `workspace: ${response.team}, user: ${response.user}`,
502
+ details: {
503
+ team: response.team,
504
+ user: response.user,
505
+ teamId: response.team_id ?? null,
506
+ userId: response.user_id ?? null,
507
+ },
508
+ };
509
+ }
510
+ export async function smokeLinearGraphqlCredentials(config) {
511
+ const client = createLinearGraphqlClient(config);
512
+ const response = await client.request(`
513
+ query AxtarySmokeViewer {
514
+ viewer {
515
+ id
516
+ name
517
+ email
518
+ }
519
+ }
520
+ `, {});
521
+ return {
522
+ provider: "linear",
523
+ ok: true,
524
+ summary: `viewer: ${response.viewer.name}`,
525
+ details: {
526
+ id: response.viewer.id,
527
+ name: response.viewer.name,
528
+ email: response.viewer.email,
529
+ },
530
+ };
531
+ }
532
+ export async function smokeAwsRestCredentials(config) {
533
+ const client = createAwsClient(config);
534
+ const identity = await getAwsCallerIdentity(client);
535
+ return {
536
+ provider: "aws",
537
+ ok: true,
538
+ summary: `account: ${identity.account}`,
539
+ details: {
540
+ account: identity.account,
541
+ arn: identity.arn,
542
+ userId: identity.userId,
543
+ },
544
+ };
545
+ }
546
+ export async function smokeGcpRestCredentials(config) {
547
+ const client = createGcpRestClient(config);
548
+ const projectId = config.projectId;
549
+ if (projectId) {
550
+ const project = await client.request(`${config.cloudResourceManagerApiBaseUrl ?? GCP_CLOUD_RESOURCE_MANAGER_API_BASE_URL}/v1/projects/${encodeURIComponent(projectId)}`);
551
+ return {
552
+ provider: "gcp",
553
+ ok: true,
554
+ summary: `project: ${project.projectId ?? project.name}`,
555
+ details: {
556
+ projectId: project.projectId ?? projectId,
557
+ name: project.name ?? null,
558
+ displayName: project.displayName ?? null,
559
+ },
560
+ };
561
+ }
562
+ const projects = await client.request(`${config.cloudResourceManagerApiBaseUrl ?? GCP_CLOUD_RESOURCE_MANAGER_API_BASE_URL}/v1/projects?pageSize=1`);
563
+ const firstProject = projects.projects?.[0] ?? null;
564
+ return {
565
+ provider: "gcp",
566
+ ok: true,
567
+ summary: firstProject
568
+ ? `project access: ${firstProject.projectId ?? firstProject.name}`
569
+ : "project access: no visible projects",
570
+ details: {
571
+ projectId: firstProject?.projectId ?? null,
572
+ name: firstProject?.name ?? null,
573
+ displayName: firstProject?.displayName ?? null,
574
+ },
575
+ };
576
+ }
577
+ export function wrapHandlersWithActionPassVerification(handlers, config) {
578
+ return Object.fromEntries(Object.entries(handlers).map(([tool, handler]) => [
579
+ tool,
580
+ createActionPassVerifiedHandler(handler, config),
581
+ ]));
582
+ }
583
+ export function createActionPassVerifiedHandler(handler, config) {
584
+ return async (request) => {
585
+ if (!request.actionPass) {
586
+ throw new Error("adapter_actionpass_required");
587
+ }
588
+ const verified = await verifyActionPass({
589
+ token: request.actionPass.token,
590
+ action: request.action,
591
+ verificationKey: config.verificationKey,
592
+ issuer: config.issuer,
593
+ algorithms: config.algorithms,
594
+ clockTolerance: config.clockTolerance,
595
+ currentDate: config.currentDate?.(),
596
+ });
597
+ if (!verified.valid) {
598
+ throw new Error(`adapter_actionpass_invalid:${verified.reason}`);
599
+ }
600
+ return handler(request);
601
+ };
602
+ }
603
+ export function createFakeGitHubPullRequestHandler(state) {
604
+ return ({ action, actionPass }) => {
605
+ const payload = action.capability.payload;
606
+ const filesChanged = getStringArray(payload.filesChanged);
607
+ const number = state.github.pullRequests.length + 1;
608
+ const pullRequest = {
609
+ id: `fake_pr_${number}`,
610
+ number,
611
+ resource: action.capability.resource,
612
+ title: getString(payload.title) ?? action.intent.declaredGoal,
613
+ baseBranch: getString(payload.baseBranch),
614
+ filesChanged,
615
+ actionPassId: actionPass?.claims.jti ?? null,
616
+ };
617
+ state.github.pullRequests.push(pullRequest);
618
+ return {
619
+ provider: "fake-github",
620
+ kind: "pull_request",
621
+ id: pullRequest.id,
622
+ number: pullRequest.number,
623
+ url: `https://github.fake/${encodeURIComponent(action.capability.resource)}/pull/${number}`,
624
+ actionPassId: pullRequest.actionPassId,
625
+ };
626
+ };
627
+ }
628
+ export function createFakeGitHubContentReadHandler(state) {
629
+ return ({ action, actionPass }) => {
630
+ const path = getString(action.capability.payload.path);
631
+ if (!path) {
632
+ throw new Error("fake_github_missing_path");
633
+ }
634
+ const read = {
635
+ resource: action.capability.resource,
636
+ path,
637
+ actionPassId: actionPass?.claims.jti ?? null,
638
+ };
639
+ state.github.contentReads.push(read);
640
+ return {
641
+ provider: "fake-github",
642
+ kind: "content_read",
643
+ path,
644
+ content: `// fake content for ${path}`,
645
+ actionPassId: read.actionPassId,
646
+ };
647
+ };
648
+ }
649
+ export function createFakeGitHubContentWriteHandler(state) {
650
+ return ({ action, actionPass }) => {
651
+ const path = requireString(action.capability.payload.path, "fake_github_missing_path");
652
+ const content = requireString(action.capability.payload.content, "fake_github_missing_content");
653
+ const branch = requireString(action.capability.payload.branch, "fake_github_missing_branch");
654
+ const message = getString(action.capability.payload.message) ?? `Update ${path} via Axtary`;
655
+ const write = {
656
+ resource: action.capability.resource,
657
+ path,
658
+ branch,
659
+ message,
660
+ contentLength: content.length,
661
+ actionPassId: actionPass?.claims.jti ?? null,
662
+ };
663
+ state.github.contentWrites.push(write);
664
+ return {
665
+ provider: "fake-github",
666
+ kind: "content_write",
667
+ path,
668
+ branch,
669
+ commitSha: `fake_commit_${state.github.contentWrites.length}`,
670
+ actionPassId: write.actionPassId,
671
+ };
672
+ };
673
+ }
674
+ export function createFakeGitHubBranchCreateHandler(state) {
675
+ return ({ action, actionPass }) => {
676
+ const branch = requireString(action.capability.payload.branch, "fake_github_missing_branch");
677
+ const baseBranch = getString(action.capability.payload.baseBranch);
678
+ const sha = getString(action.capability.payload.sha);
679
+ const created = {
680
+ resource: action.capability.resource,
681
+ branch,
682
+ baseBranch,
683
+ sha,
684
+ actionPassId: actionPass?.claims.jti ?? null,
685
+ };
686
+ state.github.branches.push(created);
687
+ return {
688
+ provider: "fake-github",
689
+ kind: "branch",
690
+ branch,
691
+ sha,
692
+ actionPassId: created.actionPassId,
693
+ };
694
+ };
695
+ }
696
+ export function createFakeSlackMessageHandler(state) {
697
+ return ({ action, actionPass }) => {
698
+ const channel = getString(action.capability.payload.channel);
699
+ const message = getString(action.capability.payload.message);
700
+ if (!channel) {
701
+ throw new Error("fake_slack_missing_channel");
702
+ }
703
+ if (!message) {
704
+ throw new Error("fake_slack_missing_message");
705
+ }
706
+ const number = state.slack.messages.length + 1;
707
+ const slackMessage = {
708
+ id: `fake_slack_msg_${number}`,
709
+ workspace: action.capability.resource,
710
+ channel,
711
+ message,
712
+ actionPassId: actionPass?.claims.jti ?? null,
713
+ };
714
+ state.slack.messages.push(slackMessage);
715
+ return {
716
+ provider: "fake-slack",
717
+ kind: "message",
718
+ id: slackMessage.id,
719
+ channel,
720
+ actionPassId: slackMessage.actionPassId,
721
+ };
722
+ };
723
+ }
724
+ export function createFakeIssueReadHandler(state, provider) {
725
+ return ({ action, actionPass }) => {
726
+ const issueKey = getString(action.capability.payload.issueKey);
727
+ if (!issueKey) {
728
+ throw new Error(`fake_${provider}_missing_issue_key`);
729
+ }
730
+ const issue = findIssue(state, provider, issueKey);
731
+ if (!issue) {
732
+ throw new Error(`fake_${provider}_issue_not_found`);
733
+ }
734
+ return {
735
+ provider: `fake-${provider}`,
736
+ kind: "issue",
737
+ issueKey: issue.issueKey,
738
+ projectKey: issue.projectKey,
739
+ title: issue.title,
740
+ status: issue.status,
741
+ actionPassId: actionPass?.claims.jti ?? null,
742
+ };
743
+ };
744
+ }
745
+ export function createFakeIssueCommentHandler(state, provider) {
746
+ return ({ action, actionPass }) => {
747
+ const issueKey = getString(action.capability.payload.issueKey);
748
+ const body = getString(action.capability.payload.body);
749
+ if (!issueKey) {
750
+ throw new Error(`fake_${provider}_missing_issue_key`);
751
+ }
752
+ if (!body) {
753
+ throw new Error(`fake_${provider}_missing_comment_body`);
754
+ }
755
+ const issue = findIssue(state, provider, issueKey);
756
+ if (!issue) {
757
+ throw new Error(`fake_${provider}_issue_not_found`);
758
+ }
759
+ const comments = state.issueTrackers[provider].comments;
760
+ const number = comments.length + 1;
761
+ const comment = {
762
+ id: `fake_${provider}_comment_${number}`,
763
+ provider,
764
+ issueKey,
765
+ body,
766
+ actionPassId: actionPass?.claims.jti ?? null,
767
+ };
768
+ comments.push(comment);
769
+ return {
770
+ provider: `fake-${provider}`,
771
+ kind: "issue_comment",
772
+ id: comment.id,
773
+ issueKey,
774
+ actionPassId: comment.actionPassId,
775
+ };
776
+ };
777
+ }
778
+ export function createFakeIssueUpdateHandler(state, provider) {
779
+ return ({ action, actionPass }) => {
780
+ const issueKey = getString(action.capability.payload.issueKey);
781
+ const status = getString(action.capability.payload.status);
782
+ if (!issueKey) {
783
+ throw new Error(`fake_${provider}_missing_issue_key`);
784
+ }
785
+ const issue = findIssue(state, provider, issueKey);
786
+ if (!issue) {
787
+ throw new Error(`fake_${provider}_issue_not_found`);
788
+ }
789
+ if (status) {
790
+ issue.status = status;
791
+ }
792
+ return {
793
+ provider: `fake-${provider}`,
794
+ kind: "issue_update",
795
+ issueKey,
796
+ status: issue.status,
797
+ actionPassId: actionPass?.claims.jti ?? null,
798
+ };
799
+ };
800
+ }
801
+ export function createGitHubRestContentReadHandler(config) {
802
+ const client = createGitHubRestClient(config);
803
+ return async ({ action, actionPass }) => {
804
+ const repo = parseGitHubRepoResource(action.capability.resource);
805
+ const path = requireString(action.capability.payload.path, "github_rest_missing_path");
806
+ const ref = getString(action.capability.payload.ref);
807
+ const query = ref ? `?ref=${encodeURIComponent(ref)}` : "";
808
+ const content = await client.request(`/repos/${repo.owner}/${repo.name}/contents/${encodeGitHubPath(path)}${query}`, { method: "GET" });
809
+ if (Array.isArray(content)) {
810
+ throw new Error("github_rest_expected_file_content");
811
+ }
812
+ const encoding = content.encoding;
813
+ const body = encoding === "base64"
814
+ ? Buffer.from(content.content.replace(/\n/g, ""), "base64").toString("utf8")
815
+ : content.content;
816
+ return {
817
+ provider: "github",
818
+ kind: "content_read",
819
+ owner: repo.owner,
820
+ repo: repo.name,
821
+ path: content.path,
822
+ sha: content.sha,
823
+ encoding,
824
+ content: body,
825
+ actionPassId: actionPass?.claims.jti ?? null,
826
+ };
827
+ };
828
+ }
829
+ export function createGitHubRestPullRequestHandler(config) {
830
+ const client = createGitHubRestClient(config);
831
+ return async ({ action, actionPass }) => {
832
+ const repo = parseGitHubRepoResource(action.capability.resource);
833
+ const payload = action.capability.payload;
834
+ const title = getString(payload.title) ?? action.intent.declaredGoal;
835
+ const head = requireString(payload.headBranch, "github_rest_missing_head_branch");
836
+ const base = requireString(payload.baseBranch, "github_rest_missing_base_branch");
837
+ const body = getString(payload.body);
838
+ const draft = getBoolean(payload.draft) ?? true;
839
+ const maintainerCanModify = getBoolean(payload.maintainerCanModify) ?? true;
840
+ const changes = getObjectArray(payload.changes);
841
+ const createdBranch = changes.length > 0 || getBoolean(payload.createBranch) === true
842
+ ? await createGitHubBranch(client, repo, {
843
+ branch: head,
844
+ baseBranch: base,
845
+ allowExisting: getBoolean(payload.allowExistingHeadBranch) ?? false,
846
+ })
847
+ : null;
848
+ const commits = [];
849
+ for (const change of changes) {
850
+ commits.push(await writeGitHubContent(client, repo, {
851
+ path: requireString(change.path, "github_rest_change_missing_path"),
852
+ content: requireString(change.content, "github_rest_change_missing_content"),
853
+ branch: head,
854
+ message: getString(change.message) ??
855
+ getString(payload.commitMessage) ??
856
+ `Update ${requireString(change.path, "github_rest_change_missing_path")}`,
857
+ sha: getString(change.sha),
858
+ }));
859
+ }
860
+ const pullRequest = await client.request(`/repos/${repo.owner}/${repo.name}/pulls`, {
861
+ method: "POST",
862
+ body: {
863
+ title,
864
+ head,
865
+ base,
866
+ body,
867
+ draft,
868
+ maintainer_can_modify: maintainerCanModify,
869
+ },
870
+ });
871
+ return {
872
+ provider: "github",
873
+ kind: "pull_request",
874
+ id: String(pullRequest.id),
875
+ number: pullRequest.number,
876
+ url: pullRequest.html_url,
877
+ state: pullRequest.state,
878
+ draft: pullRequest.draft,
879
+ branchCreated: createdBranch,
880
+ commits,
881
+ actionPassId: actionPass?.claims.jti ?? null,
882
+ };
883
+ };
884
+ }
885
+ export function createGitHubRestBranchCreateHandler(config) {
886
+ const client = createGitHubRestClient(config);
887
+ return async ({ action, actionPass }) => {
888
+ const repo = parseGitHubRepoResource(action.capability.resource);
889
+ const branch = requireString(action.capability.payload.branch, "github_rest_missing_branch");
890
+ const baseBranch = requireString(action.capability.payload.baseBranch, "github_rest_missing_base_branch");
891
+ const created = await createGitHubBranch(client, repo, {
892
+ branch,
893
+ baseBranch,
894
+ sha: getString(action.capability.payload.sha),
895
+ allowExisting: getBoolean(action.capability.payload.allowExisting) ?? false,
896
+ });
897
+ return {
898
+ provider: "github",
899
+ kind: "branch",
900
+ branch,
901
+ sha: created.sha,
902
+ actionPassId: actionPass?.claims.jti ?? null,
903
+ };
904
+ };
905
+ }
906
+ export function createGitHubRestContentWriteHandler(config) {
907
+ const client = createGitHubRestClient(config);
908
+ return async ({ action, actionPass }) => {
909
+ const repo = parseGitHubRepoResource(action.capability.resource);
910
+ const written = await writeGitHubContent(client, repo, {
911
+ path: requireString(action.capability.payload.path, "github_rest_missing_path"),
912
+ content: requireString(action.capability.payload.content, "github_rest_missing_content"),
913
+ branch: requireString(action.capability.payload.branch, "github_rest_missing_branch"),
914
+ message: getString(action.capability.payload.message) ??
915
+ `Update ${requireString(action.capability.payload.path, "github_rest_missing_path")}`,
916
+ sha: getString(action.capability.payload.sha),
917
+ });
918
+ return {
919
+ provider: "github",
920
+ kind: "content_write",
921
+ ...written,
922
+ actionPassId: actionPass?.claims.jti ?? null,
923
+ };
924
+ };
925
+ }
926
+ export function createSlackWebPostMessageHandler(config) {
927
+ const client = createSlackWebClient(config);
928
+ return async ({ action, actionPass }) => {
929
+ const channel = requireString(action.capability.payload.channel, "slack_web_missing_channel");
930
+ const text = getString(action.capability.payload.text) ??
931
+ requireString(action.capability.payload.message, "slack_web_missing_text");
932
+ const response = await client.request("chat.postMessage", {
933
+ channel,
934
+ text,
935
+ thread_ts: getString(action.capability.payload.threadTs) ?? undefined,
936
+ unfurl_links: getBoolean(action.capability.payload.unfurlLinks) ?? false,
937
+ unfurl_media: getBoolean(action.capability.payload.unfurlMedia) ?? false,
938
+ });
939
+ return {
940
+ provider: "slack",
941
+ kind: "message",
942
+ channel: response.channel,
943
+ ts: response.ts,
944
+ text,
945
+ actionPassId: actionPass?.claims.jti ?? null,
946
+ };
947
+ };
948
+ }
949
+ export function createLinearIssueReadHandler(config) {
950
+ const client = createLinearGraphqlClient(config);
951
+ return async ({ action, actionPass }) => {
952
+ const issueId = issueIdentifier(action.capability.payload);
953
+ const response = await client.request(`
954
+ query AxtaryIssueRead($id: String!) {
955
+ issue(id: $id) {
956
+ id
957
+ identifier
958
+ title
959
+ url
960
+ state { id name }
961
+ team { key }
962
+ }
963
+ }
964
+ `, { id: issueId });
965
+ const issue = response.issue;
966
+ if (!issue) {
967
+ throw new Error(`linear_graphql_issue_not_found:${issueId}`);
968
+ }
969
+ return {
970
+ provider: "linear",
971
+ kind: "issue",
972
+ id: issue.id,
973
+ issueKey: issue.identifier,
974
+ projectKey: issue.team?.key ?? null,
975
+ title: issue.title,
976
+ url: issue.url,
977
+ status: issue.state?.name ?? null,
978
+ stateId: issue.state?.id ?? null,
979
+ actionPassId: actionPass?.claims.jti ?? null,
980
+ };
981
+ };
982
+ }
983
+ export function createLinearCommentCreateHandler(config) {
984
+ const client = createLinearGraphqlClient(config);
985
+ return async ({ action, actionPass }) => {
986
+ const issueId = issueIdentifier(action.capability.payload);
987
+ const body = requireString(action.capability.payload.body, "linear_graphql_missing_body");
988
+ const response = await client.request(`
989
+ mutation AxtaryCommentCreate($input: CommentCreateInput!) {
990
+ commentCreate(input: $input) {
991
+ success
992
+ comment {
993
+ id
994
+ url
995
+ body
996
+ issue { id identifier }
997
+ }
998
+ }
999
+ }
1000
+ `, {
1001
+ input: {
1002
+ issueId,
1003
+ body,
1004
+ },
1005
+ });
1006
+ if (!response.commentCreate.success || !response.commentCreate.comment) {
1007
+ throw new Error("linear_graphql_comment_create_failed");
1008
+ }
1009
+ return {
1010
+ provider: "linear",
1011
+ kind: "issue_comment",
1012
+ id: response.commentCreate.comment.id,
1013
+ issueKey: response.commentCreate.comment.issue?.identifier ?? issueId,
1014
+ url: response.commentCreate.comment.url,
1015
+ actionPassId: actionPass?.claims.jti ?? null,
1016
+ };
1017
+ };
1018
+ }
1019
+ export function createLinearIssueUpdateHandler(config) {
1020
+ const client = createLinearGraphqlClient(config);
1021
+ return async ({ action, actionPass }) => {
1022
+ const issueId = issueIdentifier(action.capability.payload);
1023
+ const input = removeUndefined({
1024
+ title: getString(action.capability.payload.title) ?? undefined,
1025
+ description: getString(action.capability.payload.description) ?? undefined,
1026
+ stateId: getString(action.capability.payload.stateId) ?? undefined,
1027
+ priority: getNumber(action.capability.payload.priority) ?? undefined,
1028
+ });
1029
+ if (Object.keys(input).length === 0) {
1030
+ throw new Error("linear_graphql_missing_update_fields");
1031
+ }
1032
+ const response = await client.request(`
1033
+ mutation AxtaryIssueUpdate($id: String!, $input: IssueUpdateInput!) {
1034
+ issueUpdate(id: $id, input: $input) {
1035
+ success
1036
+ issue {
1037
+ id
1038
+ identifier
1039
+ title
1040
+ url
1041
+ state { id name }
1042
+ }
1043
+ }
1044
+ }
1045
+ `, {
1046
+ id: issueId,
1047
+ input,
1048
+ });
1049
+ if (!response.issueUpdate.success || !response.issueUpdate.issue) {
1050
+ throw new Error("linear_graphql_issue_update_failed");
1051
+ }
1052
+ return {
1053
+ provider: "linear",
1054
+ kind: "issue_update",
1055
+ id: response.issueUpdate.issue.id,
1056
+ issueKey: response.issueUpdate.issue.identifier,
1057
+ title: response.issueUpdate.issue.title,
1058
+ url: response.issueUpdate.issue.url,
1059
+ status: response.issueUpdate.issue.state?.name ?? null,
1060
+ stateId: response.issueUpdate.issue.state?.id ?? null,
1061
+ actionPassId: actionPass?.claims.jti ?? null,
1062
+ };
1063
+ };
1064
+ }
1065
+ export function createAwsIdentityGetHandler(config) {
1066
+ const client = createAwsClient(config);
1067
+ return async ({ actionPass }) => {
1068
+ const identity = await getAwsCallerIdentity(client);
1069
+ return {
1070
+ provider: "aws",
1071
+ kind: "identity",
1072
+ account: identity.account,
1073
+ arn: identity.arn,
1074
+ userId: identity.userId,
1075
+ actionPassId: actionPass?.claims.jti ?? null,
1076
+ };
1077
+ };
1078
+ }
1079
+ export function createAwsS3ObjectsListHandler(config) {
1080
+ const client = createAwsClient(config);
1081
+ return async ({ action, actionPass }) => {
1082
+ const bucket = requireString(action.capability.payload.bucket, "aws_s3_missing_bucket");
1083
+ const region = getString(action.capability.payload.region) ?? config.region ?? "us-east-1";
1084
+ const prefix = getString(action.capability.payload.prefix);
1085
+ const maxKeys = Math.min(Math.max(getNumber(action.capability.payload.maxKeys) ?? 25, 1), 1000);
1086
+ const url = new URL(`https://${bucket}.s3.${region}.amazonaws.com/`);
1087
+ url.searchParams.set("list-type", "2");
1088
+ url.searchParams.set("max-keys", String(maxKeys));
1089
+ if (prefix) {
1090
+ url.searchParams.set("prefix", prefix);
1091
+ }
1092
+ const xml = await client.requestText({
1093
+ method: "GET",
1094
+ url,
1095
+ region,
1096
+ service: "s3",
1097
+ });
1098
+ const objects = parseAwsS3ListBucketResult(xml);
1099
+ return {
1100
+ provider: "aws",
1101
+ kind: "s3_objects",
1102
+ bucket,
1103
+ region,
1104
+ prefix: prefix ?? null,
1105
+ objects,
1106
+ actionPassId: actionPass?.claims.jti ?? null,
1107
+ };
1108
+ };
1109
+ }
1110
+ export function createGcpProjectGetHandler(config) {
1111
+ const client = createGcpRestClient(config);
1112
+ const baseUrl = config.cloudResourceManagerApiBaseUrl ?? GCP_CLOUD_RESOURCE_MANAGER_API_BASE_URL;
1113
+ return async ({ action, actionPass }) => {
1114
+ const projectId = getString(action.capability.payload.projectId) ??
1115
+ config.projectId ??
1116
+ fail("gcp_missing_project_id");
1117
+ const project = await client.request(`${baseUrl}/v1/projects/${encodeURIComponent(projectId)}`);
1118
+ return {
1119
+ provider: "gcp",
1120
+ kind: "project",
1121
+ projectId: project.projectId ?? projectId,
1122
+ name: project.name ?? null,
1123
+ displayName: project.displayName ?? null,
1124
+ state: project.state ?? null,
1125
+ actionPassId: actionPass?.claims.jti ?? null,
1126
+ };
1127
+ };
1128
+ }
1129
+ export function createGcpStorageObjectsListHandler(config) {
1130
+ const client = createGcpRestClient(config);
1131
+ const baseUrl = config.storageApiBaseUrl ?? GCP_STORAGE_API_BASE_URL;
1132
+ return async ({ action, actionPass }) => {
1133
+ const bucket = requireString(action.capability.payload.bucket, "gcp_storage_missing_bucket");
1134
+ const prefix = getString(action.capability.payload.prefix);
1135
+ const maxResults = Math.min(Math.max(getNumber(action.capability.payload.maxResults) ?? 25, 1), 1000);
1136
+ const url = new URL(`${baseUrl}/b/${encodeURIComponent(bucket)}/o`);
1137
+ url.searchParams.set("maxResults", String(maxResults));
1138
+ if (prefix) {
1139
+ url.searchParams.set("prefix", prefix);
1140
+ }
1141
+ const response = await client.request(url.toString());
1142
+ return {
1143
+ provider: "gcp",
1144
+ kind: "storage_objects",
1145
+ bucket,
1146
+ prefix: prefix ?? null,
1147
+ objects: (response.items ?? []).map((item) => ({
1148
+ name: item.name,
1149
+ size: item.size ?? null,
1150
+ updated: item.updated ?? null,
1151
+ })),
1152
+ actionPassId: actionPass?.claims.jti ?? null,
1153
+ };
1154
+ };
1155
+ }
1156
+ export function createLocalDocsSearchHandler(config) {
1157
+ const workspace = config.workspace ?? "local";
1158
+ const maxSearchResults = config.maxSearchResults ?? 10;
1159
+ const allowedExtensions = new Set((config.allowedExtensions ?? [".md", ".mdx", ".txt"]).map((extension) => extension.toLowerCase()));
1160
+ return async ({ action, actionPass }) => {
1161
+ const requestedWorkspace = getString(action.capability.payload.workspace) ?? workspace;
1162
+ const query = requireString(action.capability.payload.query, "docs_search_query_required").trim();
1163
+ const maxResults = Math.min(Math.max(getNumber(action.capability.payload.maxResults) ?? 5, 1), maxSearchResults);
1164
+ if (!query) {
1165
+ throw new Error("docs_search_query_required");
1166
+ }
1167
+ if (requestedWorkspace !== workspace) {
1168
+ throw new Error(`docs_workspace_not_available:${requestedWorkspace}`);
1169
+ }
1170
+ const queryLower = query.toLowerCase();
1171
+ const roots = normalizeDocsRoots(config);
1172
+ const results = [];
1173
+ for (const root of roots) {
1174
+ const files = await collectLocalDocsFiles(root, allowedExtensions);
1175
+ for (const file of files) {
1176
+ const content = await readFile(file.absolutePath, "utf8");
1177
+ const matchIndex = content.toLowerCase().indexOf(queryLower);
1178
+ if (matchIndex === -1) {
1179
+ continue;
1180
+ }
1181
+ results.push({
1182
+ path: file.relativePath,
1183
+ root: root.configuredPath,
1184
+ snippet: redactDocumentText(snippetAround(content, matchIndex, query.length)),
1185
+ });
1186
+ if (results.length >= maxResults) {
1187
+ return {
1188
+ provider: "docs",
1189
+ kind: "document_search",
1190
+ workspace,
1191
+ query,
1192
+ results,
1193
+ actionPassId: actionPass?.claims.jti ?? null,
1194
+ };
1195
+ }
1196
+ }
1197
+ }
1198
+ return {
1199
+ provider: "docs",
1200
+ kind: "document_search",
1201
+ workspace,
1202
+ query,
1203
+ results,
1204
+ actionPassId: actionPass?.claims.jti ?? null,
1205
+ };
1206
+ };
1207
+ }
1208
+ export function createLocalDocsReadHandler(config) {
1209
+ const workspace = config.workspace ?? "local";
1210
+ const maxReadBytes = config.maxReadBytes ?? 20_000;
1211
+ return async ({ action, actionPass }) => {
1212
+ const requestedWorkspace = getString(action.capability.payload.workspace) ?? workspace;
1213
+ const requestedPath = requireString(action.capability.payload.path, "docs_read_path_required");
1214
+ const normalizedPath = normalizeDocumentPath(requestedPath);
1215
+ const maxBytes = Math.min(Math.max(getNumber(action.capability.payload.maxBytes) ?? maxReadBytes, 1), maxReadBytes);
1216
+ if (requestedWorkspace !== workspace) {
1217
+ throw new Error(`docs_workspace_not_available:${requestedWorkspace}`);
1218
+ }
1219
+ const roots = normalizeDocsRoots(config);
1220
+ const resolved = await resolveLocalDocsPath(roots, normalizedPath);
1221
+ const content = await readFile(resolved.absolutePath, "utf8");
1222
+ const buffer = Buffer.from(content, "utf8");
1223
+ const truncated = buffer.byteLength > maxBytes;
1224
+ const body = truncated ? buffer.subarray(0, maxBytes).toString("utf8") : content;
1225
+ return {
1226
+ provider: "docs",
1227
+ kind: "document",
1228
+ workspace,
1229
+ path: resolved.relativePath,
1230
+ root: resolved.root.configuredPath,
1231
+ bytes: Math.min(buffer.byteLength, maxBytes),
1232
+ truncated,
1233
+ content: redactDocumentText(body),
1234
+ actionPassId: actionPass?.claims.jti ?? null,
1235
+ };
1236
+ };
1237
+ }
1238
+ function normalizeDocsRoots(config) {
1239
+ if (config.roots.length === 0) {
1240
+ throw new Error("docs_roots_required");
1241
+ }
1242
+ return config.roots.map((rootPath) => ({
1243
+ configuredPath: rootPath,
1244
+ absolutePath: resolve(rootPath),
1245
+ pathPrefix: localDocsRootPrefix(rootPath),
1246
+ }));
1247
+ }
1248
+ async function collectLocalDocsFiles(root, allowedExtensions) {
1249
+ const files = [];
1250
+ await collectLocalDocsFilesInto(root, root.absolutePath, allowedExtensions, files);
1251
+ return files;
1252
+ }
1253
+ async function collectLocalDocsFilesInto(root, dir, allowedExtensions, files) {
1254
+ const entries = await readdir(dir, { withFileTypes: true });
1255
+ for (const entry of entries) {
1256
+ const absolutePath = resolve(dir, entry.name);
1257
+ if (entry.isDirectory()) {
1258
+ if (!entry.name.startsWith(".")) {
1259
+ await collectLocalDocsFilesInto(root, absolutePath, allowedExtensions, files);
1260
+ }
1261
+ continue;
1262
+ }
1263
+ if (!entry.isFile()) {
1264
+ continue;
1265
+ }
1266
+ if (!allowedExtensions.has(extname(entry.name).toLowerCase())) {
1267
+ continue;
1268
+ }
1269
+ const relativePath = withLocalDocsRootPrefix(root, toPosixPath(relative(root.absolutePath, absolutePath)));
1270
+ if (isUnsafeDocumentPath(relativePath)) {
1271
+ continue;
1272
+ }
1273
+ files.push({
1274
+ root,
1275
+ absolutePath,
1276
+ relativePath,
1277
+ });
1278
+ }
1279
+ }
1280
+ async function resolveLocalDocsPath(roots, requestedPath) {
1281
+ for (const root of roots) {
1282
+ const pathInsideRoot = root.pathPrefix && requestedPath.startsWith(root.pathPrefix)
1283
+ ? requestedPath.slice(root.pathPrefix.length)
1284
+ : requestedPath;
1285
+ const absolutePath = resolve(root.absolutePath, pathInsideRoot);
1286
+ const relativePath = toPosixPath(relative(root.absolutePath, absolutePath));
1287
+ if (isUnsafeDocumentPath(relativePath)) {
1288
+ continue;
1289
+ }
1290
+ try {
1291
+ const stats = await stat(absolutePath);
1292
+ if (stats.isFile()) {
1293
+ return {
1294
+ root,
1295
+ absolutePath,
1296
+ relativePath: withLocalDocsRootPrefix(root, relativePath),
1297
+ };
1298
+ }
1299
+ }
1300
+ catch (error) {
1301
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
1302
+ throw error;
1303
+ }
1304
+ }
1305
+ }
1306
+ throw new Error(`docs_path_not_found:${requestedPath}`);
1307
+ }
1308
+ function normalizeDocumentPath(path) {
1309
+ const normalized = toPosixPath(path.trim());
1310
+ if (isUnsafeDocumentPath(normalized)) {
1311
+ throw new Error("docs_path_unsafe");
1312
+ }
1313
+ return normalized;
1314
+ }
1315
+ function isUnsafeDocumentPath(path) {
1316
+ if (!path || path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
1317
+ return true;
1318
+ }
1319
+ return path.split("/").some((segment) => segment === ".." || segment === "");
1320
+ }
1321
+ function toPosixPath(path) {
1322
+ return path.split(sep).join("/").replace(/\\/g, "/");
1323
+ }
1324
+ function localDocsRootPrefix(rootPath) {
1325
+ const name = basename(rootPath);
1326
+ if (!name || name === "." || (isAbsolute(rootPath) && name !== "docs" && name !== "product")) {
1327
+ return null;
1328
+ }
1329
+ return `${name}/`;
1330
+ }
1331
+ function withLocalDocsRootPrefix(root, relativePath) {
1332
+ return root.pathPrefix && !relativePath.startsWith(root.pathPrefix)
1333
+ ? `${root.pathPrefix}${relativePath}`
1334
+ : relativePath;
1335
+ }
1336
+ function snippetAround(content, matchIndex, queryLength) {
1337
+ const start = Math.max(0, matchIndex - 80);
1338
+ const end = Math.min(content.length, matchIndex + queryLength + 120);
1339
+ const prefix = start > 0 ? "..." : "";
1340
+ const suffix = end < content.length ? "..." : "";
1341
+ return `${prefix}${content.slice(start, end).replace(/\s+/g, " ").trim()}${suffix}`;
1342
+ }
1343
+ function redactDocumentText(text) {
1344
+ return text
1345
+ .replace(/\b(token|secret|password|api[_-]?key)\s*[:=]\s*([^\s`'"]+)/gi, "$1=[redacted]")
1346
+ .replace(/\b(Authorization:\s*Bearer\s+)([^\s`'"]+)/gi, "$1[redacted]");
1347
+ }
1348
+ function findIssue(state, provider, issueKey) {
1349
+ return state.issueTrackers[provider].issues.find((issue) => issue.issueKey === issueKey);
1350
+ }
1351
+ function createAwsClient(config) {
1352
+ const fetchImpl = config.fetch ?? fetch;
1353
+ const maxRetries = config.maxRetries ?? DEFAULT_PROVIDER_RETRIES;
1354
+ return {
1355
+ stsUrl: config.stsUrl ?? AWS_STS_API_URL,
1356
+ async requestText(input) {
1357
+ return requestWithRetry(maxRetries, async () => {
1358
+ const signed = signAwsRequest({
1359
+ ...input,
1360
+ credentials: config.credentials,
1361
+ });
1362
+ const response = await fetchImpl(input.url.toString(), {
1363
+ method: input.method,
1364
+ headers: signed.headers,
1365
+ body: input.body,
1366
+ });
1367
+ if (!response.ok) {
1368
+ throw await providerError("aws_rest", response);
1369
+ }
1370
+ return response.text();
1371
+ });
1372
+ },
1373
+ };
1374
+ }
1375
+ async function getAwsCallerIdentity(client) {
1376
+ const url = new URL(client.stsUrl);
1377
+ const body = "Action=GetCallerIdentity&Version=2011-06-15";
1378
+ const xml = await client.requestText({
1379
+ method: "POST",
1380
+ url,
1381
+ region: "us-east-1",
1382
+ service: "sts",
1383
+ body,
1384
+ headers: {
1385
+ "content-type": "application/x-www-form-urlencoded; charset=utf-8",
1386
+ },
1387
+ });
1388
+ return {
1389
+ account: requireXmlText(xml, "Account", "aws_sts_missing_account"),
1390
+ arn: requireXmlText(xml, "Arn", "aws_sts_missing_arn"),
1391
+ userId: requireXmlText(xml, "UserId", "aws_sts_missing_user_id"),
1392
+ };
1393
+ }
1394
+ function createGcpRestClient(config) {
1395
+ const fetchImpl = config.fetch ?? fetch;
1396
+ const maxRetries = config.maxRetries ?? DEFAULT_PROVIDER_RETRIES;
1397
+ return {
1398
+ async request(url) {
1399
+ return requestWithRetry(maxRetries, async () => {
1400
+ const response = await fetchImpl(url, {
1401
+ method: "GET",
1402
+ headers: {
1403
+ authorization: `Bearer ${config.accessToken}`,
1404
+ },
1405
+ });
1406
+ if (!response.ok) {
1407
+ throw await providerError("gcp_rest", response);
1408
+ }
1409
+ return response.json();
1410
+ });
1411
+ },
1412
+ };
1413
+ }
1414
+ function createGitHubRestClient(config) {
1415
+ const fetchImpl = config.fetch ?? fetch;
1416
+ const apiBaseUrl = (config.apiBaseUrl ?? GITHUB_REST_API_BASE_URL).replace(/\/$/, "");
1417
+ const userAgent = config.userAgent ?? GITHUB_REST_USER_AGENT;
1418
+ const maxRetries = config.maxRetries ?? DEFAULT_PROVIDER_RETRIES;
1419
+ return {
1420
+ async request(path, options) {
1421
+ return requestWithRetry(maxRetries, async () => {
1422
+ const response = await fetchImpl(`${apiBaseUrl}${path}`, {
1423
+ method: options.method,
1424
+ headers: {
1425
+ accept: "application/vnd.github+json",
1426
+ authorization: `Bearer ${config.token}`,
1427
+ "content-type": "application/json",
1428
+ "user-agent": userAgent,
1429
+ "x-github-api-version": "2022-11-28",
1430
+ },
1431
+ body: options.body ? JSON.stringify(removeUndefined(options.body)) : undefined,
1432
+ });
1433
+ if (!response.ok) {
1434
+ throw await providerError("github_rest", response);
1435
+ }
1436
+ return response.json();
1437
+ });
1438
+ },
1439
+ };
1440
+ }
1441
+ function createSlackWebClient(config) {
1442
+ const fetchImpl = config.fetch ?? fetch;
1443
+ const apiBaseUrl = (config.apiBaseUrl ?? SLACK_WEB_API_BASE_URL).replace(/\/$/, "");
1444
+ const maxRetries = config.maxRetries ?? DEFAULT_PROVIDER_RETRIES;
1445
+ return {
1446
+ async request(method, body) {
1447
+ return requestWithRetry(maxRetries, async () => {
1448
+ const response = await fetchImpl(`${apiBaseUrl}/${method}`, {
1449
+ method: "POST",
1450
+ headers: {
1451
+ authorization: `Bearer ${config.token}`,
1452
+ "content-type": "application/json; charset=utf-8",
1453
+ },
1454
+ body: JSON.stringify(removeUndefined(body)),
1455
+ });
1456
+ if (!response.ok) {
1457
+ throw await providerError("slack_web", response);
1458
+ }
1459
+ const parsed = (await response.json());
1460
+ if (!parsed.ok) {
1461
+ throw new ProviderRequestError(`slack_web_error:${parsed.error ?? "unknown"}`, 200, parsed.error === "ratelimited" ? "rate_limited" : "provider_error");
1462
+ }
1463
+ return parsed;
1464
+ });
1465
+ },
1466
+ };
1467
+ }
1468
+ function createLinearGraphqlClient(config) {
1469
+ const fetchImpl = config.fetch ?? fetch;
1470
+ const apiUrl = config.apiUrl ?? LINEAR_GRAPHQL_API_URL;
1471
+ const maxRetries = config.maxRetries ?? DEFAULT_PROVIDER_RETRIES;
1472
+ return {
1473
+ async request(query, variables) {
1474
+ return requestWithRetry(maxRetries, async () => {
1475
+ const response = await fetchImpl(apiUrl, {
1476
+ method: "POST",
1477
+ headers: {
1478
+ authorization: config.token,
1479
+ "content-type": "application/json",
1480
+ },
1481
+ body: JSON.stringify({
1482
+ query,
1483
+ variables,
1484
+ }),
1485
+ });
1486
+ if (!response.ok) {
1487
+ throw await providerError("linear_graphql", response);
1488
+ }
1489
+ const parsed = (await response.json());
1490
+ if (parsed.errors && parsed.errors.length > 0) {
1491
+ throw new ProviderRequestError(`linear_graphql_error:${parsed.errors
1492
+ .map((error) => error.message ?? "unknown")
1493
+ .join(";")}`, 200, "provider_error");
1494
+ }
1495
+ if (!parsed.data) {
1496
+ throw new Error("linear_graphql_missing_data");
1497
+ }
1498
+ return parsed.data;
1499
+ });
1500
+ },
1501
+ };
1502
+ }
1503
+ class ProviderRequestError extends Error {
1504
+ status;
1505
+ category;
1506
+ retryAfterMs;
1507
+ constructor(message, status, category, retryAfterMs = null) {
1508
+ super(message);
1509
+ this.name = "ProviderRequestError";
1510
+ this.status = status;
1511
+ this.category = category;
1512
+ this.retryAfterMs = retryAfterMs;
1513
+ }
1514
+ }
1515
+ async function providerError(prefix, response) {
1516
+ const message = (await response.text()).slice(0, 500);
1517
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
1518
+ const category = response.status === 401 || response.status === 403
1519
+ ? response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
1520
+ ? "rate_limited"
1521
+ : "auth_error"
1522
+ : response.status === 429
1523
+ ? "rate_limited"
1524
+ : response.status >= 500
1525
+ ? "server_error"
1526
+ : "provider_error";
1527
+ return new ProviderRequestError(`${prefix}_${category}_${response.status}:${message}`, response.status, category, retryAfter);
1528
+ }
1529
+ async function requestWithRetry(maxRetries, run) {
1530
+ let attempt = 0;
1531
+ while (true) {
1532
+ try {
1533
+ return await run();
1534
+ }
1535
+ catch (error) {
1536
+ if (!(error instanceof ProviderRequestError) ||
1537
+ attempt >= maxRetries ||
1538
+ (error.category !== "rate_limited" && error.category !== "server_error")) {
1539
+ throw error;
1540
+ }
1541
+ await sleep(error.retryAfterMs ?? Math.min(250, 25 * 2 ** attempt));
1542
+ attempt += 1;
1543
+ }
1544
+ }
1545
+ }
1546
+ function sleep(ms) {
1547
+ return new Promise((resolve) => {
1548
+ setTimeout(resolve, ms);
1549
+ });
1550
+ }
1551
+ function parseRetryAfter(value) {
1552
+ if (!value) {
1553
+ return null;
1554
+ }
1555
+ const seconds = Number.parseInt(value, 10);
1556
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000 : null;
1557
+ }
1558
+ async function createGitHubBranch(client, repo, input) {
1559
+ const sha = input.sha ??
1560
+ (await client.request(`/repos/${repo.owner}/${repo.name}/branches/${encodeURIComponent(input.baseBranch)}`, { method: "GET" })).commit.sha;
1561
+ try {
1562
+ const created = await client.request(`/repos/${repo.owner}/${repo.name}/git/refs`, {
1563
+ method: "POST",
1564
+ body: {
1565
+ ref: `refs/heads/${input.branch}`,
1566
+ sha,
1567
+ },
1568
+ });
1569
+ return {
1570
+ ref: created.ref,
1571
+ sha: created.object.sha,
1572
+ alreadyExisted: false,
1573
+ };
1574
+ }
1575
+ catch (error) {
1576
+ if (input.allowExisting &&
1577
+ error instanceof ProviderRequestError &&
1578
+ error.status === 422) {
1579
+ const existing = await client.request(`/repos/${repo.owner}/${repo.name}/git/ref/heads/${encodeGitHubPath(input.branch)}`, { method: "GET" });
1580
+ return {
1581
+ ref: existing.ref,
1582
+ sha: existing.object.sha,
1583
+ alreadyExisted: true,
1584
+ };
1585
+ }
1586
+ throw error;
1587
+ }
1588
+ }
1589
+ async function writeGitHubContent(client, repo, input) {
1590
+ const response = await client.request(`/repos/${repo.owner}/${repo.name}/contents/${encodeGitHubPath(input.path)}`, {
1591
+ method: "PUT",
1592
+ body: {
1593
+ message: input.message,
1594
+ content: Buffer.from(input.content, "utf8").toString("base64"),
1595
+ branch: input.branch,
1596
+ sha: input.sha ?? undefined,
1597
+ },
1598
+ });
1599
+ return {
1600
+ path: response.content?.path ?? input.path,
1601
+ branch: input.branch,
1602
+ commitSha: response.commit.sha,
1603
+ contentSha: response.content?.sha ?? null,
1604
+ };
1605
+ }
1606
+ function signAwsRequest(input) {
1607
+ const now = new Date();
1608
+ const amzDate = toAwsAmzDate(now);
1609
+ const dateStamp = amzDate.slice(0, 8);
1610
+ const body = input.body ?? "";
1611
+ const headers = lowerCaseHeaders({
1612
+ ...(input.headers ?? {}),
1613
+ host: input.url.host,
1614
+ "x-amz-date": amzDate,
1615
+ ...(input.credentials.sessionToken
1616
+ ? { "x-amz-security-token": input.credentials.sessionToken }
1617
+ : {}),
1618
+ });
1619
+ const canonicalHeaders = Object.entries(headers)
1620
+ .sort(([left], [right]) => left.localeCompare(right))
1621
+ .map(([key, value]) => `${key}:${value.trim()}\n`)
1622
+ .join("");
1623
+ const signedHeaders = Object.keys(headers).sort().join(";");
1624
+ const canonicalRequest = [
1625
+ input.method,
1626
+ input.url.pathname || "/",
1627
+ canonicalQueryString(input.url.searchParams),
1628
+ canonicalHeaders,
1629
+ signedHeaders,
1630
+ sha256Hex(body),
1631
+ ].join("\n");
1632
+ const credentialScope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
1633
+ const stringToSign = [
1634
+ "AWS4-HMAC-SHA256",
1635
+ amzDate,
1636
+ credentialScope,
1637
+ sha256Hex(canonicalRequest),
1638
+ ].join("\n");
1639
+ const signingKey = awsSigningKey(input.credentials.secretAccessKey, dateStamp, input.region, input.service);
1640
+ const signature = hmacHex(signingKey, stringToSign);
1641
+ return {
1642
+ headers: {
1643
+ ...headers,
1644
+ authorization: [
1645
+ `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}`,
1646
+ `SignedHeaders=${signedHeaders}`,
1647
+ `Signature=${signature}`,
1648
+ ].join(", "),
1649
+ },
1650
+ };
1651
+ }
1652
+ function parseAwsS3ListBucketResult(xml) {
1653
+ return [...xml.matchAll(/<Contents>([\s\S]*?)<\/Contents>/g)].map((match) => {
1654
+ const block = match[1] ?? "";
1655
+ return {
1656
+ key: decodeXmlText(xmlText(block, "Key") ?? ""),
1657
+ size: parseOptionalNumber(xmlText(block, "Size")),
1658
+ lastModified: xmlText(block, "LastModified"),
1659
+ };
1660
+ });
1661
+ }
1662
+ function requireXmlText(xml, tag, reason) {
1663
+ return xmlText(xml, tag) ?? fail(reason);
1664
+ }
1665
+ function xmlText(xml, tag) {
1666
+ const match = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`).exec(xml);
1667
+ return match ? decodeXmlText(match[1]) : null;
1668
+ }
1669
+ function decodeXmlText(value) {
1670
+ return value
1671
+ .replace(/&lt;/g, "<")
1672
+ .replace(/&gt;/g, ">")
1673
+ .replace(/&quot;/g, '"')
1674
+ .replace(/&apos;/g, "'")
1675
+ .replace(/&amp;/g, "&");
1676
+ }
1677
+ function toAwsAmzDate(value) {
1678
+ return value.toISOString().replace(/[:-]|\.\d{3}/g, "");
1679
+ }
1680
+ function canonicalQueryString(searchParams) {
1681
+ return [...searchParams.entries()]
1682
+ .sort(([leftKey, leftValue], [rightKey, rightValue]) => leftKey === rightKey
1683
+ ? leftValue.localeCompare(rightValue)
1684
+ : leftKey.localeCompare(rightKey))
1685
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
1686
+ .join("&");
1687
+ }
1688
+ function awsSigningKey(secretAccessKey, dateStamp, region, service) {
1689
+ const dateKey = hmacBuffer(`AWS4${secretAccessKey}`, dateStamp);
1690
+ const regionKey = hmacBuffer(dateKey, region);
1691
+ const serviceKey = hmacBuffer(regionKey, service);
1692
+ return hmacBuffer(serviceKey, "aws4_request");
1693
+ }
1694
+ function lowerCaseHeaders(headers) {
1695
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
1696
+ }
1697
+ function hmacBuffer(key, value) {
1698
+ return createHmac("sha256", key).update(value).digest();
1699
+ }
1700
+ function hmacHex(key, value) {
1701
+ return createHmac("sha256", key).update(value).digest("hex");
1702
+ }
1703
+ function sha256Hex(value) {
1704
+ return createHash("sha256").update(value).digest("hex");
1705
+ }
1706
+ function parseGitHubRepoResource(resource) {
1707
+ const match = /^repo:([^/]+)\/([^/]+)$/.exec(resource);
1708
+ if (!match) {
1709
+ throw new Error(`github_rest_invalid_repo_resource:${resource}`);
1710
+ }
1711
+ return {
1712
+ owner: encodeURIComponent(match[1]),
1713
+ name: encodeURIComponent(match[2]),
1714
+ };
1715
+ }
1716
+ function encodeGitHubPath(path) {
1717
+ return path
1718
+ .split("/")
1719
+ .map((segment) => encodeURIComponent(segment))
1720
+ .join("/");
1721
+ }
1722
+ function requireString(value, reason) {
1723
+ const text = getString(value);
1724
+ if (!text) {
1725
+ throw new Error(reason);
1726
+ }
1727
+ return text;
1728
+ }
1729
+ function issueIdentifier(payload) {
1730
+ return (getString(payload.issueId) ??
1731
+ getString(payload.issueKey) ??
1732
+ fail("linear_graphql_missing_issue_identifier"));
1733
+ }
1734
+ function fail(reason) {
1735
+ throw new Error(reason);
1736
+ }
1737
+ function getBoolean(value) {
1738
+ return typeof value === "boolean" ? value : null;
1739
+ }
1740
+ function getNumber(value) {
1741
+ return typeof value === "number" ? value : null;
1742
+ }
1743
+ function parseOptionalNumber(value) {
1744
+ if (value === null) {
1745
+ return null;
1746
+ }
1747
+ const number = Number(value);
1748
+ return Number.isFinite(number) ? number : null;
1749
+ }
1750
+ function getObjectArray(value) {
1751
+ if (!Array.isArray(value)) {
1752
+ return [];
1753
+ }
1754
+ return value.filter((item) => item !== null && typeof item === "object" && !Array.isArray(item));
1755
+ }
1756
+ function removeUndefined(value) {
1757
+ return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined));
1758
+ }
1759
+ function getString(value) {
1760
+ return typeof value === "string" ? value : null;
1761
+ }
1762
+ function getStringArray(value) {
1763
+ if (!Array.isArray(value)) {
1764
+ return [];
1765
+ }
1766
+ return value.filter((item) => typeof item === "string");
1767
+ }
1768
+ //# sourceMappingURL=index.js.map