(rawSlug: string, input?: OpenApiUpdateSpecInput)
| 989 | // the curated description, connections, and policies are all untouched - |
| 990 | // this is the "spec changed upstream" path, not a re-add. |
| 991 | const updateSpec = (rawSlug: string, input?: OpenApiUpdateSpecInput) => |
| 992 | Effect.gen(function* () { |
| 993 | const slug = IntegrationSlug.make(rawSlug); |
| 994 | const record = yield* ctx.core.integrations.get(slug); |
| 995 | const current = record ? decodeOpenApiIntegrationConfig(record.config) : null; |
| 996 | if (!record || !current) { |
| 997 | return yield* new IntegrationNotFoundError({ slug }); |
| 998 | } |
| 999 | yield* ctx.core.integrations.authorizeWrite(); |
| 1000 | |
| 1001 | // The new spec source: explicit input wins; otherwise re-fetch from |
| 1002 | // where the spec originally came from. A pasted-blob integration has |
| 1003 | // no origin, so updating it requires a new input. |
| 1004 | const nextOverrides = input?.specOverrides ?? current.specOverrides ?? []; |
| 1005 | const storedSourceHash = current.sourceSpecHash ?? current.specHash; |
| 1006 | const storedSourceText = |
| 1007 | input?.spec === undefined && !current.specUrl && input?.specOverrides !== undefined |
| 1008 | ? storedSourceHash |
| 1009 | ? yield* ctx.storage.getSpec(storedSourceHash) |
| 1010 | : null |
| 1011 | : null; |
| 1012 | const specInput: OpenApiSpecInput | null = |
| 1013 | input?.spec ?? |
| 1014 | (current.specUrl |
| 1015 | ? { kind: "url", url: current.specUrl } |
| 1016 | : storedSourceText |
| 1017 | ? { kind: "blob", value: storedSourceText } |
| 1018 | : null); |
| 1019 | if (specInput === null) { |
| 1020 | return yield* new OpenApiParseError({ |
| 1021 | message: |
| 1022 | "This integration's spec was pasted inline and has no source URL to re-fetch. Provide the updated spec content.", |
| 1023 | }); |
| 1024 | } |
| 1025 | |
| 1026 | // Resolve + compile BEFORE the transaction (same Hyperdrive-deadlock |
| 1027 | // rule as addSpec: never hold BEGIN across a network fetch). |
| 1028 | const resolved = yield* resolveSpecForInput( |
| 1029 | { |
| 1030 | spec: specInput, |
| 1031 | specFormat: current.specFormat, |
| 1032 | specOverrides: nextOverrides, |
| 1033 | headers: current.headers, |
| 1034 | queryParams: current.queryParams, |
| 1035 | baseUrl: current.baseUrl, |
| 1036 | authenticationTemplate: current.authenticationTemplate, |
| 1037 | }, |
| 1038 | httpClientLayer, |
| 1039 | ); |
| 1040 | const compiled = resolved.keepPathItem |
| 1041 | ? undefined |
| 1042 | : yield* compileOpenApiSpec(resolved.specText); |
| 1043 | |
| 1044 | const previousOperations = yield* ctx.storage.listOperations(rawSlug); |
| 1045 | const previousNames = new Set(previousOperations.map((op) => op.toolName)); |
| 1046 | |
| 1047 | // The resolved spec text lives in the plugin blob store keyed by its |
| 1048 | // content hash (`spec/<hash>`); the config carries only the hash. Put |
nothing calls this directly
no test coverage detected