config-sync calls openapi.addSpec without ever passing credentialTargetScope. addSpecInternal forwarded the field unchanged to rebuildSource, where targetScopeForBinding rejected the missing field with "credentialTargetScope is required when adding direct OpenAPI credentials" — so any source with a header secret in executor.jsonc broke the daemon at startup. Fall back to config.scope, matching what refreshSource and editSource already do (refreshSource hardcodes scope; editSource uses input.credentialTargetScope ?? scope). Add a regression test that exercises addSpec with header secrets but no credentialTargetScope — the test fails on main and passes after the fix.
Unified · Split
| @@ -549,6 +549,57 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 548 unmodified lines | |||
| 549 | }), | 549 | }), |
| 550 | ); | 550 | ); |
| 551 | 551 | ||
| 552 | it.effect("addSpec without credentialTargetScope defaults to the source's scope", () => | ||
| 553 | // Regression: config-sync calls addSpec without ever setting | ||
| 554 | // credentialTargetScope. Before the fix, any source with a | ||
| 555 | // header secret in executor.jsonc errored with | ||
| 556 | // "credentialTargetScope is required when adding direct OpenAPI | ||
| 557 | // credentials" the moment the daemon started. | ||
| 558 | Effect.gen(function* () { | ||
| 559 | const httpClient = yield* HttpClient.HttpClient; | ||
| 560 | const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient); | ||
| 561 | |||
| 562 | const executor = yield* createExecutor( | ||
| 563 | makeTestConfig({ | ||
| 564 | plugins: [ | ||
| 565 | openApiPlugin({ httpClientLayer: clientLayer }), | ||
| 566 | memorySecretsPlugin(), | ||
| 567 | ] as const, | ||
| 568 | }), | ||
| 569 | ); | ||
| 570 | |||
| 571 | yield* executor.secrets.set( | ||
| 572 | new SetSecretInput({ | ||
| 573 | id: SecretId.make("config-sync-token"), | ||
| 574 | scope: ScopeId.make(TEST_SCOPE), | ||
| 575 | name: "Config-sync token", | ||
| 576 | value: "secret-from-jsonc", | ||
| 577 | }), | ||
| 578 | ); | ||
| 579 | |||
| 580 | yield* executor.openapi.addSpec({ | ||
| 581 | spec: specJson, | ||
| 582 | scope: TEST_SCOPE, | ||
| 583 | namespace: "default_target_scope", | ||
| 584 | baseUrl: "", | ||
| 585 | headers: { | ||
| 586 | Authorization: { secretId: "config-sync-token", prefix: "Bearer " }, | ||
| 587 | }, | ||
| 588 | }); | ||
| 589 | |||
| 590 | const bindings = yield* executor.openapi.listSourceBindings( | ||
| 591 | "default_target_scope", | ||
| 592 | TEST_SCOPE, | ||
| 593 | ); | ||
| 594 | expect(bindings).toHaveLength(1); | ||
| 595 | expect(bindings[0]).toMatchObject({ | ||
| 596 | scopeId: ScopeId.make(TEST_SCOPE), | ||
| 597 | slot: "header:authorization", | ||
| 598 | value: { kind: "secret", secretId: SecretId.make("config-sync-token") }, | ||
| 599 | }); | ||
| 600 | }), | ||
| 601 | ); | ||
| 602 | |||
| 552 | it.effect("fails clearly when a secret is missing", () => | 603 | it.effect("fails clearly when a secret is missing", () => |
| 553 | Effect.gen(function* () { | 604 | Effect.gen(function* () { |
| 554 | const httpClient = yield* HttpClient.HttpClient; | 605 | const httpClient = yield* HttpClient.HttpClient; |
| @@ -1004,7 +1004,11 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 1003 unmodified lines | |||
| 1004 | queryParams: config.queryParams, | 1004 | queryParams: config.queryParams, |
| 1005 | specFetchCredentials: config.specFetchCredentials, | 1005 | specFetchCredentials: config.specFetchCredentials, |
| 1006 | oauth2: config.oauth2, | 1006 | oauth2: config.oauth2, |
| 1007 | credentialTargetScope: config.credentialTargetScope, | 1007 | // Default to the source's own scope. refreshSource and editSource |
| 1008 | // do the same; without this, config-sync's addSpec — which never | ||
| 1009 | // passes the field — fails with "credentialTargetScope is | ||
| 1010 | // required" the moment the jsonc declares any header secret. | ||
| 1011 | credentialTargetScope: config.credentialTargetScope ?? config.scope, | ||
| 1008 | }); | 1012 | }); |
| 1009 | }); | 1013 | }); |
| 1010 | 1014 | ||