(rawSlug: string, input?: OpenApiUpdateSpecInput)
| 961 | // the curated description, connections, and policies are all untouched - |
| 962 | // this is the "spec changed upstream" path, not a re-add. |
| 963 | const updateSpec = (rawSlug: string, input?: OpenApiUpdateSpecInput) => |
| 964 | Effect.gen(function* () { |
| 965 | const slug = IntegrationSlug.make(rawSlug); |
| 966 | const record = yield* ctx.core.integrations.get(slug); |
| 967 | const current = record ? decodeOpenApiIntegrationConfig(record.config) : null; |
| 968 | if (!record || !current) { |
| 969 | return yield* new IntegrationNotFoundError({ slug }); |
| 970 | } |
| 971 | |
| 972 | // The new spec source: explicit input wins; otherwise re-fetch from |
| 973 | // where the spec originally came from. A pasted-blob integration has |
| 974 | // no origin, so updating it requires a new input. |
| 975 | const nextOverrides = input?.specOverrides ?? current.specOverrides ?? []; |
| 976 | const storedSourceHash = current.sourceSpecHash ?? current.specHash; |
| 977 | const storedSourceText = |
| 978 | input?.spec === undefined && !current.specUrl && input?.specOverrides !== undefined |
| 979 | ? storedSourceHash |
| 980 | ? yield* ctx.storage.getSpec(storedSourceHash) |
| 981 | : null |
| 982 | : null; |
| 983 | const specInput: OpenApiSpecInput | null = |
| 984 | input?.spec ?? |
| 985 | (current.specUrl |
| 986 | ? { kind: "url", url: current.specUrl } |
| 987 | : storedSourceText |
| 988 | ? { kind: "blob", value: storedSourceText } |
| 989 | : null); |
| 990 | if (specInput === null) { |
| 991 | return yield* new OpenApiParseError({ |
| 992 | message: |
| 993 | "This integration's spec was pasted inline and has no source URL to re-fetch. Provide the updated spec content.", |
| 994 | }); |
| 995 | } |
| 996 | |
| 997 | // Resolve + compile BEFORE the transaction (same Hyperdrive-deadlock |
| 998 | // rule as addSpec: never hold BEGIN across a network fetch). |
| 999 | const resolved = yield* resolveSpecForInput( |
| 1000 | { |
| 1001 | spec: specInput, |
| 1002 | specFormat: current.specFormat, |
| 1003 | specOverrides: nextOverrides, |
| 1004 | headers: current.headers, |
| 1005 | queryParams: current.queryParams, |
| 1006 | baseUrl: current.baseUrl, |
| 1007 | authenticationTemplate: current.authenticationTemplate, |
| 1008 | }, |
| 1009 | httpClientLayer, |
| 1010 | ); |
| 1011 | const compiled = resolved.keepPathItem |
| 1012 | ? undefined |
| 1013 | : yield* compileOpenApiSpec(resolved.specText); |
| 1014 | |
| 1015 | const previousOperations = yield* ctx.storage.listOperations(rawSlug); |
| 1016 | const previousNames = new Set(previousOperations.map((op) => op.toolName)); |
| 1017 | |
| 1018 | // The resolved spec text lives in the plugin blob store keyed by its |
| 1019 | // content hash (`spec/<hash>`); the config carries only the hash. Put |
| 1020 | // the blob outside the transaction - re-puts are idempotent and an |
nothing calls this directly
no test coverage detected