@ganju/utils 0.0.14 → 0.0.16
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/channelCustomer.d.ts +99 -0
- package/dist/channelCustomer.d.ts.map +1 -0
- package/dist/channelCustomer.js +533 -0
- package/dist/channelDebounce.d.ts +7 -0
- package/dist/channelDebounce.d.ts.map +1 -1
- package/dist/chunkOverlap.d.ts +2 -0
- package/dist/chunkOverlap.d.ts.map +1 -0
- package/dist/chunkOverlap.js +9 -0
- package/dist/chunking.d.ts.map +1 -1
- package/dist/chunking.js +24 -4
- package/dist/citations.d.ts +28 -0
- package/dist/citations.d.ts.map +1 -0
- package/dist/citations.js +111 -0
- package/dist/constants.d.ts +49 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +202 -17
- package/dist/data.d.ts +9 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +78 -0
- package/dist/dataApi.d.ts +511 -0
- package/dist/dataApi.d.ts.map +1 -0
- package/dist/dataApi.js +355 -0
- package/dist/dataCompiler.d.ts +68 -0
- package/dist/dataCompiler.d.ts.map +1 -0
- package/dist/dataCompiler.js +726 -0
- package/dist/dataDdl.d.ts +29 -0
- package/dist/dataDdl.d.ts.map +1 -0
- package/dist/dataDdl.js +354 -0
- package/dist/dataEngine.d.ts +143 -0
- package/dist/dataEngine.d.ts.map +1 -0
- package/dist/dataEngine.js +2022 -0
- package/dist/dataIdentifiers.d.ts +8 -0
- package/dist/dataIdentifiers.d.ts.map +1 -0
- package/dist/dataIdentifiers.js +55 -0
- package/dist/dataImport.d.ts +77 -0
- package/dist/dataImport.d.ts.map +1 -0
- package/dist/dataImport.js +450 -0
- package/dist/dataRpc.d.ts +131 -0
- package/dist/dataRpc.d.ts.map +1 -0
- package/dist/dataRpc.js +2 -0
- package/dist/dataTypes.d.ts +157 -0
- package/dist/dataTypes.d.ts.map +1 -0
- package/dist/dataTypes.js +160 -0
- package/dist/index.d.ts +570 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +72 -0
- package/dist/jsonSchemaToZodShape.d.ts +5 -1
- package/dist/jsonSchemaToZodShape.d.ts.map +1 -1
- package/dist/jsonSchemaToZodShape.js +21 -9
- package/dist/oauth.d.ts +14 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +42 -2
- package/dist/pagination.d.ts +47 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +160 -0
- package/dist/processQueueBatch.d.ts +6 -1
- package/dist/processQueueBatch.d.ts.map +1 -1
- package/dist/processQueueBatch.js +23 -10
- package/dist/queueFailure.d.ts +14 -0
- package/dist/queueFailure.d.ts.map +1 -0
- package/dist/queueFailure.js +43 -0
- package/dist/resourceRerank.d.ts +3 -2
- package/dist/resourceRerank.d.ts.map +1 -1
- package/dist/resourceRerank.js +8 -3
- package/dist/schema.d.ts +374 -29
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +290 -6
- package/dist/ssrf.d.ts +1 -0
- package/dist/ssrf.d.ts.map +1 -1
- package/dist/ssrf.js +48 -12
- package/dist/toolConfirmation.d.ts +48 -0
- package/dist/toolConfirmation.d.ts.map +1 -1
- package/dist/toolConfirmation.js +75 -1
- package/dist/toolCount.d.ts +13 -0
- package/dist/toolCount.d.ts.map +1 -0
- package/dist/toolCount.js +45 -0
- package/dist/toolLint.d.ts +1 -1
- package/dist/toolLint.d.ts.map +1 -1
- package/dist/toolLint.js +20 -0
- package/dist/untrustedContent.d.ts +135 -0
- package/dist/untrustedContent.d.ts.map +1 -0
- package/dist/untrustedContent.js +402 -0
- package/package.json +17 -1
- package/src/channelCustomer.ts +604 -0
- package/src/channelDebounce.ts +17 -0
- package/src/chunkOverlap.ts +6 -0
- package/src/chunking.ts +25 -4
- package/src/citations.ts +127 -0
- package/src/constants.ts +219 -17
- package/src/data.ts +74 -0
- package/src/dataApi.ts +428 -0
- package/src/dataCompiler.ts +948 -0
- package/src/dataDdl.ts +419 -0
- package/src/dataEngine.ts +2753 -0
- package/src/dataIdentifiers.ts +63 -0
- package/src/dataImport.ts +554 -0
- package/src/dataRpc.ts +121 -0
- package/src/dataTypes.ts +312 -0
- package/src/index.ts +156 -2
- package/src/jsonSchemaToZodShape.ts +31 -13
- package/src/oauth.ts +42 -1
- package/src/pagination.ts +193 -0
- package/src/processQueueBatch.ts +42 -12
- package/src/queueFailure.ts +63 -0
- package/src/resourceRerank.ts +10 -3
- package/src/schema.ts +346 -4
- package/src/ssrf.ts +40 -11
- package/src/toolConfirmation.ts +123 -0
- package/src/toolCount.ts +53 -0
- package/src/toolLint.ts +27 -0
- package/src/untrustedContent.ts +476 -0
package/src/schema.ts
CHANGED
|
@@ -3,9 +3,27 @@ import { z } from 'zod';
|
|
|
3
3
|
import { constants } from './constants';
|
|
4
4
|
import { isAllowedLlmBaseUrl, requiresLlmBaseUrl } from './llmBaseUrl';
|
|
5
5
|
import { isReservedSlug, isValidSlugFormat } from './slug';
|
|
6
|
+
import { isBlockedUrl } from './ssrf';
|
|
6
7
|
import { isReservedToolName } from './reservedToolName';
|
|
7
8
|
import { slugifyTitle } from './slugifyTitle';
|
|
8
9
|
import { TOOL_EFFECTS } from './toolConfirmation';
|
|
10
|
+
import {
|
|
11
|
+
PARTICIPANT_DAILY_LIMIT_MAX,
|
|
12
|
+
PARTICIPANT_DAILY_LIMIT_OFF
|
|
13
|
+
} from './channelCustomer';
|
|
14
|
+
import {
|
|
15
|
+
DATA_ACCESS_LEVELS,
|
|
16
|
+
DATA_COLUMN_TYPES,
|
|
17
|
+
DATA_MAX_COLUMNS,
|
|
18
|
+
DATA_MAX_DESCRIPTION,
|
|
19
|
+
DATA_MAX_ENUM_VALUES,
|
|
20
|
+
DATA_MAX_INSERT_ROWS,
|
|
21
|
+
DATA_MAX_LABEL,
|
|
22
|
+
DATA_IMPACT_MAX_IDS,
|
|
23
|
+
DATA_MAX_PAGE,
|
|
24
|
+
DATA_MAX_WRITE_OPERATIONS,
|
|
25
|
+
DATA_ON_DELETE
|
|
26
|
+
} from './dataTypes';
|
|
9
27
|
|
|
10
28
|
// A prompt title becomes a slash command; it must not collide with a command
|
|
11
29
|
// the channel runner handles itself (e.g. `/link`).
|
|
@@ -181,6 +199,15 @@ const INVITATION_RESPOND = z.object({
|
|
|
181
199
|
action: z.enum(constants.INVITATION_RESPONSES)
|
|
182
200
|
});
|
|
183
201
|
|
|
202
|
+
const TOOL_APPROVAL_GET = z.object({
|
|
203
|
+
approvalId: z.uuid()
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const TOOL_APPROVAL_DECIDE = z.object({
|
|
207
|
+
approvalId: z.uuid(),
|
|
208
|
+
decision: z.enum(constants.TOOL_APPROVAL_DECISIONS)
|
|
209
|
+
});
|
|
210
|
+
|
|
184
211
|
const INVITATION_GET_BY_TOKEN = z.object({
|
|
185
212
|
token: z.string().min(8).max(128)
|
|
186
213
|
});
|
|
@@ -279,6 +306,10 @@ const ARTIFACT_CREATE_WEBSITE_VIEW = z.object({
|
|
|
279
306
|
.refine(
|
|
280
307
|
v => /^https?:\/\//i.test(v),
|
|
281
308
|
'Only http and https URLs are supported'
|
|
309
|
+
)
|
|
310
|
+
.refine(
|
|
311
|
+
v => !isBlockedUrl(v),
|
|
312
|
+
"This address points to a private network and can't be crawled"
|
|
282
313
|
),
|
|
283
314
|
description: z.string().optional(),
|
|
284
315
|
maxPages: z.number().int().min(1).max(constants.CRAWL_MAX_PAGES_LIMIT),
|
|
@@ -292,6 +323,10 @@ const ARTIFACT_CREATE_WEBSITE = z.object({
|
|
|
292
323
|
.refine(
|
|
293
324
|
v => /^https?:\/\//i.test(v),
|
|
294
325
|
'Only http and https URLs are supported'
|
|
326
|
+
)
|
|
327
|
+
.refine(
|
|
328
|
+
v => !isBlockedUrl(v),
|
|
329
|
+
"This address points to a private network and can't be crawled"
|
|
295
330
|
),
|
|
296
331
|
description: z.string().optional(),
|
|
297
332
|
crawlConfig: z.object({
|
|
@@ -489,6 +524,214 @@ const ARTIFACT_REMOVE_RESOURCE = z.object({
|
|
|
489
524
|
organizationId: z.uuid()
|
|
490
525
|
});
|
|
491
526
|
|
|
527
|
+
const DATA_DATABASE_GET = z.object({
|
|
528
|
+
projectId: z.uuid(),
|
|
529
|
+
userId: z.uuid(),
|
|
530
|
+
organizationId: z.uuid()
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// The name is what the owner types to delete it; it defaults to the
|
|
534
|
+
// project's slug. `replaceDeleted` starts over while a deleted database is
|
|
535
|
+
// still in its grace period, giving up the chance to restore it.
|
|
536
|
+
const DATA_DATABASE_CREATE = DATA_DATABASE_GET.extend({
|
|
537
|
+
name: z.string().trim().min(1).max(100).optional(),
|
|
538
|
+
replaceDeleted: z.boolean().optional()
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const DATA_DATABASE_DELETE = DATA_DATABASE_GET.extend({
|
|
542
|
+
confirmName: z.string().min(1)
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// The bodies and query strings of the Data API. Each one is what a client
|
|
546
|
+
// sends; the route schemas below add the path's ids. The database object
|
|
547
|
+
// checks every rule again (names, types, relations), so these describe the
|
|
548
|
+
// shape and leave the meaning to it.
|
|
549
|
+
const DATA_IDENTIFIER = z.string().min(1).max(63);
|
|
550
|
+
const DATA_DEFAULT_VALUE = z.union([
|
|
551
|
+
z.string(),
|
|
552
|
+
z.number(),
|
|
553
|
+
z.boolean(),
|
|
554
|
+
z.null()
|
|
555
|
+
]);
|
|
556
|
+
const DATA_TEXT = (max: number) => z.string().max(max).nullable().optional();
|
|
557
|
+
|
|
558
|
+
const DATA_COLUMN_INPUT = z.strictObject({
|
|
559
|
+
name: DATA_IDENTIFIER,
|
|
560
|
+
type: z.enum(DATA_COLUMN_TYPES),
|
|
561
|
+
label: DATA_TEXT(DATA_MAX_LABEL),
|
|
562
|
+
description: DATA_TEXT(DATA_MAX_DESCRIPTION),
|
|
563
|
+
required: z.boolean().optional(),
|
|
564
|
+
unique: z.boolean().optional(),
|
|
565
|
+
filterable: z.boolean().optional(),
|
|
566
|
+
searchable: z.boolean().optional(),
|
|
567
|
+
personal: z.boolean().optional(),
|
|
568
|
+
values: z.array(z.string().min(1)).max(DATA_MAX_ENUM_VALUES).optional(),
|
|
569
|
+
table: DATA_IDENTIFIER.optional(),
|
|
570
|
+
onDelete: z.enum(DATA_ON_DELETE).optional(),
|
|
571
|
+
min: z.number().nullable().optional(),
|
|
572
|
+
default: DATA_DEFAULT_VALUE.optional()
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// The CLI's schema file holds a list of these.
|
|
576
|
+
const DATA_TABLE_INPUT = z.strictObject({
|
|
577
|
+
name: DATA_IDENTIFIER,
|
|
578
|
+
label: DATA_TEXT(DATA_MAX_LABEL),
|
|
579
|
+
description: DATA_TEXT(DATA_MAX_DESCRIPTION),
|
|
580
|
+
displayColumn: DATA_IDENTIFIER.nullable().optional(),
|
|
581
|
+
access: z.enum(DATA_ACCESS_LEVELS).optional(),
|
|
582
|
+
customerScoped: z.boolean().optional(),
|
|
583
|
+
history: z.boolean().optional(),
|
|
584
|
+
columns: z.array(DATA_COLUMN_INPUT).max(DATA_MAX_COLUMNS)
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const DATA_TABLE_PATCH = DATA_TABLE_INPUT.omit({ columns: true }).partial();
|
|
588
|
+
|
|
589
|
+
// A null puts the setting back to its default.
|
|
590
|
+
const DATA_COLUMN_PATCH = DATA_COLUMN_INPUT.partial();
|
|
591
|
+
|
|
592
|
+
const DATA_TABLE_DROP_BODY = z.strictObject({ confirmName: z.string().min(1) });
|
|
593
|
+
|
|
594
|
+
const DATA_ROW_VALUES = z.record(z.string(), z.unknown());
|
|
595
|
+
|
|
596
|
+
// A JSON value carried in a query string, e.g. `where={"status":"open"}`.
|
|
597
|
+
const DATA_JSON_PARAM = z.string().transform((text, ctx) => {
|
|
598
|
+
try {
|
|
599
|
+
return JSON.parse(text) as unknown;
|
|
600
|
+
} catch {
|
|
601
|
+
ctx.addIssue({
|
|
602
|
+
code: 'custom',
|
|
603
|
+
message: 'This is JSON, e.g. {"status":"open"}'
|
|
604
|
+
});
|
|
605
|
+
return z.NEVER;
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// A comma-separated list in a query string: `sort=-created_at,name`.
|
|
610
|
+
const DATA_LIST_PARAM = z.string().transform(text =>
|
|
611
|
+
text
|
|
612
|
+
.split(',')
|
|
613
|
+
.map(item => item.trim())
|
|
614
|
+
.filter(Boolean)
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
const DATA_ROW_LIST_QUERY = z.strictObject({
|
|
618
|
+
where: DATA_JSON_PARAM.optional(),
|
|
619
|
+
sort: DATA_LIST_PARAM.optional(),
|
|
620
|
+
include: DATA_LIST_PARAM.optional(),
|
|
621
|
+
limit: z.coerce.number().int().min(1).max(DATA_MAX_PAGE).optional(),
|
|
622
|
+
cursor: z.string().max(2000).optional(),
|
|
623
|
+
q: z.string().max(500).optional(),
|
|
624
|
+
timeZone: z.string().max(64).optional()
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const DATA_ROW_GET_QUERY = DATA_ROW_LIST_QUERY.pick({
|
|
628
|
+
include: true,
|
|
629
|
+
timeZone: true
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const DATA_ROW_CREATE_BODY = z.union([
|
|
633
|
+
z.strictObject({ values: DATA_ROW_VALUES }),
|
|
634
|
+
z.strictObject({
|
|
635
|
+
rows: z.array(DATA_ROW_VALUES).min(1).max(DATA_MAX_INSERT_ROWS)
|
|
636
|
+
})
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
const DATA_ROW_VERSION = z.number().int().min(1);
|
|
640
|
+
|
|
641
|
+
// `set` takes values, or relative changes such as { "stock": { "decrement": 2 } }.
|
|
642
|
+
const DATA_ROW_UPDATE_BODY = z.strictObject({
|
|
643
|
+
set: DATA_ROW_VALUES,
|
|
644
|
+
version: DATA_ROW_VERSION.optional()
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const DATA_ROW_DELETE_BODY = z.strictObject({
|
|
648
|
+
version: DATA_ROW_VERSION.optional()
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const DATA_ROW_ID = z.coerce.number().int().min(1);
|
|
652
|
+
|
|
653
|
+
// Without `ids`, every row of the table.
|
|
654
|
+
const DATA_DELETE_PREVIEW_BODY = z.strictObject({
|
|
655
|
+
ids: z.array(z.number().int().min(1)).min(1).max(DATA_IMPACT_MAX_IDS).optional()
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const DATA_WRITE_TARGET = {
|
|
659
|
+
table: DATA_IDENTIFIER,
|
|
660
|
+
id: z
|
|
661
|
+
.union([
|
|
662
|
+
z.number().int().min(1),
|
|
663
|
+
z.strictObject({ $ref: z.number().int().min(0) })
|
|
664
|
+
])
|
|
665
|
+
.optional(),
|
|
666
|
+
where: z.record(z.string(), z.unknown()).optional(),
|
|
667
|
+
expect: z.number().int().min(0).optional(),
|
|
668
|
+
version: DATA_ROW_VERSION.optional()
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Applied together or not at all. A later step can use an earlier insert's
|
|
672
|
+
// id as { "$ref": <step> }.
|
|
673
|
+
const DATA_WRITE_OPERATION = z.discriminatedUnion('op', [
|
|
674
|
+
z.strictObject({
|
|
675
|
+
op: z.literal('insert'),
|
|
676
|
+
table: DATA_IDENTIFIER,
|
|
677
|
+
values: DATA_ROW_VALUES.optional(),
|
|
678
|
+
rows: z.array(DATA_ROW_VALUES).min(1).max(DATA_MAX_INSERT_ROWS).optional()
|
|
679
|
+
}),
|
|
680
|
+
z.strictObject({
|
|
681
|
+
op: z.literal('update'),
|
|
682
|
+
...DATA_WRITE_TARGET,
|
|
683
|
+
set: DATA_ROW_VALUES
|
|
684
|
+
}),
|
|
685
|
+
z.strictObject({ op: z.literal('delete'), ...DATA_WRITE_TARGET })
|
|
686
|
+
]);
|
|
687
|
+
|
|
688
|
+
const DATA_WRITE_BODY = z.strictObject({
|
|
689
|
+
operations: z
|
|
690
|
+
.array(DATA_WRITE_OPERATION)
|
|
691
|
+
.min(1)
|
|
692
|
+
.max(DATA_MAX_WRITE_OPERATIONS)
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const DATA_ROW_HISTORY_QUERY = z.strictObject({
|
|
696
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
697
|
+
cursor: z.coerce.number().int().min(1).optional(),
|
|
698
|
+
timeZone: z.string().max(64).optional()
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// A time within the last 30 days, or a bookmark this API handed out (a drop,
|
|
702
|
+
// a type change, or the restore to undo).
|
|
703
|
+
const DATA_RESTORE_POINT_BODY = z.union([
|
|
704
|
+
z.strictObject({
|
|
705
|
+
timestamp: z.union([z.iso.datetime({ offset: true }), z.number().int()])
|
|
706
|
+
}),
|
|
707
|
+
z.strictObject({ bookmark: z.string().min(1).max(200) })
|
|
708
|
+
]);
|
|
709
|
+
|
|
710
|
+
const DATA_EXPORT_BODY = z.strictObject({
|
|
711
|
+
format: z.enum(['jsonl', 'csv']).optional()
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
const DATA_EXPORT_QUERY = z.strictObject({
|
|
715
|
+
exportId: z.uuid().optional()
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Without `table`, the export's schema.json.
|
|
719
|
+
const DATA_EXPORT_FILE_QUERY = z.strictObject({
|
|
720
|
+
exportId: z.uuid(),
|
|
721
|
+
table: DATA_IDENTIFIER.optional()
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const DATA_CHANGE_LIST_QUERY = z.strictObject({
|
|
725
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
726
|
+
cursor: z.uuid().optional()
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const DATA_TABLE_PARAMS = DATA_DATABASE_GET.extend({ table: DATA_IDENTIFIER });
|
|
730
|
+
const DATA_COLUMN_PARAMS = DATA_TABLE_PARAMS.extend({
|
|
731
|
+
column: DATA_IDENTIFIER
|
|
732
|
+
});
|
|
733
|
+
const DATA_ROW_PARAMS = DATA_TABLE_PARAMS.extend({ id: DATA_ROW_ID });
|
|
734
|
+
|
|
492
735
|
const ARTIFACT_UPLOAD_RESOURCE_FILE = z.object({
|
|
493
736
|
resourceId: z.uuid(),
|
|
494
737
|
projectId: z.uuid(),
|
|
@@ -722,11 +965,11 @@ const ORGANIZATION_UPDATE_LLM_VIEW = refineUpdateBaseUrl(
|
|
|
722
965
|
})
|
|
723
966
|
);
|
|
724
967
|
|
|
725
|
-
//
|
|
726
|
-
//
|
|
968
|
+
// The channel settings an owner can write. Strict: a typo or a setting that
|
|
969
|
+
// no longer exists is refused rather than stored, where nothing would read it.
|
|
727
970
|
// `debounceMs` is how long a burst from one participant is buffered before the
|
|
728
971
|
// agent answers it as a single turn; 0 disables buffering.
|
|
729
|
-
const CHANNEL_CONFIG = z.
|
|
972
|
+
const CHANNEL_CONFIG = z.strictObject({
|
|
730
973
|
debounceMs: z
|
|
731
974
|
.number()
|
|
732
975
|
.int()
|
|
@@ -737,7 +980,16 @@ const CHANNEL_CONFIG = z.looseObject({
|
|
|
737
980
|
value <= constants.CHANNEL_DEBOUNCE_MAX_MS),
|
|
738
981
|
`debounceMs must be 0 (disabled) or between ${constants.CHANNEL_DEBOUNCE_MIN_MS} and ${constants.CHANNEL_DEBOUNCE_MAX_MS} ms`
|
|
739
982
|
)
|
|
740
|
-
.optional()
|
|
983
|
+
.optional(),
|
|
984
|
+
// Replies one customer gets in 24 hours; 0 turns the limit off.
|
|
985
|
+
participantDailyLimit: z
|
|
986
|
+
.number()
|
|
987
|
+
.int()
|
|
988
|
+
.min(PARTICIPANT_DAILY_LIMIT_OFF)
|
|
989
|
+
.max(PARTICIPANT_DAILY_LIMIT_MAX)
|
|
990
|
+
.optional(),
|
|
991
|
+
// Remember each customer's name, address, allergies and dates.
|
|
992
|
+
participantMemory: z.boolean().optional()
|
|
741
993
|
});
|
|
742
994
|
|
|
743
995
|
const CHANNEL_CREATE = z.object({
|
|
@@ -754,6 +1006,9 @@ const CHANNEL_UPDATE = z.object({
|
|
|
754
1006
|
channelId: z.uuid(),
|
|
755
1007
|
status: z.enum(constants.CHANNEL_STATUS).optional(),
|
|
756
1008
|
config: CHANNEL_CONFIG.optional(),
|
|
1009
|
+
// Keys to change on the stored config, merged by the database, so two
|
|
1010
|
+
// settings saved at once don't overwrite each other.
|
|
1011
|
+
configPatch: CHANNEL_CONFIG.optional(),
|
|
757
1012
|
credentials: z.record(z.string(), z.string()).optional(),
|
|
758
1013
|
llmId: z.uuid().nullable().optional(),
|
|
759
1014
|
projectId: z.uuid(),
|
|
@@ -975,6 +1230,15 @@ const MCP_PROXY_CONFIG = z.object({
|
|
|
975
1230
|
allowedResources: z.array(z.string().min(1).max(2048)).max(500).optional(),
|
|
976
1231
|
allowedPrompts: z.array(z.string().min(1).max(256)).max(500).optional(),
|
|
977
1232
|
auth: MCP_PROXY_AUTH.default({ kind: constants.MCP_PROXY_AUTH_KIND_NONE }),
|
|
1233
|
+
// Whether the remote's own readOnlyHint / destructiveHint decide which of its
|
|
1234
|
+
// tools are confirmed. Off (or absent), every proxied tool counts as
|
|
1235
|
+
// sensitive unless the owner marked it below: a server that labels its
|
|
1236
|
+
// delete "read-only" would otherwise skip the question.
|
|
1237
|
+
trustAnnotations: z.boolean().optional(),
|
|
1238
|
+
// Remote tool names the owner marked as only reading. Read-only whatever the
|
|
1239
|
+
// remote says, trusted or not. Left optional rather than defaulted, so a
|
|
1240
|
+
// save that doesn't mention it keeps the stored marks.
|
|
1241
|
+
readOnlyTools: z.array(z.string().min(1).max(256)).max(500).optional(),
|
|
978
1242
|
timeoutMs: z
|
|
979
1243
|
.number()
|
|
980
1244
|
.int()
|
|
@@ -1247,6 +1511,53 @@ const ARTIFACT_CUSTOM_CODE_LIST_LOGS = z.object({
|
|
|
1247
1511
|
organizationId: z.uuid()
|
|
1248
1512
|
});
|
|
1249
1513
|
|
|
1514
|
+
// Tool observability on the project home: per-tool health over a window.
|
|
1515
|
+
const PROJECT_TOOL_HEALTH = z.object({
|
|
1516
|
+
days: z.coerce
|
|
1517
|
+
.number()
|
|
1518
|
+
.int()
|
|
1519
|
+
.refine(days => constants.OBSERVABILITY_RANGE_DAYS.includes(days), {
|
|
1520
|
+
message: 'Range is not valid'
|
|
1521
|
+
})
|
|
1522
|
+
.default(7),
|
|
1523
|
+
projectId: z.uuid(),
|
|
1524
|
+
userId: z.uuid(),
|
|
1525
|
+
organizationId: z.uuid()
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
// The calls behind a row of that table, newest first. Paged by the id of the
|
|
1529
|
+
// last call shown, not its time: calls written in one insert share a
|
|
1530
|
+
// timestamp, and the client only sees it to the millisecond.
|
|
1531
|
+
const PROJECT_TOOL_CALLS = z.object({
|
|
1532
|
+
tool: z.string().min(1).max(256).optional(),
|
|
1533
|
+
status: z.enum(['all', 'error']).default('all'),
|
|
1534
|
+
limit: z.coerce
|
|
1535
|
+
.number()
|
|
1536
|
+
.int()
|
|
1537
|
+
.min(1)
|
|
1538
|
+
.max(constants.OBSERVABILITY_CALLS_MAX_LIMIT)
|
|
1539
|
+
.default(constants.OBSERVABILITY_CALLS_DEFAULT_LIMIT),
|
|
1540
|
+
after: z.uuid().optional(),
|
|
1541
|
+
projectId: z.uuid(),
|
|
1542
|
+
userId: z.uuid(),
|
|
1543
|
+
organizationId: z.uuid()
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
// One recorded call, whole, and the session timeline it belongs to.
|
|
1547
|
+
const PROJECT_TOOL_CALL = z.object({
|
|
1548
|
+
requestId: z.uuid(),
|
|
1549
|
+
projectId: z.uuid(),
|
|
1550
|
+
userId: z.uuid(),
|
|
1551
|
+
organizationId: z.uuid()
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
const PROJECT_MCP_SESSION = z.object({
|
|
1555
|
+
sessionId: z.uuid(),
|
|
1556
|
+
projectId: z.uuid(),
|
|
1557
|
+
userId: z.uuid(),
|
|
1558
|
+
organizationId: z.uuid()
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1250
1561
|
// Reads the artifact's version history and which one is currently active.
|
|
1251
1562
|
const ARTIFACT_CUSTOM_CODE_LIST_VERSIONS = z.object({
|
|
1252
1563
|
projectId: z.uuid(),
|
|
@@ -1609,6 +1920,8 @@ export const Schema = {
|
|
|
1609
1920
|
PROJECT_INVITATION_REMOVE,
|
|
1610
1921
|
INVITATION_RESPOND,
|
|
1611
1922
|
INVITATION_GET_BY_TOKEN,
|
|
1923
|
+
TOOL_APPROVAL_GET,
|
|
1924
|
+
TOOL_APPROVAL_DECIDE,
|
|
1612
1925
|
ORGANIZATION_MEMBER_LIST,
|
|
1613
1926
|
ORGANIZATION_MEMBER_REMOVE,
|
|
1614
1927
|
PROJECT_MEMBER_LIST,
|
|
@@ -1635,6 +1948,31 @@ export const Schema = {
|
|
|
1635
1948
|
ARTIFACT_UPDATE_RESOURCE_VIEW,
|
|
1636
1949
|
ARTIFACT_GET_RESOURCE,
|
|
1637
1950
|
ARTIFACT_GET_RESOURCE_BY_ID,
|
|
1951
|
+
DATA_DATABASE_GET,
|
|
1952
|
+
DATA_DATABASE_CREATE,
|
|
1953
|
+
DATA_DATABASE_DELETE,
|
|
1954
|
+
DATA_COLUMN_INPUT,
|
|
1955
|
+
DATA_TABLE_INPUT,
|
|
1956
|
+
DATA_TABLE_PATCH,
|
|
1957
|
+
DATA_COLUMN_PATCH,
|
|
1958
|
+
DATA_TABLE_DROP_BODY,
|
|
1959
|
+
DATA_ROW_LIST_QUERY,
|
|
1960
|
+
DATA_ROW_GET_QUERY,
|
|
1961
|
+
DATA_ROW_CREATE_BODY,
|
|
1962
|
+
DATA_ROW_UPDATE_BODY,
|
|
1963
|
+
DATA_ROW_DELETE_BODY,
|
|
1964
|
+
DATA_DELETE_PREVIEW_BODY,
|
|
1965
|
+
DATA_WRITE_OPERATION,
|
|
1966
|
+
DATA_WRITE_BODY,
|
|
1967
|
+
DATA_ROW_HISTORY_QUERY,
|
|
1968
|
+
DATA_RESTORE_POINT_BODY,
|
|
1969
|
+
DATA_EXPORT_BODY,
|
|
1970
|
+
DATA_EXPORT_QUERY,
|
|
1971
|
+
DATA_EXPORT_FILE_QUERY,
|
|
1972
|
+
DATA_CHANGE_LIST_QUERY,
|
|
1973
|
+
DATA_TABLE_PARAMS,
|
|
1974
|
+
DATA_COLUMN_PARAMS,
|
|
1975
|
+
DATA_ROW_PARAMS,
|
|
1638
1976
|
ARTIFACT_REMOVE_RESOURCE,
|
|
1639
1977
|
ARTIFACT_UPLOAD_RESOURCE_FILE,
|
|
1640
1978
|
ARTIFACT_DOWNLOAD_RESOURCE_FILE,
|
|
@@ -1688,6 +2026,10 @@ export const Schema = {
|
|
|
1688
2026
|
ARTIFACT_CUSTOM_CODE_ROLLBACK,
|
|
1689
2027
|
ARTIFACT_CUSTOM_CODE_LIST_LOGS,
|
|
1690
2028
|
ARTIFACT_CUSTOM_CODE_LIST_VERSIONS,
|
|
2029
|
+
PROJECT_TOOL_HEALTH,
|
|
2030
|
+
PROJECT_TOOL_CALLS,
|
|
2031
|
+
PROJECT_TOOL_CALL,
|
|
2032
|
+
PROJECT_MCP_SESSION,
|
|
1691
2033
|
CUSTOM_CODE_INVOKE_REQUEST,
|
|
1692
2034
|
CUSTOM_CODE_INVOKE_RESPONSE,
|
|
1693
2035
|
CUSTOM_CODE_BROKER_CONNECTION,
|
package/src/ssrf.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Literal-host SSRF screening shared by the proxied tool dispatchers
|
|
2
|
-
// (http-endpoint, mcp-proxy). The Workers runtime can't resolve DNS, so we can
|
|
2
|
+
// (http-endpoint, mcp-proxy) and the website crawler. The Workers runtime can't resolve DNS, so we can
|
|
3
3
|
// only screen literal hosts and IPs (not DNS rebinding). Private/loopback/
|
|
4
4
|
// link-local ranges are always rejected; callers may further narrow with an
|
|
5
5
|
// allowlist.
|
|
@@ -13,22 +13,51 @@ export const ipv4InPrivateRange = (host: string): boolean => {
|
|
|
13
13
|
if (a === 169 && b === 254) return true; // link-local (incl. cloud metadata)
|
|
14
14
|
if (a === 192 && b === 168) return true; // private
|
|
15
15
|
if (a === 172 && b >= 16 && b <= 31) return true; // private
|
|
16
|
+
if (a === 100 && b >= 64 && b <= 127) return true; // carrier-grade NAT
|
|
16
17
|
return false;
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// The IPv4 address an IPv4-mapped IPv6 literal reaches. URL parsing rewrites
|
|
21
|
+
// `[::ffff:127.0.0.1]` to `[::ffff:7f00:1]`, so both spellings are read.
|
|
22
|
+
const mappedIpv4 = (host: string): string | null => {
|
|
23
|
+
if (!host.startsWith('::ffff:')) return null;
|
|
24
|
+
const rest = host.slice('::ffff:'.length);
|
|
25
|
+
if (rest.includes('.')) return rest;
|
|
26
|
+
const match = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(rest);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const hi = Number.parseInt(match[1], 16);
|
|
29
|
+
const lo = Number.parseInt(match[2], 16);
|
|
30
|
+
return [hi >> 8, hi & 0xff, lo >> 8, lo & 0xff].join('.');
|
|
31
|
+
};
|
|
32
|
+
|
|
19
33
|
export const isBlockedHost = (hostname: string): boolean => {
|
|
20
|
-
const host = hostname
|
|
34
|
+
const host = hostname
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/^\[|\]$/g, '')
|
|
37
|
+
.replace(/\.$/, '');
|
|
21
38
|
if (!host) return true;
|
|
22
39
|
if (host === 'localhost' || host.endsWith('.localhost')) return true;
|
|
23
|
-
if (host
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
host
|
|
27
|
-
|
|
28
|
-
host
|
|
29
|
-
|
|
40
|
+
if (host.includes(':')) {
|
|
41
|
+
// An IPv6 literal. Its prefixes are only checked here: a domain name such
|
|
42
|
+
// as fdic.gov or fcbarcelona.com starts with the same letters.
|
|
43
|
+
if (host === '::1' || host === '::') return true;
|
|
44
|
+
// Link-local (fe80::/10) and unique-local (fc00::/7).
|
|
45
|
+
if (/^fe[89ab]/.test(host) || /^f[cd]/.test(host)) return true;
|
|
46
|
+
const mapped = mappedIpv4(host);
|
|
47
|
+
return mapped !== null && ipv4InPrivateRange(mapped);
|
|
48
|
+
}
|
|
49
|
+
return ipv4InPrivateRange(host);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Whether a URL may not be fetched on a user's behalf: not http(s), not a URL
|
|
53
|
+
// at all, or pointing at a private, loopback or link-local host.
|
|
54
|
+
export const isBlockedUrl = (url: string): boolean => {
|
|
55
|
+
let parsed: URL;
|
|
56
|
+
try {
|
|
57
|
+
parsed = new URL(url);
|
|
58
|
+
} catch {
|
|
30
59
|
return true;
|
|
31
60
|
}
|
|
32
|
-
if (
|
|
33
|
-
return
|
|
61
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return true;
|
|
62
|
+
return isBlockedHost(parsed.hostname);
|
|
34
63
|
};
|
package/src/toolConfirmation.ts
CHANGED
|
@@ -274,6 +274,94 @@ export const isSensitiveTool = (
|
|
|
274
274
|
): boolean =>
|
|
275
275
|
annotations?.readOnlyHint !== true && annotations?.destructiveHint !== false;
|
|
276
276
|
|
|
277
|
+
// How far a proxied tool's own hints are believed, from its install's config:
|
|
278
|
+
// the remote's read-only / destructive hints only when the owner chose to
|
|
279
|
+
// trust that server, and the owner's own "only reads" mark on a tool always.
|
|
280
|
+
export interface ProxiedToolTrust {
|
|
281
|
+
trustHints: boolean;
|
|
282
|
+
readOnly: boolean;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const proxiedToolTrust = (
|
|
286
|
+
config:
|
|
287
|
+
| { trustAnnotations?: unknown; readOnlyTools?: unknown }
|
|
288
|
+
| null
|
|
289
|
+
| undefined,
|
|
290
|
+
remoteToolName: string
|
|
291
|
+
): ProxiedToolTrust => ({
|
|
292
|
+
trustHints: config?.trustAnnotations === true,
|
|
293
|
+
readOnly:
|
|
294
|
+
Array.isArray(config?.readOnlyTools) &&
|
|
295
|
+
config.readOnlyTools.includes(remoteToolName)
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The annotations a proxied tool is registered with. A remote server's hints
|
|
300
|
+
* are its own claim about itself, and one that marks a delete read-only would
|
|
301
|
+
* skip every confirmation — so unless the organization chose to trust that
|
|
302
|
+
* server, the read-only and destructive hints are dropped and the tool reads
|
|
303
|
+
* as sensitive by the defaults above. A tool the owner marked as only reading
|
|
304
|
+
* is read-only whatever the remote says, trusted or not. Clients see the
|
|
305
|
+
* same, so they ask about the same tools. The harmless parts (title,
|
|
306
|
+
* openWorldHint, idempotentHint) pass through.
|
|
307
|
+
*/
|
|
308
|
+
export const proxiedToolAnnotations = <
|
|
309
|
+
T extends { readOnlyHint?: boolean; destructiveHint?: boolean }
|
|
310
|
+
>(
|
|
311
|
+
annotations: T | null | undefined,
|
|
312
|
+
trust: ProxiedToolTrust
|
|
313
|
+
): Partial<T> | undefined => {
|
|
314
|
+
const result = { ...annotations } as Partial<T>;
|
|
315
|
+
if (trust.readOnly) {
|
|
316
|
+
delete result.destructiveHint;
|
|
317
|
+
return { ...result, readOnlyHint: true };
|
|
318
|
+
}
|
|
319
|
+
if (!trust.trustHints) {
|
|
320
|
+
delete result.readOnlyHint;
|
|
321
|
+
delete result.destructiveHint;
|
|
322
|
+
}
|
|
323
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// The install-config keys that hold the owner's trust marks above.
|
|
327
|
+
const PROXIED_TOOL_MARK_KEYS = ['trustAnnotations', 'readOnlyTools'] as const;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* An install's new config with the owner's trust marks carried over from the
|
|
331
|
+
* stored one wherever the new one doesn't say. A save that only knew about
|
|
332
|
+
* the allow-lists (an older dashboard, a script) must not quietly turn the
|
|
333
|
+
* marks off; one that sends them, even as false or empty, decides.
|
|
334
|
+
*/
|
|
335
|
+
export const keepProxiedToolMarks = (
|
|
336
|
+
stored: unknown,
|
|
337
|
+
incoming: unknown
|
|
338
|
+
): unknown => {
|
|
339
|
+
if (
|
|
340
|
+
!stored ||
|
|
341
|
+
typeof stored !== 'object' ||
|
|
342
|
+
!incoming ||
|
|
343
|
+
typeof incoming !== 'object'
|
|
344
|
+
) {
|
|
345
|
+
return incoming;
|
|
346
|
+
}
|
|
347
|
+
const merged = { ...(incoming as Record<string, unknown>) };
|
|
348
|
+
for (const key of PROXIED_TOOL_MARK_KEYS) {
|
|
349
|
+
if (merged[key] === undefined && key in stored) {
|
|
350
|
+
merged[key] = (stored as Record<string, unknown>)[key];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return merged;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/** Whether a proxied tool is confirmed, decided as its registration is. */
|
|
357
|
+
export const isSensitiveProxiedTool = (
|
|
358
|
+
annotations:
|
|
359
|
+
| { readOnlyHint?: boolean; destructiveHint?: boolean }
|
|
360
|
+
| null
|
|
361
|
+
| undefined,
|
|
362
|
+
trust: ProxiedToolTrust
|
|
363
|
+
): boolean => isSensitiveTool(proxiedToolAnnotations(annotations, trust));
|
|
364
|
+
|
|
277
365
|
// JSON with keys sorted at every depth, so two argument objects that say the
|
|
278
366
|
// same thing serialize the same whatever order the model emitted them in.
|
|
279
367
|
export const stableJson = (value: unknown): string => {
|
|
@@ -289,3 +377,38 @@ export const stableJson = (value: unknown): string => {
|
|
|
289
377
|
}
|
|
290
378
|
return JSON.stringify(value) ?? 'null';
|
|
291
379
|
};
|
|
380
|
+
|
|
381
|
+
export type ToolApprovalStatus =
|
|
382
|
+
| 'pending'
|
|
383
|
+
| 'approved'
|
|
384
|
+
| 'denied'
|
|
385
|
+
| 'used'
|
|
386
|
+
| 'expired';
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Where an approval stands. Spent and refused come first, since an answer
|
|
390
|
+
* stays the answer after the window closes; past its time, one never answered
|
|
391
|
+
* or never used can no longer be.
|
|
392
|
+
*/
|
|
393
|
+
export const toolApprovalStatus = (
|
|
394
|
+
approval: {
|
|
395
|
+
expiresAt: Date;
|
|
396
|
+
approvedAt: Date | null;
|
|
397
|
+
deniedAt: Date | null;
|
|
398
|
+
usedAt: Date | null;
|
|
399
|
+
},
|
|
400
|
+
now: number = Date.now()
|
|
401
|
+
): ToolApprovalStatus => {
|
|
402
|
+
if (approval.usedAt) return 'used';
|
|
403
|
+
if (approval.deniedAt) return 'denied';
|
|
404
|
+
if (approval.expiresAt.getTime() <= now) return 'expired';
|
|
405
|
+
return approval.approvedAt ? 'approved' : 'pending';
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/** The dashboard page where the user answers one approval. */
|
|
409
|
+
export const toolApprovalUrl = (
|
|
410
|
+
webUrl: string,
|
|
411
|
+
path: string,
|
|
412
|
+
approvalId: string
|
|
413
|
+
): string =>
|
|
414
|
+
`${webUrl.replace(/\/+$/, '')}${path}/${encodeURIComponent(approvalId)}`;
|
package/src/toolCount.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// How many tools an install puts in front of a model, which is what the tool
|
|
2
|
+
// limit counts. Imports nothing, so node --test runs it directly.
|
|
3
|
+
|
|
4
|
+
// The same values as constants.TOOL_DEFINITION_KEY_MCP_PROXY and
|
|
5
|
+
// TOOL_DEFINITION_KEY_CUSTOM_CODE, written out rather than imported, since
|
|
6
|
+
// this file imports nothing.
|
|
7
|
+
const MCP_PROXY_TOOL_KEY = 'mcp-proxy';
|
|
8
|
+
const CUSTOM_CODE_TOOL_KEY = 'custom-code';
|
|
9
|
+
|
|
10
|
+
// The tools an allow-list keeps out of `names`. An empty or absent list keeps
|
|
11
|
+
// all of them, as the MCP boot loop reads it.
|
|
12
|
+
const allowedCount = (names: string[], config: unknown): number => {
|
|
13
|
+
const allowedTools = (config as { allowedTools?: unknown } | null)
|
|
14
|
+
?.allowedTools;
|
|
15
|
+
if (!Array.isArray(allowedTools) || allowedTools.length === 0) {
|
|
16
|
+
return names.length;
|
|
17
|
+
}
|
|
18
|
+
const allowed = new Set(allowedTools);
|
|
19
|
+
return names.filter(name => allowed.has(name)).length;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An mcp-proxy row exposes each discovered remote tool its allow-list keeps,
|
|
24
|
+
* so a single GitHub install can be dozens of tools. A custom-code row exposes
|
|
25
|
+
* the functions of its published version the allow-list keeps — pass their
|
|
26
|
+
* names as `customCodeFunctions`; without them the row counts as one. Every
|
|
27
|
+
* other row is one tool.
|
|
28
|
+
*/
|
|
29
|
+
export const countInstallTools = (
|
|
30
|
+
install: {
|
|
31
|
+
toolKey: string;
|
|
32
|
+
config: unknown;
|
|
33
|
+
metadata: unknown;
|
|
34
|
+
},
|
|
35
|
+
customCodeFunctions?: string[]
|
|
36
|
+
): number => {
|
|
37
|
+
if (install.toolKey === CUSTOM_CODE_TOOL_KEY) {
|
|
38
|
+
return customCodeFunctions
|
|
39
|
+
? allowedCount(customCodeFunctions, install.config)
|
|
40
|
+
: 1;
|
|
41
|
+
}
|
|
42
|
+
if (install.toolKey !== MCP_PROXY_TOOL_KEY) return 1;
|
|
43
|
+
|
|
44
|
+
const discovered = (
|
|
45
|
+
install.metadata as { discovery?: { tools?: unknown } } | null
|
|
46
|
+
)?.discovery?.tools;
|
|
47
|
+
if (!Array.isArray(discovered)) return 0;
|
|
48
|
+
|
|
49
|
+
const names = discovered
|
|
50
|
+
.map(tool => (tool as { name?: unknown } | null)?.name)
|
|
51
|
+
.filter((name): name is string => typeof name === 'string');
|
|
52
|
+
return allowedCount(names, install.config);
|
|
53
|
+
};
|