History

Merge branch 'main' into rs/mcp-per-user-oauth

Resolves migration ordering: main added 0001_harsh_meltdown (source_url) while this branch added 0001_funny_betty_brant (expires_at bigint). Kept main's 0001 as-is and regenerated the bigint migration as 0002. Cloud schema auto-merge picked up both changes.

Rhys Sullivan committed 6 months ago

Commit d34dec82 parents b32b601 596d57615 files · +3263 −90Browse files

Unified · Split

15 files
  1. apps/cloud/drizzle/0001_harsh_meltdown.sql +1 −0
  2. apps/cloud/drizzle/0002_fat_white_tiger.sql +0 −0
  3. apps/cloud/drizzle/meta/0001_snapshot.json +8 −2
  4. apps/cloud/drizzle/meta/0002_snapshot.json +1276 −0
  5. apps/cloud/drizzle/meta/_journal.json +9 −2
  6. apps/cloud/src/services/executor-schema.ts +2 −0
  7. apps/cloud/src/services/sources-refresh.node.test.ts +177 −0
  8. apps/local/drizzle/0001_sour_catseye.sql +1 −0
  9. apps/local/drizzle/meta/0001_snapshot.json +1143 −0
  10. apps/local/drizzle/meta/_journal.json +7 −0
  11. apps/local/src/server/executor-schema.ts +1 −3
  12. apps/local/src/server/mcp-oauth.test.ts +458 −0
  13. packages/plugins/mcp/src/sdk/presets.ts +8 −0
  14. packages/plugins/openapi/src/sdk/plugin.ts +161 −83
  15. packages/plugins/openapi/src/sdk/store.ts +11 −0
apps/cloud/drizzle/0001_harsh_meltdown.sql+1−0
@@ -0,0 +1 @@
1ALTER TABLE "openapi_source" ADD COLUMN "source_url" text;
apps/cloud/drizzle/0002_fat_white_tiger.sqlrenamed from apps/cloud/drizzle/0001_funny_betty_brant.sql+0−0

File renamed without changes.

apps/cloud/drizzle/meta/0001_snapshot.json+8−2
@@ -1,5 +1,5 @@
1{1{
2 "id": "1c9eefdc-19c2-40cf-b2a7-ae3243a4d731",2 "id": "12ebb69b-fb44-4e61-80b9-a48b5471ef5a",
3 "prevId": "93e9f8ef-fe9a-4a8c-b326-329464d876ca",3 "prevId": "93e9f8ef-fe9a-4a8c-b326-329464d876ca",
4 "version": "7",4 "version": "7",
5 "dialect": "postgresql",5 "dialect": "postgresql",
@@ -526,7 +526,7 @@ · 520 unmodified lines
526 },526 },
527 "expires_at": {527 "expires_at": {
528 "name": "expires_at",528 "name": "expires_at",
529 "type": "bigint",529 "type": "integer",
530 "primaryKey": false,530 "primaryKey": false,
531 "notNull": true531 "notNull": true
532 },532 },
@@ -801,6 +801,12 @@ · 268 unmodified lines
801 "primaryKey": false,801 "primaryKey": false,
802 "notNull": true802 "notNull": true
803 },803 },
804 "source_url": {
805 "name": "source_url",
806 "type": "text",
807 "primaryKey": false,
808 "notNull": false
809 },
804 "base_url": {810 "base_url": {
805 "name": "base_url",811 "name": "base_url",
806 "type": "text",812 "type": "text",
apps/cloud/drizzle/meta/0002_snapshot.json+1276−0
@@ -0,0 +1,1276 @@
1{
2 "id": "f029f809-1bf3-46e0-a31b-82df7c9c7171",
3 "prevId": "12ebb69b-fb44-4e61-80b9-a48b5471ef5a",
4 "version": "7",
5 "dialect": "postgresql",
6 "tables": {
7 "public.accounts": {
8 "name": "accounts",
9 "schema": "",
10 "columns": {
11 "id": {
12 "name": "id",
13 "type": "text",
14 "primaryKey": true,
15 "notNull": true
16 },
17 "created_at": {
18 "name": "created_at",
19 "type": "timestamp with time zone",
20 "primaryKey": false,
21 "notNull": true,
22 "default": "now()"
23 }
24 },
25 "indexes": {},
26 "foreignKeys": {},
27 "compositePrimaryKeys": {},
28 "uniqueConstraints": {},
29 "policies": {},
30 "checkConstraints": {},
31 "isRLSEnabled": false
32 },
33 "public.memberships": {
34 "name": "memberships",
35 "schema": "",
36 "columns": {
37 "account_id": {
38 "name": "account_id",
39 "type": "text",
40 "primaryKey": false,
41 "notNull": true
42 },
43 "organization_id": {
44 "name": "organization_id",
45 "type": "text",
46 "primaryKey": false,
47 "notNull": true
48 },
49 "created_at": {
50 "name": "created_at",
51 "type": "timestamp with time zone",
52 "primaryKey": false,
53 "notNull": true,
54 "default": "now()"
55 }
56 },
57 "indexes": {},
58 "foreignKeys": {
59 "memberships_account_id_accounts_id_fk": {
60 "name": "memberships_account_id_accounts_id_fk",
61 "tableFrom": "memberships",
62 "tableTo": "accounts",
63 "columnsFrom": [
64 "account_id"
65 ],
66 "columnsTo": [
67 "id"
68 ],
69 "onDelete": "cascade",
70 "onUpdate": "no action"
71 },
72 "memberships_organization_id_organizations_id_fk": {
73 "name": "memberships_organization_id_organizations_id_fk",
74 "tableFrom": "memberships",
75 "tableTo": "organizations",
76 "columnsFrom": [
77 "organization_id"
78 ],
79 "columnsTo": [
80 "id"
81 ],
82 "onDelete": "cascade",
83 "onUpdate": "no action"
84 }
85 },
86 "compositePrimaryKeys": {
87 "memberships_account_id_organization_id_pk": {
88 "name": "memberships_account_id_organization_id_pk",
89 "columns": [
90 "account_id",
91 "organization_id"
92 ]
93 }
94 },
95 "uniqueConstraints": {},
96 "policies": {},
97 "checkConstraints": {},
98 "isRLSEnabled": false
99 },
100 "public.organizations": {
101 "name": "organizations",
102 "schema": "",
103 "columns": {
104 "id": {
105 "name": "id",
106 "type": "text",
107 "primaryKey": true,
108 "notNull": true
109 },
110 "name": {
111 "name": "name",
112 "type": "text",
113 "primaryKey": false,
114 "notNull": true
115 },
116 "created_at": {
117 "name": "created_at",
118 "type": "timestamp with time zone",
119 "primaryKey": false,
120 "notNull": true,
121 "default": "now()"
122 }
123 },
124 "indexes": {},
125 "foreignKeys": {},
126 "compositePrimaryKeys": {},
127 "uniqueConstraints": {},
128 "policies": {},
129 "checkConstraints": {},
130 "isRLSEnabled": false
131 },
132 "public.blob": {
133 "name": "blob",
134 "schema": "",
135 "columns": {
136 "namespace": {
137 "name": "namespace",
138 "type": "text",
139 "primaryKey": false,
140 "notNull": true
141 },
142 "key": {
143 "name": "key",
144 "type": "text",
145 "primaryKey": false,
146 "notNull": true
147 },
148 "value": {
149 "name": "value",
150 "type": "text",
151 "primaryKey": false,
152 "notNull": true
153 }
154 },
155 "indexes": {},
156 "foreignKeys": {},
157 "compositePrimaryKeys": {
158 "blob_namespace_key_pk": {
159 "name": "blob_namespace_key_pk",
160 "columns": [
161 "namespace",
162 "key"
163 ]
164 }
165 },
166 "uniqueConstraints": {},
167 "policies": {},
168 "checkConstraints": {},
169 "isRLSEnabled": false
170 },
171 "public.definition": {
172 "name": "definition",
173 "schema": "",
174 "columns": {
175 "id": {
176 "name": "id",
177 "type": "text",
178 "primaryKey": false,
179 "notNull": true
180 },
181 "scope_id": {
182 "name": "scope_id",
183 "type": "text",
184 "primaryKey": false,
185 "notNull": true
186 },
187 "source_id": {
188 "name": "source_id",
189 "type": "text",
190 "primaryKey": false,
191 "notNull": true
192 },
193 "plugin_id": {
194 "name": "plugin_id",
195 "type": "text",
196 "primaryKey": false,
197 "notNull": true
198 },
199 "name": {
200 "name": "name",
201 "type": "text",
202 "primaryKey": false,
203 "notNull": true
204 },
205 "schema": {
206 "name": "schema",
207 "type": "jsonb",
208 "primaryKey": false,
209 "notNull": true
210 },
211 "created_at": {
212 "name": "created_at",
213 "type": "timestamp",
214 "primaryKey": false,
215 "notNull": true
216 }
217 },
218 "indexes": {
219 "definition_scope_id_idx": {
220 "name": "definition_scope_id_idx",
221 "columns": [
222 {
223 "expression": "scope_id",
224 "isExpression": false,
225 "asc": true,
226 "nulls": "last"
227 }
228 ],
229 "isUnique": false,
230 "concurrently": false,
231 "method": "btree",
232 "with": {}
233 },
234 "definition_source_id_idx": {
235 "name": "definition_source_id_idx",
236 "columns": [
237 {
238 "expression": "source_id",
239 "isExpression": false,
240 "asc": true,
241 "nulls": "last"
242 }
243 ],
244 "isUnique": false,
245 "concurrently": false,
246 "method": "btree",
247 "with": {}
248 },
249 "definition_plugin_id_idx": {
250 "name": "definition_plugin_id_idx",
251 "columns": [
252 {
253 "expression": "plugin_id",
254 "isExpression": false,
255 "asc": true,
256 "nulls": "last"
257 }
258 ],
259 "isUnique": false,
260 "concurrently": false,
261 "method": "btree",
262 "with": {}
263 }
264 },
265 "foreignKeys": {},
266 "compositePrimaryKeys": {
267 "definition_scope_id_id_pk": {
268 "name": "definition_scope_id_id_pk",
269 "columns": [
270 "scope_id",
271 "id"
272 ]
273 }
274 },
275 "uniqueConstraints": {},
276 "policies": {},
277 "checkConstraints": {},
278 "isRLSEnabled": false
279 },
280 "public.graphql_operation": {
281 "name": "graphql_operation",
282 "schema": "",
283 "columns": {
284 "id": {
285 "name": "id",
286 "type": "text",
287 "primaryKey": false,
288 "notNull": true
289 },
290 "scope_id": {
291 "name": "scope_id",
292 "type": "text",
293 "primaryKey": false,
294 "notNull": true
295 },
296 "source_id": {
297 "name": "source_id",
298 "type": "text",
299 "primaryKey": false,
300 "notNull": true
301 },
302 "binding": {
303 "name": "binding",
304 "type": "jsonb",
305 "primaryKey": false,
306 "notNull": true
307 }
308 },
309 "indexes": {
310 "graphql_operation_scope_id_idx": {
311 "name": "graphql_operation_scope_id_idx",
312 "columns": [
313 {
314 "expression": "scope_id",
315 "isExpression": false,
316 "asc": true,
317 "nulls": "last"
318 }
319 ],
320 "isUnique": false,
321 "concurrently": false,
322 "method": "btree",
323 "with": {}
324 },
325 "graphql_operation_source_id_idx": {
326 "name": "graphql_operation_source_id_idx",
327 "columns": [
328 {
329 "expression": "source_id",
330 "isExpression": false,
331 "asc": true,
332 "nulls": "last"
333 }
334 ],
335 "isUnique": false,
336 "concurrently": false,
337 "method": "btree",
338 "with": {}
339 }
340 },
341 "foreignKeys": {},
342 "compositePrimaryKeys": {
343 "graphql_operation_scope_id_id_pk": {
344 "name": "graphql_operation_scope_id_id_pk",
345 "columns": [
346 "scope_id",
347 "id"
348 ]
349 }
350 },
351 "uniqueConstraints": {},
352 "policies": {},
353 "checkConstraints": {},
354 "isRLSEnabled": false
355 },
356 "public.graphql_source": {
357 "name": "graphql_source",
358 "schema": "",
359 "columns": {
360 "id": {
361 "name": "id",
362 "type": "text",
363 "primaryKey": false,
364 "notNull": true
365 },
366 "scope_id": {
367 "name": "scope_id",
368 "type": "text",
369 "primaryKey": false,
370 "notNull": true
371 },
372 "name": {
373 "name": "name",
374 "type": "text",
375 "primaryKey": false,
376 "notNull": true
377 },
378 "endpoint": {
379 "name": "endpoint",
380 "type": "text",
381 "primaryKey": false,
382 "notNull": true
383 },
384 "headers": {
385 "name": "headers",
386 "type": "jsonb",
387 "primaryKey": false,
388 "notNull": false
389 }
390 },
391 "indexes": {
392 "graphql_source_scope_id_idx": {
393 "name": "graphql_source_scope_id_idx",
394 "columns": [
395 {
396 "expression": "scope_id",
397 "isExpression": false,
398 "asc": true,
399 "nulls": "last"
400 }
401 ],
402 "isUnique": false,
403 "concurrently": false,
404 "method": "btree",
405 "with": {}
406 }
407 },
408 "foreignKeys": {},
409 "compositePrimaryKeys": {
410 "graphql_source_scope_id_id_pk": {
411 "name": "graphql_source_scope_id_id_pk",
412 "columns": [
413 "scope_id",
414 "id"
415 ]
416 }
417 },
418 "uniqueConstraints": {},
419 "policies": {},
420 "checkConstraints": {},
421 "isRLSEnabled": false
422 },
423 "public.mcp_binding": {
424 "name": "mcp_binding",
425 "schema": "",
426 "columns": {
427 "id": {
428 "name": "id",
429 "type": "text",
430 "primaryKey": false,
431 "notNull": true
432 },
433 "scope_id": {
434 "name": "scope_id",
435 "type": "text",
436 "primaryKey": false,
437 "notNull": true
438 },
439 "source_id": {
440 "name": "source_id",
441 "type": "text",
442 "primaryKey": false,
443 "notNull": true
444 },
445 "binding": {
446 "name": "binding",
447 "type": "jsonb",
448 "primaryKey": false,
449 "notNull": true
450 },
451 "created_at": {
452 "name": "created_at",
453 "type": "timestamp",
454 "primaryKey": false,
455 "notNull": true
456 }
457 },
458 "indexes": {
459 "mcp_binding_scope_id_idx": {
460 "name": "mcp_binding_scope_id_idx",
461 "columns": [
462 {
463 "expression": "scope_id",
464 "isExpression": false,
465 "asc": true,
466 "nulls": "last"
467 }
468 ],
469 "isUnique": false,
470 "concurrently": false,
471 "method": "btree",
472 "with": {}
473 },
474 "mcp_binding_source_id_idx": {
475 "name": "mcp_binding_source_id_idx",
476 "columns": [
477 {
478 "expression": "source_id",
479 "isExpression": false,
480 "asc": true,
481 "nulls": "last"
482 }
483 ],
484 "isUnique": false,
485 "concurrently": false,
486 "method": "btree",
487 "with": {}
488 }
489 },
490 "foreignKeys": {},
491 "compositePrimaryKeys": {
492 "mcp_binding_scope_id_id_pk": {
493 "name": "mcp_binding_scope_id_id_pk",
494 "columns": [
495 "scope_id",
496 "id"
497 ]
498 }
499 },
500 "uniqueConstraints": {},
501 "policies": {},
502 "checkConstraints": {},
503 "isRLSEnabled": false
504 },
505 "public.mcp_oauth_session": {
506 "name": "mcp_oauth_session",
507 "schema": "",
508 "columns": {
509 "id": {
510 "name": "id",
511 "type": "text",
512 "primaryKey": false,
513 "notNull": true
514 },
515 "scope_id": {
516 "name": "scope_id",
517 "type": "text",
518 "primaryKey": false,
519 "notNull": true
520 },
521 "session": {
522 "name": "session",
523 "type": "jsonb",
524 "primaryKey": false,
525 "notNull": true
526 },
527 "expires_at": {
528 "name": "expires_at",
529 "type": "bigint",
530 "primaryKey": false,
531 "notNull": true
532 },
533 "created_at": {
534 "name": "created_at",
535 "type": "timestamp",
536 "primaryKey": false,
537 "notNull": true
538 }
539 },
540 "indexes": {
541 "mcp_oauth_session_scope_id_idx": {
542 "name": "mcp_oauth_session_scope_id_idx",
543 "columns": [
544 {
545 "expression": "scope_id",
546 "isExpression": false,
547 "asc": true,
548 "nulls": "last"
549 }
550 ],
551 "isUnique": false,
552 "concurrently": false,
553 "method": "btree",
554 "with": {}
555 }
556 },
557 "foreignKeys": {},
558 "compositePrimaryKeys": {
559 "mcp_oauth_session_scope_id_id_pk": {
560 "name": "mcp_oauth_session_scope_id_id_pk",
561 "columns": [
562 "scope_id",
563 "id"
564 ]
565 }
566 },
567 "uniqueConstraints": {},
568 "policies": {},
569 "checkConstraints": {},
570 "isRLSEnabled": false
571 },
572 "public.mcp_source": {
573 "name": "mcp_source",
574 "schema": "",
575 "columns": {
576 "id": {
577 "name": "id",
578 "type": "text",
579 "primaryKey": false,
580 "notNull": true
581 },
582 "scope_id": {
583 "name": "scope_id",
584 "type": "text",
585 "primaryKey": false,
586 "notNull": true
587 },
588 "name": {
589 "name": "name",
590 "type": "text",
591 "primaryKey": false,
592 "notNull": true
593 },
594 "config": {
595 "name": "config",
596 "type": "jsonb",
597 "primaryKey": false,
598 "notNull": true
599 },
600 "created_at": {
601 "name": "created_at",
602 "type": "timestamp",
603 "primaryKey": false,
604 "notNull": true
605 }
606 },
607 "indexes": {
608 "mcp_source_scope_id_idx": {
609 "name": "mcp_source_scope_id_idx",
610 "columns": [
611 {
612 "expression": "scope_id",
613 "isExpression": false,
614 "asc": true,
615 "nulls": "last"
616 }
617 ],
618 "isUnique": false,
619 "concurrently": false,
620 "method": "btree",
621 "with": {}
622 }
623 },
624 "foreignKeys": {},
625 "compositePrimaryKeys": {
626 "mcp_source_scope_id_id_pk": {
627 "name": "mcp_source_scope_id_id_pk",
628 "columns": [
629 "scope_id",
630 "id"
631 ]
632 }
633 },
634 "uniqueConstraints": {},
635 "policies": {},
636 "checkConstraints": {},
637 "isRLSEnabled": false
638 },
639 "public.openapi_oauth_session": {
640 "name": "openapi_oauth_session",
641 "schema": "",
642 "columns": {
643 "id": {
644 "name": "id",
645 "type": "text",
646 "primaryKey": false,
647 "notNull": true
648 },
649 "scope_id": {
650 "name": "scope_id",
651 "type": "text",
652 "primaryKey": false,
653 "notNull": true
654 },
655 "session": {
656 "name": "session",
657 "type": "jsonb",
658 "primaryKey": false,
659 "notNull": true
660 },
661 "created_at": {
662 "name": "created_at",
663 "type": "timestamp",
664 "primaryKey": false,
665 "notNull": true
666 }
667 },
668 "indexes": {
669 "openapi_oauth_session_scope_id_idx": {
670 "name": "openapi_oauth_session_scope_id_idx",
671 "columns": [
672 {
673 "expression": "scope_id",
674 "isExpression": false,
675 "asc": true,
676 "nulls": "last"
677 }
678 ],
679 "isUnique": false,
680 "concurrently": false,
681 "method": "btree",
682 "with": {}
683 }
684 },
685 "foreignKeys": {},
686 "compositePrimaryKeys": {
687 "openapi_oauth_session_scope_id_id_pk": {
688 "name": "openapi_oauth_session_scope_id_id_pk",
689 "columns": [
690 "scope_id",
691 "id"
692 ]
693 }
694 },
695 "uniqueConstraints": {},
696 "policies": {},
697 "checkConstraints": {},
698 "isRLSEnabled": false
699 },
700 "public.openapi_operation": {
701 "name": "openapi_operation",
702 "schema": "",
703 "columns": {
704 "id": {
705 "name": "id",
706 "type": "text",
707 "primaryKey": false,
708 "notNull": true
709 },
710 "scope_id": {
711 "name": "scope_id",
712 "type": "text",
713 "primaryKey": false,
714 "notNull": true
715 },
716 "source_id": {
717 "name": "source_id",
718 "type": "text",
719 "primaryKey": false,
720 "notNull": true
721 },
722 "binding": {
723 "name": "binding",
724 "type": "jsonb",
725 "primaryKey": false,
726 "notNull": true
727 }
728 },
729 "indexes": {
730 "openapi_operation_scope_id_idx": {
731 "name": "openapi_operation_scope_id_idx",
732 "columns": [
733 {
734 "expression": "scope_id",
735 "isExpression": false,
736 "asc": true,
737 "nulls": "last"
738 }
739 ],
740 "isUnique": false,
741 "concurrently": false,
742 "method": "btree",
743 "with": {}
744 },
745 "openapi_operation_source_id_idx": {
746 "name": "openapi_operation_source_id_idx",
747 "columns": [
748 {
749 "expression": "source_id",
750 "isExpression": false,
751 "asc": true,
752 "nulls": "last"
753 }
754 ],
755 "isUnique": false,
756 "concurrently": false,
757 "method": "btree",
758 "with": {}
759 }
760 },
761 "foreignKeys": {},
762 "compositePrimaryKeys": {
763 "openapi_operation_scope_id_id_pk": {
764 "name": "openapi_operation_scope_id_id_pk",
765 "columns": [
766 "scope_id",
767 "id"
768 ]
769 }
770 },
771 "uniqueConstraints": {},
772 "policies": {},
773 "checkConstraints": {},
774 "isRLSEnabled": false
775 },
776 "public.openapi_source": {
777 "name": "openapi_source",
778 "schema": "",
779 "columns": {
780 "id": {
781 "name": "id",
782 "type": "text",
783 "primaryKey": false,
784 "notNull": true
785 },
786 "scope_id": {
787 "name": "scope_id",
788 "type": "text",
789 "primaryKey": false,
790 "notNull": true
791 },
792 "name": {
793 "name": "name",
794 "type": "text",
795 "primaryKey": false,
796 "notNull": true
797 },
798 "spec": {
799 "name": "spec",
800 "type": "text",
801 "primaryKey": false,
802 "notNull": true
803 },
804 "source_url": {
805 "name": "source_url",
806 "type": "text",
807 "primaryKey": false,
808 "notNull": false
809 },
810 "base_url": {
811 "name": "base_url",
812 "type": "text",
813 "primaryKey": false,
814 "notNull": false
815 },
816 "headers": {
817 "name": "headers",
818 "type": "jsonb",
819 "primaryKey": false,
820 "notNull": false
821 },
822 "oauth2": {
823 "name": "oauth2",
824 "type": "jsonb",
825 "primaryKey": false,
826 "notNull": false
827 },
828 "invocation_config": {
829 "name": "invocation_config",
830 "type": "jsonb",
831 "primaryKey": false,
832 "notNull": true
833 }
834 },
835 "indexes": {
836 "openapi_source_scope_id_idx": {
837 "name": "openapi_source_scope_id_idx",
838 "columns": [
839 {
840 "expression": "scope_id",
841 "isExpression": false,
842 "asc": true,
843 "nulls": "last"
844 }
845 ],
846 "isUnique": false,
847 "concurrently": false,
848 "method": "btree",
849 "with": {}
850 }
851 },
852 "foreignKeys": {},
853 "compositePrimaryKeys": {
854 "openapi_source_scope_id_id_pk": {
855 "name": "openapi_source_scope_id_id_pk",
856 "columns": [
857 "scope_id",
858 "id"
859 ]
860 }
861 },
862 "uniqueConstraints": {},
863 "policies": {},
864 "checkConstraints": {},
865 "isRLSEnabled": false
866 },
867 "public.secret": {
868 "name": "secret",
869 "schema": "",
870 "columns": {
871 "id": {
872 "name": "id",
873 "type": "text",
874 "primaryKey": false,
875 "notNull": true
876 },
877 "scope_id": {
878 "name": "scope_id",
879 "type": "text",
880 "primaryKey": false,
881 "notNull": true
882 },
883 "name": {
884 "name": "name",
885 "type": "text",
886 "primaryKey": false,
887 "notNull": true
888 },
889 "provider": {
890 "name": "provider",
891 "type": "text",
892 "primaryKey": false,
893 "notNull": true
894 },
895 "created_at": {
896 "name": "created_at",
897 "type": "timestamp",
898 "primaryKey": false,
899 "notNull": true
900 }
901 },
902 "indexes": {
903 "secret_scope_id_idx": {
904 "name": "secret_scope_id_idx",
905 "columns": [
906 {
907 "expression": "scope_id",
908 "isExpression": false,
909 "asc": true,
910 "nulls": "last"
911 }
912 ],
913 "isUnique": false,
914 "concurrently": false,
915 "method": "btree",
916 "with": {}
917 },
918 "secret_provider_idx": {
919 "name": "secret_provider_idx",
920 "columns": [
921 {
922 "expression": "provider",
923 "isExpression": false,
924 "asc": true,
925 "nulls": "last"
926 }
927 ],
928 "isUnique": false,
929 "concurrently": false,
930 "method": "btree",
931 "with": {}
932 }
933 },
934 "foreignKeys": {},
935 "compositePrimaryKeys": {
936 "secret_scope_id_id_pk": {
937 "name": "secret_scope_id_id_pk",
938 "columns": [
939 "scope_id",
940 "id"
941 ]
942 }
943 },
944 "uniqueConstraints": {},
945 "policies": {},
946 "checkConstraints": {},
947 "isRLSEnabled": false
948 },
949 "public.source": {
950 "name": "source",
951 "schema": "",
952 "columns": {
953 "id": {
954 "name": "id",
955 "type": "text",
956 "primaryKey": false,
957 "notNull": true
958 },
959 "scope_id": {
960 "name": "scope_id",
961 "type": "text",
962 "primaryKey": false,
963 "notNull": true
964 },
965 "plugin_id": {
966 "name": "plugin_id",
967 "type": "text",
968 "primaryKey": false,
969 "notNull": true
970 },
971 "kind": {
972 "name": "kind",
973 "type": "text",
974 "primaryKey": false,
975 "notNull": true
976 },
977 "name": {
978 "name": "name",
979 "type": "text",
980 "primaryKey": false,
981 "notNull": true
982 },
983 "url": {
984 "name": "url",
985 "type": "text",
986 "primaryKey": false,
987 "notNull": false
988 },
989 "can_remove": {
990 "name": "can_remove",
991 "type": "boolean",
992 "primaryKey": false,
993 "notNull": true,
994 "default": true
995 },
996 "can_refresh": {
997 "name": "can_refresh",
998 "type": "boolean",
999 "primaryKey": false,
1000 "notNull": true,
1001 "default": false
1002 },
1003 "can_edit": {
1004 "name": "can_edit",
1005 "type": "boolean",
1006 "primaryKey": false,
1007 "notNull": true,
1008 "default": false
1009 },
1010 "created_at": {
1011 "name": "created_at",
1012 "type": "timestamp",
1013 "primaryKey": false,
1014 "notNull": true
1015 },
1016 "updated_at": {
1017 "name": "updated_at",
1018 "type": "timestamp",
1019 "primaryKey": false,
1020 "notNull": true
1021 }
1022 },
1023 "indexes": {
1024 "source_scope_id_idx": {
1025 "name": "source_scope_id_idx",
1026 "columns": [
1027 {
1028 "expression": "scope_id",
1029 "isExpression": false,
1030 "asc": true,
1031 "nulls": "last"
1032 }
1033 ],
1034 "isUnique": false,
1035 "concurrently": false,
1036 "method": "btree",
1037 "with": {}
1038 },
1039 "source_plugin_id_idx": {
1040 "name": "source_plugin_id_idx",
1041 "columns": [
1042 {
1043 "expression": "plugin_id",
1044 "isExpression": false,
1045 "asc": true,
1046 "nulls": "last"
1047 }
1048 ],
1049 "isUnique": false,
1050 "concurrently": false,
1051 "method": "btree",
1052 "with": {}
1053 }
1054 },
1055 "foreignKeys": {},
1056 "compositePrimaryKeys": {
1057 "source_scope_id_id_pk": {
1058 "name": "source_scope_id_id_pk",
1059 "columns": [
1060 "scope_id",
1061 "id"
1062 ]
1063 }
1064 },
1065 "uniqueConstraints": {},
1066 "policies": {},
1067 "checkConstraints": {},
1068 "isRLSEnabled": false
1069 },
1070 "public.tool": {
1071 "name": "tool",
1072 "schema": "",
1073 "columns": {
1074 "id": {
1075 "name": "id",
1076 "type": "text",
1077 "primaryKey": false,
1078 "notNull": true
1079 },
1080 "scope_id": {
1081 "name": "scope_id",
1082 "type": "text",
1083 "primaryKey": false,
1084 "notNull": true
1085 },
1086 "source_id": {
1087 "name": "source_id",
1088 "type": "text",
1089 "primaryKey": false,
1090 "notNull": true
1091 },
1092 "plugin_id": {
1093 "name": "plugin_id",
1094 "type": "text",
1095 "primaryKey": false,
1096 "notNull": true
1097 },
1098 "name": {
1099 "name": "name",
1100 "type": "text",
1101 "primaryKey": false,
1102 "notNull": true
1103 },
1104 "description": {
1105 "name": "description",
1106 "type": "text",
1107 "primaryKey": false,
1108 "notNull": true
1109 },
1110 "input_schema": {
1111 "name": "input_schema",
1112 "type": "jsonb",
1113 "primaryKey": false,
1114 "notNull": false
1115 },
1116 "output_schema": {
1117 "name": "output_schema",
1118 "type": "jsonb",
1119 "primaryKey": false,
1120 "notNull": false
1121 },
1122 "created_at": {
1123 "name": "created_at",
1124 "type": "timestamp",
1125 "primaryKey": false,
1126 "notNull": true
1127 },
1128 "updated_at": {
1129 "name": "updated_at",
1130 "type": "timestamp",
1131 "primaryKey": false,
1132 "notNull": true
1133 }
1134 },
1135 "indexes": {
1136 "tool_scope_id_idx": {
1137 "name": "tool_scope_id_idx",
1138 "columns": [
1139 {
1140 "expression": "scope_id",
1141 "isExpression": false,
1142 "asc": true,
1143 "nulls": "last"
1144 }
1145 ],
1146 "isUnique": false,
1147 "concurrently": false,
1148 "method": "btree",
1149 "with": {}
1150 },
1151 "tool_source_id_idx": {
1152 "name": "tool_source_id_idx",
1153 "columns": [
1154 {
1155 "expression": "source_id",
1156 "isExpression": false,
1157 "asc": true,
1158 "nulls": "last"
1159 }
1160 ],
1161 "isUnique": false,
1162 "concurrently": false,
1163 "method": "btree",
1164 "with": {}
1165 },
1166 "tool_plugin_id_idx": {
1167 "name": "tool_plugin_id_idx",
1168 "columns": [
1169 {
1170 "expression": "plugin_id",
1171 "isExpression": false,
1172 "asc": true,
1173 "nulls": "last"
1174 }
1175 ],
1176 "isUnique": false,
1177 "concurrently": false,
1178 "method": "btree",
1179 "with": {}
1180 }
1181 },
1182 "foreignKeys": {},
1183 "compositePrimaryKeys": {
1184 "tool_scope_id_id_pk": {
1185 "name": "tool_scope_id_id_pk",
1186 "columns": [
1187 "scope_id",
1188 "id"
1189 ]
1190 }
1191 },
1192 "uniqueConstraints": {},
1193 "policies": {},
1194 "checkConstraints": {},
1195 "isRLSEnabled": false
1196 },
1197 "public.workos_vault_metadata": {
1198 "name": "workos_vault_metadata",
1199 "schema": "",
1200 "columns": {
1201 "id": {
1202 "name": "id",
1203 "type": "text",
1204 "primaryKey": false,
1205 "notNull": true
1206 },
1207 "scope_id": {
1208 "name": "scope_id",
1209 "type": "text",
1210 "primaryKey": false,
1211 "notNull": true
1212 },
1213 "name": {
1214 "name": "name",
1215 "type": "text",
1216 "primaryKey": false,
1217 "notNull": true
1218 },
1219 "purpose": {
1220 "name": "purpose",
1221 "type": "text",
1222 "primaryKey": false,
1223 "notNull": false
1224 },
1225 "created_at": {
1226 "name": "created_at",
1227 "type": "timestamp",
1228 "primaryKey": false,
1229 "notNull": true
1230 }
1231 },
1232 "indexes": {
1233 "workos_vault_metadata_scope_id_idx": {
1234 "name": "workos_vault_metadata_scope_id_idx",
1235 "columns": [
1236 {
1237 "expression": "scope_id",
1238 "isExpression": false,
1239 "asc": true,
1240 "nulls": "last"
1241 }
1242 ],
1243 "isUnique": false,
1244 "concurrently": false,
1245 "method": "btree",
1246 "with": {}
1247 }
1248 },
1249 "foreignKeys": {},
1250 "compositePrimaryKeys": {
1251 "workos_vault_metadata_scope_id_id_pk": {
1252 "name": "workos_vault_metadata_scope_id_id_pk",
1253 "columns": [
1254 "scope_id",
1255 "id"
1256 ]
1257 }
1258 },
1259 "uniqueConstraints": {},
1260 "policies": {},
1261 "checkConstraints": {},
1262 "isRLSEnabled": false
1263 }
1264 },
1265 "enums": {},
1266 "schemas": {},
1267 "sequences": {},
1268 "roles": {},
1269 "policies": {},
1270 "views": {},
1271 "_meta": {
1272 "columns": {},
1273 "schemas": {},
1274 "tables": {}
1275 }
1276}
apps/cloud/drizzle/meta/_journal.json+9−2
@@ -12,8 +12,15 @@ · 11 unmodified lines
12 {12 {
13 "idx": 1,13 "idx": 1,
14 "version": "7",14 "version": "7",
15 "when": 1776706145641,15 "when": 1776678968180,
16 "tag": "0001_funny_betty_brant",16 "tag": "0001_harsh_meltdown",
17 "breakpoints": true
18 },
19 {
20 "idx": 2,
21 "version": "7",
22 "when": 1776709954401,
23 "tag": "0002_fat_white_tiger",
17 "breakpoints": true24 "breakpoints": true
18 }25 }
19 ]26 ]
apps/cloud/src/services/executor-schema.ts+2−0
@@ -68,6 +68,7 @@ export const openapi_source = pgTable("openapi_source", { · 67 unmodified lines
68 scope_id: text('scope_id').notNull(),68 scope_id: text('scope_id').notNull(),
69 name: text('name').notNull(),69 name: text('name').notNull(),
70 spec: text('spec').notNull(),70 spec: text('spec').notNull(),
71 source_url: text('source_url'),
71 base_url: text('base_url'),72 base_url: text('base_url'),
72 headers: jsonb('headers'),73 headers: jsonb('headers'),
73 oauth2: jsonb('oauth2'),74 oauth2: jsonb('oauth2'),
@@ -176,3 +177,4 @@ export const blob = pgTable("blob", { · 102 unmodified lines
176 primaryKey({ columns: [table.namespace, table.key] }),177 primaryKey({ columns: [table.namespace, table.key] }),
177]);178]);
178 179
180
apps/cloud/src/services/sources-refresh.node.test.ts+177−0
@@ -0,0 +1,177 @@
1// Refresh endpoint — covers `sources.refresh(id)` for an OpenAPI
2// source added from a URL. Stands up a local HTTP server that serves
3// one of two spec versions (swappable mid-test) so we can verify the
4// refresh path re-fetches from the stored origin and replaces the
5// operation set. Raw-text sources assert the no-op branch.
6
7import { describe, expect, it } from "@effect/vitest";
8import { Effect } from "effect";
9import http from "node:http";
10import { AddressInfo } from "node:net";
11
12import { ScopeId } from "@executor/sdk";
13
14import { asOrg } from "./__test-harness__/api-harness";
15
16const specV1 = JSON.stringify({
17 openapi: "3.0.0",
18 info: { title: "Refresh Fixture", version: "1.0.0" },
19 paths: {
20 "/ping": {
21 get: {
22 operationId: "ping",
23 summary: "ping",
24 responses: { "200": { description: "ok" } },
25 },
26 },
27 },
28});
29
30const specV2 = JSON.stringify({
31 openapi: "3.0.0",
32 info: { title: "Refresh Fixture", version: "2.0.0" },
33 paths: {
34 "/ping": {
35 get: {
36 operationId: "ping",
37 summary: "ping",
38 responses: { "200": { description: "ok" } },
39 },
40 },
41 "/pong": {
42 get: {
43 operationId: "pong",
44 summary: "pong",
45 responses: { "200": { description: "ok" } },
46 },
47 },
48 },
49});
50
51// Mutable ref: tests flip `current` between v1 and v2 around the
52// refresh call. Using a single server keeps the URL stable across
53// both addSpec and refresh — the plugin persists the original URL,
54// so the second fetch goes back to the same endpoint.
55const serveMutableSpec = () => {
56 const state = { current: specV1, requests: 0 };
57 const server = http.createServer((req, res) => {
58 state.requests++;
59 res.writeHead(200, { "content-type": "application/json" });
60 res.end(state.current);
61 });
62 return new Promise<{
63 baseUrl: string;
64 setSpec: (s: string) => void;
65 requestCount: () => number;
66 close: () => Promise<void>;
67 }>((resolve) => {
68 server.listen(0, "127.0.0.1", () => {
69 const { port } = server.address() as AddressInfo;
70 resolve({
71 baseUrl: `http://127.0.0.1:${port}`,
72 setSpec: (s) => {
73 state.current = s;
74 },
75 requestCount: () => state.requests,
76 close: () =>
77 new Promise((r) => {
78 server.close(() => r());
79 }),
80 });
81 });
82 });
83};
84
85describe("sources.refresh (HTTP)", () => {
86 it.effect("addSpec from URL → canRefresh:true; refresh re-fetches and updates tools", () =>
87 Effect.gen(function* () {
88 const server = yield* Effect.promise(() => serveMutableSpec());
89 try {
90 const org = `org_${crypto.randomUUID()}`;
91 const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;
92
93 yield* asOrg(org, (client) =>
94 client.openapi.addSpec({
95 path: { scopeId: ScopeId.make(org) },
96 payload: { spec: `${server.baseUrl}/spec.json`, namespace },
97 }),
98 );
99
100 const before = yield* asOrg(org, (client) =>
101 client.sources.list({ path: { scopeId: ScopeId.make(org) } }),
102 );
103 const beforeSource = before.find((s) => s.id === namespace);
104 expect(beforeSource?.canRefresh).toBe(true);
105
106 const fetchedBefore = yield* asOrg(org, (client) =>
107 client.openapi.getSource({
108 path: { scopeId: ScopeId.make(org), namespace },
109 }),
110 );
111 expect(fetchedBefore?.config.sourceUrl).toBe(`${server.baseUrl}/spec.json`);
112
113 const beforeTools = yield* asOrg(org, (client) =>
114 client.sources.tools({
115 path: { scopeId: ScopeId.make(org), sourceId: namespace },
116 }),
117 );
118 expect(beforeTools.length).toBe(1);
119 expect(beforeTools.some((t) => t.name.startsWith("ping"))).toBe(true);
120 expect(beforeTools.some((t) => t.name.startsWith("pong"))).toBe(false);
121
122 // Flip the remote to v2 (adds `pong`) and trigger refresh.
123 server.setSpec(specV2);
124 const requestsBefore = server.requestCount();
125
126 const refreshResult = yield* asOrg(org, (client) =>
127 client.sources.refresh({
128 path: { scopeId: ScopeId.make(org), sourceId: namespace },
129 }),
130 );
131 expect(refreshResult.refreshed).toBe(true);
132 expect(server.requestCount()).toBeGreaterThan(requestsBefore);
133
134 const afterTools = yield* asOrg(org, (client) =>
135 client.sources.tools({
136 path: { scopeId: ScopeId.make(org), sourceId: namespace },
137 }),
138 );
139 expect(afterTools.length).toBe(2);
140 expect(afterTools.some((t) => t.name.startsWith("ping"))).toBe(true);
141 expect(afterTools.some((t) => t.name.startsWith("pong"))).toBe(true);
142 } finally {
143 yield* Effect.promise(() => server.close());
144 }
145 }),
146 );
147
148 it.effect("addSpec from raw text → canRefresh:false; refresh is a no-op", () =>
149 Effect.gen(function* () {
150 const org = `org_${crypto.randomUUID()}`;
151 const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`;
152
153 yield* asOrg(org, (client) =>
154 client.openapi.addSpec({
155 path: { scopeId: ScopeId.make(org) },
156 payload: { spec: specV1, namespace },
157 }),
158 );
159
160 const sources = yield* asOrg(org, (client) =>
161 client.sources.list({ path: { scopeId: ScopeId.make(org) } }),
162 );
163 const row = sources.find((s) => s.id === namespace);
164 expect(row?.canRefresh).toBe(false);
165
166 // Raw-text sources reach the plugin with no stored URL and
167 // silently no-op — UI gates the action on canRefresh, but the
168 // server should not 500 if a caller slips through.
169 const result = yield* asOrg(org, (client) =>
170 client.sources.refresh({
171 path: { scopeId: ScopeId.make(org), sourceId: namespace },
172 }),
173 );
174 expect(result.refreshed).toBe(true);
175 }),
176 );
177});
apps/local/drizzle/0001_sour_catseye.sql+1−0
@@ -0,0 +1 @@
1ALTER TABLE `openapi_source` ADD `source_url` text;
apps/local/drizzle/meta/0001_snapshot.json+1143−0
@@ -0,0 +1,1143 @@
1{
2 "version": "6",
3 "dialect": "sqlite",
4 "id": "acff0d94-170c-40bb-9271-dc3a429ba0ce",
5 "prevId": "61fe33e0-7218-468e-8568-9a3f19e821ee",
6 "tables": {
7 "blob": {
8 "name": "blob",
9 "columns": {
10 "namespace": {
11 "name": "namespace",
12 "type": "text",
13 "primaryKey": false,
14 "notNull": true,
15 "autoincrement": false
16 },
17 "key": {
18 "name": "key",
19 "type": "text",
20 "primaryKey": false,
21 "notNull": true,
22 "autoincrement": false
23 },
24 "value": {
25 "name": "value",
26 "type": "text",
27 "primaryKey": false,
28 "notNull": true,
29 "autoincrement": false
30 }
31 },
32 "indexes": {},
33 "foreignKeys": {},
34 "compositePrimaryKeys": {
35 "blob_namespace_key_pk": {
36 "columns": [
37 "namespace",
38 "key"
39 ],
40 "name": "blob_namespace_key_pk"
41 }
42 },
43 "uniqueConstraints": {},
44 "checkConstraints": {}
45 },
46 "definition": {
47 "name": "definition",
48 "columns": {
49 "id": {
50 "name": "id",
51 "type": "text",
52 "primaryKey": false,
53 "notNull": true,
54 "autoincrement": false
55 },
56 "scope_id": {
57 "name": "scope_id",
58 "type": "text",
59 "primaryKey": false,
60 "notNull": true,
61 "autoincrement": false
62 },
63 "source_id": {
64 "name": "source_id",
65 "type": "text",
66 "primaryKey": false,
67 "notNull": true,
68 "autoincrement": false
69 },
70 "plugin_id": {
71 "name": "plugin_id",
72 "type": "text",
73 "primaryKey": false,
74 "notNull": true,
75 "autoincrement": false
76 },
77 "name": {
78 "name": "name",
79 "type": "text",
80 "primaryKey": false,
81 "notNull": true,
82 "autoincrement": false
83 },
84 "schema": {
85 "name": "schema",
86 "type": "text",
87 "primaryKey": false,
88 "notNull": true,
89 "autoincrement": false
90 },
91 "created_at": {
92 "name": "created_at",
93 "type": "integer",
94 "primaryKey": false,
95 "notNull": true,
96 "autoincrement": false
97 }
98 },
99 "indexes": {
100 "definition_scope_id_idx": {
101 "name": "definition_scope_id_idx",
102 "columns": [
103 "scope_id"
104 ],
105 "isUnique": false
106 },
107 "definition_source_id_idx": {
108 "name": "definition_source_id_idx",
109 "columns": [
110 "source_id"
111 ],
112 "isUnique": false
113 },
114 "definition_plugin_id_idx": {
115 "name": "definition_plugin_id_idx",
116 "columns": [
117 "plugin_id"
118 ],
119 "isUnique": false
120 }
121 },
122 "foreignKeys": {},
123 "compositePrimaryKeys": {
124 "definition_scope_id_id_pk": {
125 "columns": [
126 "scope_id",
127 "id"
128 ],
129 "name": "definition_scope_id_id_pk"
130 }
131 },
132 "uniqueConstraints": {},
133 "checkConstraints": {}
134 },
135 "google_discovery_binding": {
136 "name": "google_discovery_binding",
137 "columns": {
138 "id": {
139 "name": "id",
140 "type": "text",
141 "primaryKey": false,
142 "notNull": true,
143 "autoincrement": false
144 },
145 "scope_id": {
146 "name": "scope_id",
147 "type": "text",
148 "primaryKey": false,
149 "notNull": true,
150 "autoincrement": false
151 },
152 "source_id": {
153 "name": "source_id",
154 "type": "text",
155 "primaryKey": false,
156 "notNull": true,
157 "autoincrement": false
158 },
159 "binding": {
160 "name": "binding",
161 "type": "text",
162 "primaryKey": false,
163 "notNull": true,
164 "autoincrement": false
165 },
166 "created_at": {
167 "name": "created_at",
168 "type": "integer",
169 "primaryKey": false,
170 "notNull": true,
171 "autoincrement": false
172 }
173 },
174 "indexes": {
175 "google_discovery_binding_scope_id_idx": {
176 "name": "google_discovery_binding_scope_id_idx",
177 "columns": [
178 "scope_id"
179 ],
180 "isUnique": false
181 },
182 "google_discovery_binding_source_id_idx": {
183 "name": "google_discovery_binding_source_id_idx",
184 "columns": [
185 "source_id"
186 ],
187 "isUnique": false
188 }
189 },
190 "foreignKeys": {},
191 "compositePrimaryKeys": {
192 "google_discovery_binding_scope_id_id_pk": {
193 "columns": [
194 "scope_id",
195 "id"
196 ],
197 "name": "google_discovery_binding_scope_id_id_pk"
198 }
199 },
200 "uniqueConstraints": {},
201 "checkConstraints": {}
202 },
203 "google_discovery_oauth_session": {
204 "name": "google_discovery_oauth_session",
205 "columns": {
206 "id": {
207 "name": "id",
208 "type": "text",
209 "primaryKey": false,
210 "notNull": true,
211 "autoincrement": false
212 },
213 "scope_id": {
214 "name": "scope_id",
215 "type": "text",
216 "primaryKey": false,
217 "notNull": true,
218 "autoincrement": false
219 },
220 "session": {
221 "name": "session",
222 "type": "text",
223 "primaryKey": false,
224 "notNull": true,
225 "autoincrement": false
226 },
227 "expires_at": {
228 "name": "expires_at",
229 "type": "integer",
230 "primaryKey": false,
231 "notNull": true,
232 "autoincrement": false
233 }
234 },
235 "indexes": {
236 "google_discovery_oauth_session_scope_id_idx": {
237 "name": "google_discovery_oauth_session_scope_id_idx",
238 "columns": [
239 "scope_id"
240 ],
241 "isUnique": false
242 }
243 },
244 "foreignKeys": {},
245 "compositePrimaryKeys": {
246 "google_discovery_oauth_session_scope_id_id_pk": {
247 "columns": [
248 "scope_id",
249 "id"
250 ],
251 "name": "google_discovery_oauth_session_scope_id_id_pk"
252 }
253 },
254 "uniqueConstraints": {},
255 "checkConstraints": {}
256 },
257 "google_discovery_source": {
258 "name": "google_discovery_source",
259 "columns": {
260 "id": {
261 "name": "id",
262 "type": "text",
263 "primaryKey": false,
264 "notNull": true,
265 "autoincrement": false
266 },
267 "scope_id": {
268 "name": "scope_id",
269 "type": "text",
270 "primaryKey": false,
271 "notNull": true,
272 "autoincrement": false
273 },
274 "name": {
275 "name": "name",
276 "type": "text",
277 "primaryKey": false,
278 "notNull": true,
279 "autoincrement": false
280 },
281 "config": {
282 "name": "config",
283 "type": "text",
284 "primaryKey": false,
285 "notNull": true,
286 "autoincrement": false
287 },
288 "created_at": {
289 "name": "created_at",
290 "type": "integer",
291 "primaryKey": false,
292 "notNull": true,
293 "autoincrement": false
294 },
295 "updated_at": {
296 "name": "updated_at",
297 "type": "integer",
298 "primaryKey": false,
299 "notNull": true,
300 "autoincrement": false
301 }
302 },
303 "indexes": {
304 "google_discovery_source_scope_id_idx": {
305 "name": "google_discovery_source_scope_id_idx",
306 "columns": [
307 "scope_id"
308 ],
309 "isUnique": false
310 }
311 },
312 "foreignKeys": {},
313 "compositePrimaryKeys": {
314 "google_discovery_source_scope_id_id_pk": {
315 "columns": [
316 "scope_id",
317 "id"
318 ],
319 "name": "google_discovery_source_scope_id_id_pk"
320 }
321 },
322 "uniqueConstraints": {},
323 "checkConstraints": {}
324 },
325 "graphql_operation": {
326 "name": "graphql_operation",
327 "columns": {
328 "id": {
329 "name": "id",
330 "type": "text",
331 "primaryKey": false,
332 "notNull": true,
333 "autoincrement": false
334 },
335 "scope_id": {
336 "name": "scope_id",
337 "type": "text",
338 "primaryKey": false,
339 "notNull": true,
340 "autoincrement": false
341 },
342 "source_id": {
343 "name": "source_id",
344 "type": "text",
345 "primaryKey": false,
346 "notNull": true,
347 "autoincrement": false
348 },
349 "binding": {
350 "name": "binding",
351 "type": "text",
352 "primaryKey": false,
353 "notNull": true,
354 "autoincrement": false
355 }
356 },
357 "indexes": {
358 "graphql_operation_scope_id_idx": {
359 "name": "graphql_operation_scope_id_idx",
360 "columns": [
361 "scope_id"
362 ],
363 "isUnique": false
364 },
365 "graphql_operation_source_id_idx": {
366 "name": "graphql_operation_source_id_idx",
367 "columns": [
368 "source_id"
369 ],
370 "isUnique": false
371 }
372 },
373 "foreignKeys": {},
374 "compositePrimaryKeys": {
375 "graphql_operation_scope_id_id_pk": {
376 "columns": [
377 "scope_id",
378 "id"
379 ],
380 "name": "graphql_operation_scope_id_id_pk"
381 }
382 },
383 "uniqueConstraints": {},
384 "checkConstraints": {}
385 },
386 "graphql_source": {
387 "name": "graphql_source",
388 "columns": {
389 "id": {
390 "name": "id",
391 "type": "text",
392 "primaryKey": false,
393 "notNull": true,
394 "autoincrement": false
395 },
396 "scope_id": {
397 "name": "scope_id",
398 "type": "text",
399 "primaryKey": false,
400 "notNull": true,
401 "autoincrement": false
402 },
403 "name": {
404 "name": "name",
405 "type": "text",
406 "primaryKey": false,
407 "notNull": true,
408 "autoincrement": false
409 },
410 "endpoint": {
411 "name": "endpoint",
412 "type": "text",
413 "primaryKey": false,
414 "notNull": true,
415 "autoincrement": false
416 },
417 "headers": {
418 "name": "headers",
419 "type": "text",
420 "primaryKey": false,
421 "notNull": false,
422 "autoincrement": false
423 }
424 },
425 "indexes": {
426 "graphql_source_scope_id_idx": {
427 "name": "graphql_source_scope_id_idx",
428 "columns": [
429 "scope_id"
430 ],
431 "isUnique": false
432 }
433 },
434 "foreignKeys": {},
435 "compositePrimaryKeys": {
436 "graphql_source_scope_id_id_pk": {
437 "columns": [
438 "scope_id",
439 "id"
440 ],
441 "name": "graphql_source_scope_id_id_pk"
442 }
443 },
444 "uniqueConstraints": {},
445 "checkConstraints": {}
446 },
447 "mcp_binding": {
448 "name": "mcp_binding",
449 "columns": {
450 "id": {
451 "name": "id",
452 "type": "text",
453 "primaryKey": false,
454 "notNull": true,
455 "autoincrement": false
456 },
457 "scope_id": {
458 "name": "scope_id",
459 "type": "text",
460 "primaryKey": false,
461 "notNull": true,
462 "autoincrement": false
463 },
464 "source_id": {
465 "name": "source_id",
466 "type": "text",
467 "primaryKey": false,
468 "notNull": true,
469 "autoincrement": false
470 },
471 "binding": {
472 "name": "binding",
473 "type": "text",
474 "primaryKey": false,
475 "notNull": true,
476 "autoincrement": false
477 },
478 "created_at": {
479 "name": "created_at",
480 "type": "integer",
481 "primaryKey": false,
482 "notNull": true,
483 "autoincrement": false
484 }
485 },
486 "indexes": {
487 "mcp_binding_scope_id_idx": {
488 "name": "mcp_binding_scope_id_idx",
489 "columns": [
490 "scope_id"
491 ],
492 "isUnique": false
493 },
494 "mcp_binding_source_id_idx": {
495 "name": "mcp_binding_source_id_idx",
496 "columns": [
497 "source_id"
498 ],
499 "isUnique": false
500 }
501 },
502 "foreignKeys": {},
503 "compositePrimaryKeys": {
504 "mcp_binding_scope_id_id_pk": {
505 "columns": [
506 "scope_id",
507 "id"
508 ],
509 "name": "mcp_binding_scope_id_id_pk"
510 }
511 },
512 "uniqueConstraints": {},
513 "checkConstraints": {}
514 },
515 "mcp_oauth_session": {
516 "name": "mcp_oauth_session",
517 "columns": {
518 "id": {
519 "name": "id",
520 "type": "text",
521 "primaryKey": false,
522 "notNull": true,
523 "autoincrement": false
524 },
525 "scope_id": {
526 "name": "scope_id",
527 "type": "text",
528 "primaryKey": false,
529 "notNull": true,
530 "autoincrement": false
531 },
532 "session": {
533 "name": "session",
534 "type": "text",
535 "primaryKey": false,
536 "notNull": true,
537 "autoincrement": false
538 },
539 "expires_at": {
540 "name": "expires_at",
541 "type": "integer",
542 "primaryKey": false,
543 "notNull": true,
544 "autoincrement": false
545 },
546 "created_at": {
547 "name": "created_at",
548 "type": "integer",
549 "primaryKey": false,
550 "notNull": true,
551 "autoincrement": false
552 }
553 },
554 "indexes": {
555 "mcp_oauth_session_scope_id_idx": {
556 "name": "mcp_oauth_session_scope_id_idx",
557 "columns": [
558 "scope_id"
559 ],
560 "isUnique": false
561 }
562 },
563 "foreignKeys": {},
564 "compositePrimaryKeys": {
565 "mcp_oauth_session_scope_id_id_pk": {
566 "columns": [
567 "scope_id",
568 "id"
569 ],
570 "name": "mcp_oauth_session_scope_id_id_pk"
571 }
572 },
573 "uniqueConstraints": {},
574 "checkConstraints": {}
575 },
576 "mcp_source": {
577 "name": "mcp_source",
578 "columns": {
579 "id": {
580 "name": "id",
581 "type": "text",
582 "primaryKey": false,
583 "notNull": true,
584 "autoincrement": false
585 },
586 "scope_id": {
587 "name": "scope_id",
588 "type": "text",
589 "primaryKey": false,
590 "notNull": true,
591 "autoincrement": false
592 },
593 "name": {
594 "name": "name",
595 "type": "text",
596 "primaryKey": false,
597 "notNull": true,
598 "autoincrement": false
599 },
600 "config": {
601 "name": "config",
602 "type": "text",
603 "primaryKey": false,
604 "notNull": true,
605 "autoincrement": false
606 },
607 "created_at": {
608 "name": "created_at",
609 "type": "integer",
610 "primaryKey": false,
611 "notNull": true,
612 "autoincrement": false
613 }
614 },
615 "indexes": {
616 "mcp_source_scope_id_idx": {
617 "name": "mcp_source_scope_id_idx",
618 "columns": [
619 "scope_id"
620 ],
621 "isUnique": false
622 }
623 },
624 "foreignKeys": {},
625 "compositePrimaryKeys": {
626 "mcp_source_scope_id_id_pk": {
627 "columns": [
628 "scope_id",
629 "id"
630 ],
631 "name": "mcp_source_scope_id_id_pk"
632 }
633 },
634 "uniqueConstraints": {},
635 "checkConstraints": {}
636 },
637 "openapi_oauth_session": {
638 "name": "openapi_oauth_session",
639 "columns": {
640 "id": {
641 "name": "id",
642 "type": "text",
643 "primaryKey": false,
644 "notNull": true,
645 "autoincrement": false
646 },
647 "scope_id": {
648 "name": "scope_id",
649 "type": "text",
650 "primaryKey": false,
651 "notNull": true,
652 "autoincrement": false
653 },
654 "session": {
655 "name": "session",
656 "type": "text",
657 "primaryKey": false,
658 "notNull": true,
659 "autoincrement": false
660 },
661 "created_at": {
662 "name": "created_at",
663 "type": "integer",
664 "primaryKey": false,
665 "notNull": true,
666 "autoincrement": false
667 }
668 },
669 "indexes": {
670 "openapi_oauth_session_scope_id_idx": {
671 "name": "openapi_oauth_session_scope_id_idx",
672 "columns": [
673 "scope_id"
674 ],
675 "isUnique": false
676 }
677 },
678 "foreignKeys": {},
679 "compositePrimaryKeys": {
680 "openapi_oauth_session_scope_id_id_pk": {
681 "columns": [
682 "scope_id",
683 "id"
684 ],
685 "name": "openapi_oauth_session_scope_id_id_pk"
686 }
687 },
688 "uniqueConstraints": {},
689 "checkConstraints": {}
690 },
691 "openapi_operation": {
692 "name": "openapi_operation",
693 "columns": {
694 "id": {
695 "name": "id",
696 "type": "text",
697 "primaryKey": false,
698 "notNull": true,
699 "autoincrement": false
700 },
701 "scope_id": {
702 "name": "scope_id",
703 "type": "text",
704 "primaryKey": false,
705 "notNull": true,
706 "autoincrement": false
707 },
708 "source_id": {
709 "name": "source_id",
710 "type": "text",
711 "primaryKey": false,
712 "notNull": true,
713 "autoincrement": false
714 },
715 "binding": {
716 "name": "binding",
717 "type": "text",
718 "primaryKey": false,
719 "notNull": true,
720 "autoincrement": false
721 }
722 },
723 "indexes": {
724 "openapi_operation_scope_id_idx": {
725 "name": "openapi_operation_scope_id_idx",
726 "columns": [
727 "scope_id"
728 ],
729 "isUnique": false
730 },
731 "openapi_operation_source_id_idx": {
732 "name": "openapi_operation_source_id_idx",
733 "columns": [
734 "source_id"
735 ],
736 "isUnique": false
737 }
738 },
739 "foreignKeys": {},
740 "compositePrimaryKeys": {
741 "openapi_operation_scope_id_id_pk": {
742 "columns": [
743 "scope_id",
744 "id"
745 ],
746 "name": "openapi_operation_scope_id_id_pk"
747 }
748 },
749 "uniqueConstraints": {},
750 "checkConstraints": {}
751 },
752 "openapi_source": {
753 "name": "openapi_source",
754 "columns": {
755 "id": {
756 "name": "id",
757 "type": "text",
758 "primaryKey": false,
759 "notNull": true,
760 "autoincrement": false
761 },
762 "scope_id": {
763 "name": "scope_id",
764 "type": "text",
765 "primaryKey": false,
766 "notNull": true,
767 "autoincrement": false
768 },
769 "name": {
770 "name": "name",
771 "type": "text",
772 "primaryKey": false,
773 "notNull": true,
774 "autoincrement": false
775 },
776 "spec": {
777 "name": "spec",
778 "type": "text",
779 "primaryKey": false,
780 "notNull": true,
781 "autoincrement": false
782 },
783 "source_url": {
784 "name": "source_url",
785 "type": "text",
786 "primaryKey": false,
787 "notNull": false,
788 "autoincrement": false
789 },
790 "base_url": {
791 "name": "base_url",
792 "type": "text",
793 "primaryKey": false,
794 "notNull": false,
795 "autoincrement": false
796 },
797 "headers": {
798 "name": "headers",
799 "type": "text",
800 "primaryKey": false,
801 "notNull": false,
802 "autoincrement": false
803 },
804 "oauth2": {
805 "name": "oauth2",
806 "type": "text",
807 "primaryKey": false,
808 "notNull": false,
809 "autoincrement": false
810 },
811 "invocation_config": {
812 "name": "invocation_config",
813 "type": "text",
814 "primaryKey": false,
815 "notNull": true,
816 "autoincrement": false
817 }
818 },
819 "indexes": {
820 "openapi_source_scope_id_idx": {
821 "name": "openapi_source_scope_id_idx",
822 "columns": [
823 "scope_id"
824 ],
825 "isUnique": false
826 }
827 },
828 "foreignKeys": {},
829 "compositePrimaryKeys": {
830 "openapi_source_scope_id_id_pk": {
831 "columns": [
832 "scope_id",
833 "id"
834 ],
835 "name": "openapi_source_scope_id_id_pk"
836 }
837 },
838 "uniqueConstraints": {},
839 "checkConstraints": {}
840 },
841 "secret": {
842 "name": "secret",
843 "columns": {
844 "id": {
845 "name": "id",
846 "type": "text",
847 "primaryKey": false,
848 "notNull": true,
849 "autoincrement": false
850 },
851 "scope_id": {
852 "name": "scope_id",
853 "type": "text",
854 "primaryKey": false,
855 "notNull": true,
856 "autoincrement": false
857 },
858 "name": {
859 "name": "name",
860 "type": "text",
861 "primaryKey": false,
862 "notNull": true,
863 "autoincrement": false
864 },
865 "provider": {
866 "name": "provider",
867 "type": "text",
868 "primaryKey": false,
869 "notNull": true,
870 "autoincrement": false
871 },
872 "created_at": {
873 "name": "created_at",
874 "type": "integer",
875 "primaryKey": false,
876 "notNull": true,
877 "autoincrement": false
878 }
879 },
880 "indexes": {
881 "secret_scope_id_idx": {
882 "name": "secret_scope_id_idx",
883 "columns": [
884 "scope_id"
885 ],
886 "isUnique": false
887 },
888 "secret_provider_idx": {
889 "name": "secret_provider_idx",
890 "columns": [
891 "provider"
892 ],
893 "isUnique": false
894 }
895 },
896 "foreignKeys": {},
897 "compositePrimaryKeys": {
898 "secret_scope_id_id_pk": {
899 "columns": [
900 "scope_id",
901 "id"
902 ],
903 "name": "secret_scope_id_id_pk"
904 }
905 },
906 "uniqueConstraints": {},
907 "checkConstraints": {}
908 },
909 "source": {
910 "name": "source",
911 "columns": {
912 "id": {
913 "name": "id",
914 "type": "text",
915 "primaryKey": false,
916 "notNull": true,
917 "autoincrement": false
918 },
919 "scope_id": {
920 "name": "scope_id",
921 "type": "text",
922 "primaryKey": false,
923 "notNull": true,
924 "autoincrement": false
925 },
926 "plugin_id": {
927 "name": "plugin_id",
928 "type": "text",
929 "primaryKey": false,
930 "notNull": true,
931 "autoincrement": false
932 },
933 "kind": {
934 "name": "kind",
935 "type": "text",
936 "primaryKey": false,
937 "notNull": true,
938 "autoincrement": false
939 },
940 "name": {
941 "name": "name",
942 "type": "text",
943 "primaryKey": false,
944 "notNull": true,
945 "autoincrement": false
946 },
947 "url": {
948 "name": "url",
949 "type": "text",
950 "primaryKey": false,
951 "notNull": false,
952 "autoincrement": false
953 },
954 "can_remove": {
955 "name": "can_remove",
956 "type": "integer",
957 "primaryKey": false,
958 "notNull": true,
959 "autoincrement": false,
960 "default": true
961 },
962 "can_refresh": {
963 "name": "can_refresh",
964 "type": "integer",
965 "primaryKey": false,
966 "notNull": true,
967 "autoincrement": false,
968 "default": false
969 },
970 "can_edit": {
971 "name": "can_edit",
972 "type": "integer",
973 "primaryKey": false,
974 "notNull": true,
975 "autoincrement": false,
976 "default": false
977 },
978 "created_at": {
979 "name": "created_at",
980 "type": "integer",
981 "primaryKey": false,
982 "notNull": true,
983 "autoincrement": false
984 },
985 "updated_at": {
986 "name": "updated_at",
987 "type": "integer",
988 "primaryKey": false,
989 "notNull": true,
990 "autoincrement": false
991 }
992 },
993 "indexes": {
994 "source_scope_id_idx": {
995 "name": "source_scope_id_idx",
996 "columns": [
997 "scope_id"
998 ],
999 "isUnique": false
1000 },
1001 "source_plugin_id_idx": {
1002 "name": "source_plugin_id_idx",
1003 "columns": [
1004 "plugin_id"
1005 ],
1006 "isUnique": false
1007 }
1008 },
1009 "foreignKeys": {},
1010 "compositePrimaryKeys": {
1011 "source_scope_id_id_pk": {
1012 "columns": [
1013 "scope_id",
1014 "id"
1015 ],
1016 "name": "source_scope_id_id_pk"
1017 }
1018 },
1019 "uniqueConstraints": {},
1020 "checkConstraints": {}
1021 },
1022 "tool": {
1023 "name": "tool",
1024 "columns": {
1025 "id": {
1026 "name": "id",
1027 "type": "text",
1028 "primaryKey": false,
1029 "notNull": true,
1030 "autoincrement": false
1031 },
1032 "scope_id": {
1033 "name": "scope_id",
1034 "type": "text",
1035 "primaryKey": false,
1036 "notNull": true,
1037 "autoincrement": false
1038 },
1039 "source_id": {
1040 "name": "source_id",
1041 "type": "text",
1042 "primaryKey": false,
1043 "notNull": true,
1044 "autoincrement": false
1045 },
1046 "plugin_id": {
1047 "name": "plugin_id",
1048 "type": "text",
1049 "primaryKey": false,
1050 "notNull": true,
1051 "autoincrement": false
1052 },
1053 "name": {
1054 "name": "name",
1055 "type": "text",
1056 "primaryKey": false,
1057 "notNull": true,
1058 "autoincrement": false
1059 },
1060 "description": {
1061 "name": "description",
1062 "type": "text",
1063 "primaryKey": false,
1064 "notNull": true,
1065 "autoincrement": false
1066 },
1067 "input_schema": {
1068 "name": "input_schema",
1069 "type": "text",
1070 "primaryKey": false,
1071 "notNull": false,
1072 "autoincrement": false
1073 },
1074 "output_schema": {
1075 "name": "output_schema",
1076 "type": "text",
1077 "primaryKey": false,
1078 "notNull": false,
1079 "autoincrement": false
1080 },
1081 "created_at": {
1082 "name": "created_at",
1083 "type": "integer",
1084 "primaryKey": false,
1085 "notNull": true,
1086 "autoincrement": false
1087 },
1088 "updated_at": {
1089 "name": "updated_at",
1090 "type": "integer",
1091 "primaryKey": false,
1092 "notNull": true,
1093 "autoincrement": false
1094 }
1095 },
1096 "indexes": {
1097 "tool_scope_id_idx": {
1098 "name": "tool_scope_id_idx",
1099 "columns": [
1100 "scope_id"
1101 ],
1102 "isUnique": false
1103 },
1104 "tool_source_id_idx": {
1105 "name": "tool_source_id_idx",
1106 "columns": [
1107 "source_id"
1108 ],
1109 "isUnique": false
1110 },
1111 "tool_plugin_id_idx": {
1112 "name": "tool_plugin_id_idx",
1113 "columns": [
1114 "plugin_id"
1115 ],
1116 "isUnique": false
1117 }
1118 },
1119 "foreignKeys": {},
1120 "compositePrimaryKeys": {
1121 "tool_scope_id_id_pk": {
1122 "columns": [
1123 "scope_id",
1124 "id"
1125 ],
1126 "name": "tool_scope_id_id_pk"
1127 }
1128 },
1129 "uniqueConstraints": {},
1130 "checkConstraints": {}
1131 }
1132 },
1133 "views": {},
1134 "enums": {},
1135 "_meta": {
1136 "schemas": {},
1137 "tables": {},
1138 "columns": {}
1139 },
1140 "internal": {
1141 "indexes": {}
1142 }
1143}
apps/local/drizzle/meta/_journal.json+7−0
@@ -8,6 +8,13 @@ · 7 unmodified lines
8 "when": 1776367901699,8 "when": 1776367901699,
9 "tag": "0000_overconfident_sharon_carter",9 "tag": "0000_overconfident_sharon_carter",
10 "breakpoints": true10 "breakpoints": true
11 },
12 {
13 "idx": 1,
14 "version": "6",
15 "when": 1776680432025,
16 "tag": "0001_sour_catseye",
17 "breakpoints": true
11 }18 }
12 ]19 ]
13}20}
apps/local/src/server/executor-schema.ts+1−3
@@ -68,6 +68,7 @@ export const openapi_source = sqliteTable("openapi_source", { · 67 unmodified lines
68 scope_id: text('scope_id').notNull(),68 scope_id: text('scope_id').notNull(),
69 name: text('name').notNull(),69 name: text('name').notNull(),
70 spec: text('spec').notNull(),70 spec: text('spec').notNull(),
71 source_url: text('source_url'),
71 base_url: text('base_url'),72 base_url: text('base_url'),
72 headers: text('headers', { mode: "json" }),73 headers: text('headers', { mode: "json" }),
73 oauth2: text('oauth2', { mode: "json" }),74 oauth2: text('oauth2', { mode: "json" }),
@@ -188,9 +189,6 @@ export const graphql_operation = sqliteTable("graphql_operation", { · 114 unmodified lines
188 index("graphql_operation_source_id_idx").on(table.source_id),189 index("graphql_operation_source_id_idx").on(table.source_id),
189]);190]);
190 191
191// Blob store table — hand-appended. BlobStore is a separate storage
192// abstraction from DBSchema, so the CLI doesn't generate it. Keep in
193// sync with @executor/storage-file's BlobStore implementation.
194export const blob = sqliteTable("blob", {192export const blob = sqliteTable("blob", {
195 namespace: text('namespace').notNull(),193 namespace: text('namespace').notNull(),
196 key: text('key').notNull(),194 key: text('key').notNull(),
apps/local/src/server/mcp-oauth.test.ts+458−0
@@ -0,0 +1,458 @@
1// ---------------------------------------------------------------------------
2// Local app × MCP OAuth — real HTTP end-to-end
3// ---------------------------------------------------------------------------
4//
5// Mirrors apps/cloud/src/services/mcp-oauth.node.test.ts but for the local
6// (sqlite) server. Drives the real LocalApi (core + mcp groups) against a
7// real in-process OAuth + MCP server. Every layer between the test and the
8// plugin is real:
9//
10// test → HttpApiClient → in-process webHandler → LocalApi
11// → McpHandlers → mcpPlugin.startOAuth / completeOAuth
12// → MCP SDK `auth()`
13// → fake OAuth server (DCR, /authorize → 302, /token, AS metadata,
14// protected resource metadata)
15//
16// Single-scope: local has one scope per project (`${folder}-${hash}`) so
17// the OAuth flow lands tokens at that scope and `secrets.resolve` reads
18// them back through the same provider (file-secrets in a tmpdir).
19// ---------------------------------------------------------------------------
20
21import { afterAll, beforeAll, describe, expect, it } from "vitest";
22import { createServer, type Server } from "node:http";
23import type { AddressInfo } from "node:net";
24import { createHash, randomBytes } from "node:crypto";
25import { mkdtempSync, rmSync } from "node:fs";
26import { tmpdir } from "node:os";
27import { join } from "node:path";
28import { Database } from "bun:sqlite";
29import { drizzle } from "drizzle-orm/bun-sqlite";
30import { migrate } from "drizzle-orm/bun-sqlite/migrator";
31
32import {
33 FetchHttpClient,
34 HttpApi,
35 HttpApiBuilder,
36 HttpApiClient,
37 HttpMiddleware,
38 HttpRouter,
39 HttpServer,
40} from "@effect/platform";
41import { Effect, Layer } from "effect";
42
43import {
44 addGroup,
45 InternalError,
46 observabilityMiddleware,
47} from "@executor/api";
48import {
49 CoreHandlers,
50 ExecutionEngineService,
51 ExecutorService,
52} from "@executor/api/server";
53import { createExecutionEngine } from "@executor/execution";
54import { makeQuickJsExecutor } from "@executor/runtime-quickjs";
55import {
56 Scope,
57 ScopeId,
58 SecretId,
59 collectSchemas,
60 createExecutor,
61} from "@executor/sdk";
62import {
63 makeSqliteAdapter,
64 makeSqliteBlobStore,
65} from "@executor/storage-file";
66import { fileSecretsPlugin } from "@executor/plugin-file-secrets";
67import { mcpPlugin } from "@executor/plugin-mcp";
68import {
69 McpExtensionService,
70 McpGroup,
71 McpHandlers,
72} from "@executor/plugin-mcp/api";
73
74import * as executorSchema from "./executor-schema";
75import { ErrorCaptureLive } from "./observability";
76
77// Shape of the test API: core + mcp group, with InternalError surfaced at
78// the top level so `observabilityMiddleware` can land its typed-error
79// bridge on every endpoint.
80const TestApi = addGroup(McpGroup).addError(InternalError);
81type TestApiShape = typeof TestApi extends HttpApi.HttpApi<
82 infer _Id,
83 infer Groups,
84 infer ApiError,
85 infer _ApiR
86>
87 ? HttpApiClient.Client<Groups, ApiError, never>
88 : never;
89
90// ---------------------------------------------------------------------------
91// Fake OAuth + MCP server (mirrors the cloud test)
92// ---------------------------------------------------------------------------
93
94interface FakeServer {
95 readonly url: string;
96 readonly close: () => Promise<void>;
97}
98
99const startFakeServer = async (): Promise<FakeServer> => {
100 const clients = new Map<string, { redirect_uris: readonly string[] }>();
101 const codes = new Map<
102 string,
103 { readonly clientId: string; readonly codeChallenge: string }
104 >();
105 const accessTokens = new Map<string, { readonly refresh: string }>();
106 const refreshTokens = new Map<string, string>();
107 let seq = 0;
108 const next = (p: string) =>
109 `${p}_${++seq}_${randomBytes(6).toString("hex")}`;
110
111 const readBody = (req: import("node:http").IncomingMessage): Promise<string> =>
112 new Promise((resolve, reject) => {
113 let buf = "";
114 req.on("data", (chunk) => (buf += chunk));
115 req.on("end", () => resolve(buf));
116 req.on("error", reject);
117 });
118
119 const server: Server = createServer(async (req, res) => {
120 const url = new URL(req.url!, `http://${req.headers.host}`);
121 const send = (
122 status: number,
123 body: unknown,
124 headers: Record<string, string> = {},
125 ) => {
126 const payload = typeof body === "string" ? body : JSON.stringify(body);
127 res.writeHead(status, {
128 "content-type":
129 typeof body === "string" ? "text/plain" : "application/json",
130 ...headers,
131 });
132 res.end(payload);
133 };
134
135 try {
136 if (url.pathname === "/.well-known/oauth-protected-resource") {
137 const origin = `http://${req.headers.host}`;
138 return send(200, {
139 resource: origin,
140 authorization_servers: [origin],
141 bearer_methods_supported: ["header"],
142 });
143 }
144
145 if (url.pathname === "/.well-known/oauth-authorization-server") {
146 const issuer = `http://${req.headers.host}`;
147 return send(200, {
148 issuer,
149 authorization_endpoint: `${issuer}/authorize`,
150 token_endpoint: `${issuer}/token`,
151 registration_endpoint: `${issuer}/register`,
152 response_types_supported: ["code"],
153 grant_types_supported: ["authorization_code", "refresh_token"],
154 code_challenge_methods_supported: ["S256"],
155 token_endpoint_auth_methods_supported: ["none"],
156 });
157 }
158
159 if (url.pathname === "/register" && req.method === "POST") {
160 const body = await readBody(req);
161 const parsed = JSON.parse(body) as {
162 readonly redirect_uris?: readonly string[];
163 readonly grant_types?: readonly string[];
164 readonly response_types?: readonly string[];
165 };
166 const clientId = next("client");
167 clients.set(clientId, { redirect_uris: parsed.redirect_uris ?? [] });
168 return send(201, {
169 client_id: clientId,
170 client_id_issued_at: Math.floor(Date.now() / 1000),
171 redirect_uris: parsed.redirect_uris ?? [],
172 grant_types: parsed.grant_types ?? [
173 "authorization_code",
174 "refresh_token",
175 ],
176 response_types: parsed.response_types ?? ["code"],
177 token_endpoint_auth_method: "none",
178 });
179 }
180
181 if (url.pathname === "/authorize" && req.method === "GET") {
182 const clientId = url.searchParams.get("client_id") ?? "";
183 const redirectUri = url.searchParams.get("redirect_uri") ?? "";
184 const state = url.searchParams.get("state") ?? "";
185 const codeChallenge = url.searchParams.get("code_challenge") ?? "";
186 const method = url.searchParams.get("code_challenge_method") ?? "";
187 if (!clients.has(clientId)) {
188 return send(400, { error: "unknown_client" });
189 }
190 if (method !== "S256" || !codeChallenge) {
191 return send(400, { error: "invalid_request" });
192 }
193 const code = next("code");
194 codes.set(code, { clientId, codeChallenge });
195 const destination = new URL(redirectUri);
196 destination.searchParams.set("code", code);
197 if (state) destination.searchParams.set("state", state);
198 return send(302, "", { location: destination.toString() });
199 }
200
201 if (url.pathname === "/token" && req.method === "POST") {
202 const body = await readBody(req);
203 const params = new URLSearchParams(body);
204 const grant = params.get("grant_type");
205
206 if (grant === "authorization_code") {
207 const code = params.get("code") ?? "";
208 const verifier = params.get("code_verifier") ?? "";
209 const record = codes.get(code);
210 if (!record) return send(400, { error: "invalid_grant" });
211 codes.delete(code);
212 const computed = createHash("sha256")
213 .update(verifier)
214 .digest("base64url");
215 if (computed !== record.codeChallenge) {
216 return send(400, { error: "invalid_grant" });
217 }
218 const access = next("at");
219 const refresh = next("rt");
220 accessTokens.set(access, { refresh });
221 refreshTokens.set(refresh, access);
222 return send(200, {
223 access_token: access,
224 refresh_token: refresh,
225 token_type: "Bearer",
226 expires_in: 3600,
227 });
228 }
229
230 return send(400, { error: "unsupported_grant_type" });
231 }
232
233 if (url.pathname === "/mcp") {
234 const origin = `http://${req.headers.host}`;
235 return send(
236 401,
237 { error: "unauthorized" },
238 {
239 "www-authenticate": `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource"`,
240 },
241 );
242 }
243
244 send(404, { error: "not_found", path: url.pathname });
245 } catch (e) {
246 send(500, { error: "server_error", message: String(e) });
247 }
248 });
249
250 await new Promise<void>((resolve) =>
251 server.listen(0, "127.0.0.1", () => resolve()),
252 );
253 const address = server.address() as AddressInfo;
254
255 return {
256 url: `http://127.0.0.1:${address.port}`,
257 close: () => new Promise<void>((resolve) => server.close(() => resolve())),
258 };
259};
260
261// ---------------------------------------------------------------------------
262// In-process local API harness — tmpdir sqlite + minimal plugin set.
263// ---------------------------------------------------------------------------
264
265const TEST_BASE_URL = "http://local.test";
266const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle");
267
268interface Harness {
269 readonly fetch: typeof globalThis.fetch;
270 readonly scopeId: string;
271 readonly dispose: () => Promise<void>;
272}
273
274const startHarness = async (tmpDir: string): Promise<Harness> => {
275 const sqlite = new Database(join(tmpDir, "data.db"));
276 sqlite.exec("PRAGMA journal_mode = WAL");
277 const db = drizzle(sqlite, { schema: executorSchema });
278 migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
279
280 const scopeId = `test-${randomBytes(4).toString("hex")}`;
281 const plugins = [
282 mcpPlugin({ dangerouslyAllowStdioMCP: false }),
283 fileSecretsPlugin({ directory: tmpDir }),
284 ] as const;
285 const schema = collectSchemas(plugins);
286 const adapter = makeSqliteAdapter({ db, schema });
287 const blobs = makeSqliteBlobStore({ db });
288
289 const scope = new Scope({
290 id: ScopeId.make(scopeId),
291 name: "test",
292 createdAt: new Date(),
293 });
294
295 const executor = await Effect.runPromise(
296 createExecutor({ scopes: [scope], adapter, blobs, plugins }),
297 );
298
299 const engine = createExecutionEngine({
300 executor,
301 codeExecutor: makeQuickJsExecutor(),
302 });
303
304 const TestObservability = observabilityMiddleware(TestApi);
305
306 const TestApiBase = HttpApiBuilder.api(TestApi).pipe(
307 Layer.provide(CoreHandlers),
308 Layer.provide(McpHandlers),
309 Layer.provide(TestObservability),
310 Layer.provide(ErrorCaptureLive),
311 );
312
313 const pluginExtensions = Layer.succeed(McpExtensionService, executor.mcp);
314
315 const { handler: webHandler, dispose: disposeHandler } =
316 HttpApiBuilder.toWebHandler(
317 TestApiBase.pipe(
318 Layer.provideMerge(pluginExtensions),
319 Layer.provideMerge(Layer.succeed(ExecutorService, executor)),
320 Layer.provideMerge(Layer.succeed(ExecutionEngineService, engine)),
321 Layer.provideMerge(HttpServer.layerContext),
322 Layer.provideMerge(HttpRouter.setRouterConfig({ maxParamLength: 1000 })),
323 ),
324 { middleware: HttpMiddleware.logger },
325 );
326
327 return {
328 fetch: ((input: RequestInfo | URL, init?: RequestInit) =>
329 webHandler(
330 input instanceof Request ? input : new Request(input, init),
331 )) as typeof globalThis.fetch,
332 scopeId,
333 dispose: async () => {
334 await disposeHandler().catch(() => undefined);
335 await Effect.runPromise(executor.close()).catch(() => undefined);
336 sqlite.close();
337 },
338 };
339};
340
341// ---------------------------------------------------------------------------
342// Lifecycle
343// ---------------------------------------------------------------------------
344
345let fake: FakeServer;
346let tmpDir: string;
347let harness: Harness;
348
349beforeAll(async () => {
350 fake = await startFakeServer();
351 tmpDir = mkdtempSync(join(tmpdir(), "executor-local-mcp-"));
352 harness = await startHarness(tmpDir);
353});
354
355afterAll(async () => {
356 await harness.dispose();
357 rmSync(tmpDir, { recursive: true, force: true });
358 await fake.close();
359});
360
361// ---------------------------------------------------------------------------
362// Helpers
363// ---------------------------------------------------------------------------
364
365const followAuthorize = async (
366 authorizationUrl: string,
367): Promise<{ code: string; state: string }> => {
368 const response = await fetch(authorizationUrl, { redirect: "manual" });
369 expect(response.status).toBe(302);
370 const location = response.headers.get("location");
371 if (!location) throw new Error("no location header on authorize redirect");
372 const dest = new URL(location);
373 const code = dest.searchParams.get("code");
374 const state = dest.searchParams.get("state");
375 if (!code || !state)
376 throw new Error(`redirect missing code/state: ${location}`);
377 return { code, state };
378};
379
380// ---------------------------------------------------------------------------
381// Test
382// ---------------------------------------------------------------------------
383
384describe("local mcp oauth (real OAuth + MCP server)", () => {
385 it("startOAuth → authorize → completeOAuth writes tokens at the scope", async () => {
386 const clientLayer = FetchHttpClient.layer.pipe(
387 Layer.provide(Layer.succeed(FetchHttpClient.Fetch, harness.fetch)),
388 );
389
390 const namespace = `ns_${randomBytes(4).toString("hex")}`;
391 const accessTokenSecretId = `mcp_${namespace}_access_token`;
392 const refreshTokenSecretId = `mcp_${namespace}_refresh_token`;
393 const redirectUrl = "http://local.test/api/mcp/oauth/callback";
394 const scopeId = ScopeId.make(harness.scopeId);
395
396 const run = <A, E>(
397 body: (client: TestApiShape) => Effect.Effect<A, E>,
398 ): Effect.Effect<A, E> =>
399 Effect.gen(function* () {
400 const client = yield* HttpApiClient.make(TestApi, {
401 baseUrl: TEST_BASE_URL,
402 });
403 return yield* body(client);
404 }).pipe(Effect.provide(clientLayer)) as Effect.Effect<A, E>;
405
406 const started = await Effect.runPromise(
407 run((client) =>
408 client.mcp.startOAuth({
409 path: { scopeId },
410 payload: {
411 endpoint: `${fake.url}/mcp`,
412 redirectUrl,
413 accessTokenSecretId,
414 refreshTokenSecretId,
415 },
416 }),
417 ),
418 );
419 expect(started.sessionId).toMatch(/^mcp_oauth_/);
420
421 const { code, state } = await followAuthorize(started.authorizationUrl);
422 expect(state).toBe(started.sessionId);
423
424 const completed = await Effect.runPromise(
425 run((client) =>
426 client.mcp.completeOAuth({
427 path: { scopeId },
428 payload: { state, code },
429 }),
430 ),
431 );
432 expect(completed.accessTokenSecretId).toBe(accessTokenSecretId);
433 expect(completed.refreshTokenSecretId).toBe(refreshTokenSecretId);
434 expect(completed.tokenType).toBe("Bearer");
435 expect(completed.clientInformation).not.toBeNull();
436 expect(completed.authorizationServerUrl).not.toBeNull();
437
438 // Tokens landed at the (single) local scope and resolve back through
439 // the same provider that the plugin wrote them to.
440 const access = await Effect.runPromise(
441 run((client) =>
442 client.secrets.resolve({
443 path: { scopeId, secretId: SecretId.make(accessTokenSecretId) },
444 }),
445 ),
446 );
447 expect(access.value).toMatch(/^at_/);
448
449 const refresh = await Effect.runPromise(
450 run((client) =>
451 client.secrets.resolve({
452 path: { scopeId, secretId: SecretId.make(refreshTokenSecretId) },
453 }),
454 ),
455 );
456 expect(refresh.value).toMatch(/^rt_/);
457 }, 30_000);
458});
packages/plugins/mcp/src/sdk/presets.ts+8−0
@@ -87,6 +87,14 @@ export const mcpPresets: readonly McpPreset[] = [ · 86 unmodified lines
87 icon: "https://linear.app/favicon.ico",87 icon: "https://linear.app/favicon.ico",
88 featured: true,88 featured: true,
89 },89 },
90 {
91 id: "notion",
92 name: "Notion",
93 summary: "Databases, pages, blocks, and search via MCP.",
94 url: "https://mcp.notion.com/mcp",
95 icon: "https://www.notion.so/favicon.ico",
96 featured: true,
97 },
90 {98 {
91 id: "sentry",99 id: "sentry",
92 name: "Sentry",100 name: "Sentry",
packages/plugins/openapi/src/sdk/plugin.ts+161−83
@@ -18,6 +18,7 @@ import { · 17 unmodified lines
18 SetSecretInput,18 SetSecretInput,
19 SourceDetectionResult,19 SourceDetectionResult,
20 definePlugin,20 definePlugin,
21 type PluginCtx,
21 type StorageFailure,22 type StorageFailure,
22 type ToolAnnotations,23 type ToolAnnotations,
23 type ToolRow,24 type ToolRow,
@@ -270,10 +271,154 @@ const toOpenApiSourceConfig = ( · 246 unmodified lines
270 headers: headersToConfigValues(config.headers),271 headers: headersToConfigValues(config.headers),
271});272});
272 273
274const isHttpUrl = (s: string): boolean =>
275 s.startsWith("http://") || s.startsWith("https://");
276
273export const openApiPlugin = definePlugin(277export const openApiPlugin = definePlugin(
274 (options?: OpenApiPluginOptions) => {278 (options?: OpenApiPluginOptions) => {
275 const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer;279 const httpClientLayer = options?.httpClientLayer ?? FetchHttpClient.layer;
276 280
281 type RebuildInput = {
282 readonly specText: string;
283 readonly scope: string;
284 readonly sourceUrl?: string;
285 readonly name?: string;
286 readonly baseUrl?: string;
287 readonly namespace?: string;
288 readonly headers?: Record<string, HeaderValue>;
289 readonly oauth2?: OAuth2Auth;
290 };
291
292 // ctx comes from the plugin runtime — the same instance is passed to
293 // `extension(ctx)` and to every lifecycle hook (`refreshSource`, etc.),
294 // so helpers parameterised on ctx can be called from either surface.
295 const rebuildSource = (
296 ctx: PluginCtx<OpenapiStore>,
297 input: RebuildInput,
298 ) =>
299 Effect.gen(function* () {
300 const doc = yield* parse(input.specText);
301 const result = yield* extract(doc);
302
303 const namespace =
304 input.namespace ??
305 Option.getOrElse(result.title, () => "api")
306 .toLowerCase()
307 .replace(/[^a-z0-9]+/g, "_");
308
309 const hoistedDefs: Record<string, unknown> = {};
310 if (doc.components?.schemas) {
311 for (const [k, v] of Object.entries(doc.components.schemas)) {
312 hoistedDefs[k] = normalizeOpenApiRefs(v);
313 }
314 }
315
316 const baseUrl = input.baseUrl ?? resolveBaseUrl(result.servers);
317 const oauth2 = input.oauth2 ?? undefined;
318 const invocationConfig = new InvocationConfig({
319 baseUrl,
320 headers: input.headers ?? {},
321 oauth2: oauth2 ? Option.some(oauth2) : Option.none(),
322 });
323
324 const definitions = compileToolDefinitions(result.operations);
325 const sourceName =
326 input.name ?? Option.getOrElse(result.title, () => namespace);
327
328 const sourceConfig: SourceConfig = {
329 spec: input.specText,
330 sourceUrl: input.sourceUrl,
331 baseUrl: input.baseUrl,
332 namespace: input.namespace,
333 headers: input.headers,
334 oauth2,
335 };
336
337 const storedSource: StoredSource = {
338 namespace,
339 scope: input.scope,
340 name: sourceName,
341 config: sourceConfig,
342 invocationConfig,
343 };
344
345 const storedOps: StoredOperation[] = definitions.map((def) => ({
346 toolId: `${namespace}.${def.toolPath}`,
347 sourceId: namespace,
348 binding: toBinding(def),
349 }));
350
351 yield* ctx.transaction(
352 Effect.gen(function* () {
353 yield* ctx.storage.upsertSource(storedSource, storedOps);
354
355 yield* ctx.core.sources.register({
356 id: namespace,
357 scope: input.scope,
358 kind: "openapi",
359 name: sourceName,
360 url: baseUrl || undefined,
361 canRemove: true,
362 // `canRefresh` reflects whether we still know the
363 // origin URL — sources added from raw spec text have
364 // nothing to re-fetch, so refresh stays disabled.
365 canRefresh: input.sourceUrl != null,
366 canEdit: true,
367 tools: definitions.map((def) => ({
368 name: def.toolPath,
369 description: descriptionFor(def),
370 inputSchema: normalizeOpenApiRefs(
371 Option.getOrUndefined(def.operation.inputSchema),
372 ),
373 outputSchema: normalizeOpenApiRefs(
374 Option.getOrUndefined(def.operation.outputSchema),
375 ),
376 })),
377 });
378
379 if (Object.keys(hoistedDefs).length > 0) {
380 yield* ctx.core.definitions.register({
381 sourceId: namespace,
382 scope: input.scope,
383 definitions: hoistedDefs,
384 });
385 }
386 }),
387 );
388
389 return { sourceId: namespace, toolCount: definitions.length };
390 });
391
392 // No-op for missing sources and for sources added from raw spec
393 // text (no URL to re-fetch from). UIs gate the action via
394 // `canRefresh` on the source row; reaching here without a URL
395 // means the caller bypassed that gate, so we stay quiet rather
396 // than surface a 500 through the unwhitelisted error channel.
397 const refreshSourceInternal = (
398 ctx: PluginCtx<OpenapiStore>,
399 sourceId: string,
400 scope: string,
401 ) =>
402 Effect.gen(function* () {
403 const existing = yield* ctx.storage.getSource(sourceId, scope);
404 if (!existing) return;
405 const sourceUrl = existing.config.sourceUrl;
406 if (!sourceUrl) return;
407 const specText = yield* resolveSpecText(sourceUrl).pipe(
408 Effect.provide(httpClientLayer),
409 );
410 yield* rebuildSource(ctx, {
411 specText,
412 scope,
413 sourceUrl,
414 name: existing.name,
415 baseUrl: existing.config.baseUrl,
416 namespace: existing.namespace,
417 headers: existing.config.headers,
418 oauth2: existing.config.oauth2,
419 });
420 });
421
277 return {422 return {
278 id: "openapi" as const,423 id: "openapi" as const,
279 schema: openapiSchema,424 schema: openapiSchema,
@@ -310,92 +455,16 @@ export const openApiPlugin = definePlugin( · 30 unmodified lines
310 const specText = yield* resolveSpecText(config.spec).pipe(455 const specText = yield* resolveSpecText(config.spec).pipe(
311 Effect.provide(httpClientLayer),456 Effect.provide(httpClientLayer),
312 );457 );
313 const doc = yield* parse(specText);458 return yield* rebuildSource(ctx, {
314 const result = yield* extract(doc);459 specText,
315 460 scope: config.scope,
316 const namespace =461 sourceUrl: isHttpUrl(config.spec) ? config.spec : undefined,
317 config.namespace ??462 name: config.name,
318 Option.getOrElse(result.title, () => "api")
319 .toLowerCase()
320 .replace(/[^a-z0-9]+/g, "_");
321
322 const hoistedDefs: Record<string, unknown> = {};
323 if (doc.components?.schemas) {
324 for (const [k, v] of Object.entries(doc.components.schemas)) {
325 hoistedDefs[k] = normalizeOpenApiRefs(v);
326 }
327 }
328
329 const baseUrl = config.baseUrl ?? resolveBaseUrl(result.servers);
330 const oauth2 = config.oauth2 ?? undefined;
331 const invocationConfig = new InvocationConfig({
332 baseUrl,
333 headers: config.headers ?? {},
334 oauth2: oauth2 ? Option.some(oauth2) : Option.none(),
335 });
336
337 const definitions = compileToolDefinitions(result.operations);
338 const sourceName =
339 config.name ?? Option.getOrElse(result.title, () => namespace);
340
341 const sourceConfig: SourceConfig = {
342 spec: config.spec,
343 baseUrl: config.baseUrl,463 baseUrl: config.baseUrl,
344 namespace: config.namespace,464 namespace: config.namespace,
345 headers: config.headers,465 headers: config.headers,
346 oauth2,466 oauth2: config.oauth2,
347 };467 });
348
349 const storedSource: StoredSource = {
350 namespace,
351 scope: config.scope,
352 name: sourceName,
353 config: sourceConfig,
354 invocationConfig,
355 };
356
357 const storedOps: StoredOperation[] = definitions.map((def) => ({
358 toolId: `${namespace}.${def.toolPath}`,
359 sourceId: namespace,
360 binding: toBinding(def),
361 }));
362
363 yield* ctx.transaction(
364 Effect.gen(function* () {
365 yield* ctx.storage.upsertSource(storedSource, storedOps);
366
367 yield* ctx.core.sources.register({
368 id: namespace,
369 scope: config.scope,
370 kind: "openapi",
371 name: sourceName,
372 url: baseUrl || undefined,
373 canRemove: true,
374 canRefresh: false,
375 canEdit: true,
376 tools: definitions.map((def) => ({
377 name: def.toolPath,
378 description: descriptionFor(def),
379 inputSchema: normalizeOpenApiRefs(
380 Option.getOrUndefined(def.operation.inputSchema),
381 ),
382 outputSchema: normalizeOpenApiRefs(
383 Option.getOrUndefined(def.operation.outputSchema),
384 ),
385 })),
386 });
387
388 if (Object.keys(hoistedDefs).length > 0) {
389 yield* ctx.core.definitions.register({
390 sourceId: namespace,
391 scope: config.scope,
392 definitions: hoistedDefs,
393 });
394 }
395 }),
396 );
397
398 return { sourceId: namespace, toolCount: definitions.length };
399 });468 });
400 469
401 const configFile = options?.configFile;470 const configFile = options?.configFile;
@@ -802,6 +871,15 @@ export const openApiPlugin = definePlugin( · 400 unmodified lines
802 removeSource: ({ ctx, sourceId, scope }) =>871 removeSource: ({ ctx, sourceId, scope }) =>
803 ctx.storage.removeSource(sourceId, scope),872 ctx.storage.removeSource(sourceId, scope),
804 873
874 // Re-fetch the spec from its origin URL (captured at addSpec time)
875 // and replay the same parse → extract → upsertSource → register
876 // path used by addSpec. Sources without a stored URL surface a
877 // typed `OpenApiParseError` — the executor only dispatches refresh
878 // when `canRefresh: true`, so a raw-text source reaching here
879 // means stale UI state, which is worth surfacing to the caller.
880 refreshSource: ({ ctx, sourceId, scope }) =>
881 refreshSourceInternal(ctx, sourceId, scope),
882
805 detect: ({ url }) =>883 detect: ({ url }) =>
806 Effect.gen(function* () {884 Effect.gen(function* () {
807 const trimmed = url.trim();885 const trimmed = url.trim();
packages/plugins/openapi/src/sdk/store.ts+11−0
@@ -29,6 +29,11 @@ export const openapiSchema = defineSchema({ · 28 unmodified lines
29 scope_id: { type: "string", required: true, index: true },29 scope_id: { type: "string", required: true, index: true },
30 name: { type: "string", required: true },30 name: { type: "string", required: true },
31 spec: { type: "string", required: true },31 spec: { type: "string", required: true },
32 // Origin URL the spec was fetched from. Set when `addSpec` was
33 // invoked with an http(s) URL; null when the caller passed raw
34 // spec text. Drives `canRefresh` on the core source row and
35 // is the address re-fetched on `refreshSource`.
36 source_url: { type: "string", required: false },
32 base_url: { type: "string", required: false },37 base_url: { type: "string", required: false },
33 headers: { type: "json", required: false },38 headers: { type: "json", required: false },
34 oauth2: { type: "json", required: false },39 oauth2: { type: "json", required: false },
@@ -62,6 +67,9 @@ export type OpenapiSchema = typeof openapiSchema; · 27 unmodified lines
62 67
63export interface SourceConfig {68export interface SourceConfig {
64 readonly spec: string;69 readonly spec: string;
70 /** Origin URL when the spec was fetched from http(s). Absent for
71 * raw-text adds. Persisted so `refreshSource` can re-fetch. */
72 readonly sourceUrl?: string;
65 readonly baseUrl?: string;73 readonly baseUrl?: string;
66 readonly namespace?: string;74 readonly namespace?: string;
67 readonly headers?: Record<string, HeaderValue>;75 readonly headers?: Record<string, HeaderValue>;
@@ -92,6 +100,7 @@ export class StoredSourceSchema extends Schema.Class<StoredSourceSchema>( · 24 unmodified lines
92 name: Schema.String,100 name: Schema.String,
93 config: Schema.Struct({101 config: Schema.Struct({
94 spec: Schema.String,102 spec: Schema.String,
103 sourceUrl: Schema.optional(Schema.String),
95 baseUrl: Schema.optional(Schema.String),104 baseUrl: Schema.optional(Schema.String),
96 namespace: Schema.optional(Schema.String),105 namespace: Schema.optional(Schema.String),
97 headers: Schema.optional(106 headers: Schema.optional(
@@ -239,6 +248,7 @@ export const makeDefaultOpenapiStore = ({ · 141 unmodified lines
239 name: row.name as string,248 name: row.name as string,
240 config: {249 config: {
241 spec: row.spec as string,250 spec: row.spec as string,
251 sourceUrl: (row.source_url as string | null | undefined) ?? undefined,
242 baseUrl: (row.base_url as string | null | undefined) ?? undefined,252 baseUrl: (row.base_url as string | null | undefined) ?? undefined,
243 headers,253 headers,
244 oauth2,254 oauth2,
@@ -284,6 +294,7 @@ export const makeDefaultOpenapiStore = ({ · 39 unmodified lines
284 scope_id: input.scope,294 scope_id: input.scope,
285 name: input.name,295 name: input.name,
286 spec: input.config.spec,296 spec: input.config.spec,
297 source_url: input.config.sourceUrl ?? undefined,
287 base_url: input.config.baseUrl ?? undefined,298 base_url: input.config.baseUrl ?? undefined,
288 headers: (input.config.headers ?? {}) as unknown as Record<string, unknown>,299 headers: (input.config.headers ?? {}) as unknown as Record<string, unknown>,
289 oauth2: input.config.oauth2300 oauth2: input.config.oauth2