- scope handler: surfaces outermost scope (executor.scopes.at(-1)) instead of the old single scope. - secrets handler: set takes path.scopeId and threads it as SetSecretInput.scope so the executor writes at the right scope. - secret-header-auth: exports CreatableSecretPicker + InlineCreateSecret for reuse by the OpenAPI connections pane.
Unified · Split
| @@ -9,10 +9,15 @@ export const ScopeHandlers = HttpApiBuilder.group(ExecutorApi, "scope", (handler · 8 unmodified lines | |||
| 9 | handlers.handle("info", () => | 9 | handlers.handle("info", () => |
| 10 | capture(Effect.gen(function* () { | 10 | capture(Effect.gen(function* () { |
| 11 | const executor = yield* ExecutorService; | 11 | const executor = yield* ExecutorService; |
| 12 | // Outermost scope (organization / workspace). A follow-up that | ||
| 13 | // exposes per-user stacks end-to-end can extend this response | ||
| 14 | // with the full list; for now, single-scope deployments and the | ||
| 15 | // current `[org]` cloud setup see identical output. | ||
| 16 | const scope = executor.scopes.at(-1)!; | ||
| 12 | return { | 17 | return { |
| 13 | id: executor.scope.id, | 18 | id: scope.id, |
| 14 | name: executor.scope.name, | 19 | name: scope.name, |
| 15 | dir: executor.scope.name, | 20 | dir: scope.name, |
| 16 | }; | 21 | }; |
| 17 | })), | 22 | })), |
| 18 | ), | 23 | ), |
| @@ -1,6 +1,6 @@ | |||
| 1 | import { HttpApiBuilder } from "@effect/platform"; | 1 | import { HttpApiBuilder } from "@effect/platform"; |
| 2 | import { Effect } from "effect"; | 2 | import { Effect } from "effect"; |
| 3 | import { SecretNotFoundError, type SecretRef } from "@executor/sdk"; | 3 | import { SecretNotFoundError, SetSecretInput, type SecretRef } from "@executor/sdk"; |
| 4 | 4 | ||
| 5 | import { ExecutorApi } from "../api"; | 5 | import { ExecutorApi } from "../api"; |
| 6 | import { ExecutorService } from "../services"; | 6 | import { ExecutorService } from "../services"; |
| @@ -30,15 +30,18 @@ export const SecretsHandlers = HttpApiBuilder.group(ExecutorApi, "secrets", (han · 23 unmodified lines | |||
| 30 | return { secretId: path.secretId, status }; | 30 | return { secretId: path.secretId, status }; |
| 31 | })), | 31 | })), |
| 32 | ) | 32 | ) |
| 33 | .handle("set", ({ payload }) => | 33 | .handle("set", ({ path, payload }) => |
| 34 | capture(Effect.gen(function* () { | 34 | capture(Effect.gen(function* () { |
| 35 | const executor = yield* ExecutorService; | 35 | const executor = yield* ExecutorService; |
| 36 | const ref = yield* executor.secrets.set({ | 36 | const ref = yield* executor.secrets.set( |
| 37 | id: payload.id, | 37 | new SetSecretInput({ |
| 38 | name: payload.name, | 38 | id: payload.id, |
| 39 | value: payload.value, | 39 | scope: path.scopeId, |
| 40 | provider: payload.provider, | 40 | name: payload.name, |
| 41 | }); | 41 | value: payload.value, |
| 42 | provider: payload.provider, | ||
| 43 | }), | ||
| 44 | ); | ||
| 42 | return refToResponse(ref); | 45 | return refToResponse(ref); |
| 43 | })), | 46 | })), |
| 44 | ) | 47 | ) |
| @@ -68,7 +68,7 @@ function slugifyForSecretId(input: string): string { · 67 unmodified lines | |||
| 68 | .replace(/^-+|-+$/g, ""); | 68 | .replace(/^-+|-+$/g, ""); |
| 69 | } | 69 | } |
| 70 | 70 | ||
| 71 | function InlineCreateSecret(props: { | 71 | export function InlineCreateSecret(props: { |
| 72 | suggestedId: string; | 72 | suggestedId: string; |
| 73 | suggestedName: string; | 73 | suggestedName: string; |
| 74 | onCreated: (secretId: string) => void; | 74 | onCreated: (secretId: string) => void; |
| @@ -416,3 +416,51 @@ export function SecretHeaderAuthRow(props: { · 341 unmodified lines | |||
| 416 | </div> | 416 | </div> |
| 417 | ); | 417 | ); |
| 418 | } | 418 | } |
| 419 | |||
| 420 | // --------------------------------------------------------------------------- | ||
| 421 | // CreatableSecretPicker — SecretPicker + inline "+ New secret" create flow | ||
| 422 | // --------------------------------------------------------------------------- | ||
| 423 | |||
| 424 | export function CreatableSecretPicker(props: { | ||
| 425 | readonly value: string | null; | ||
| 426 | readonly onSelect: (secretId: string) => void; | ||
| 427 | readonly secrets: readonly SecretPickerSecret[]; | ||
| 428 | readonly placeholder?: string; | ||
| 429 | /** | ||
| 430 | * Display name of the source the secret belongs to (e.g. "Stripe"). | ||
| 431 | * Combined with `secretLabel` to produce a suggested name/ID. | ||
| 432 | */ | ||
| 433 | readonly sourceName?: string; | ||
| 434 | /** Role of this secret (e.g. "Client ID", "API Token"). */ | ||
| 435 | readonly secretLabel: string; | ||
| 436 | }) { | ||
| 437 | const { value, onSelect, secrets, placeholder, sourceName, secretLabel } = props; | ||
| 438 | const [creating, setCreating] = useState(false); | ||
| 439 | |||
| 440 | const suggestedName = [sourceName?.trim(), secretLabel].filter(Boolean).join(" "); | ||
| 441 | const suggestedId = slugifyForSecretId(suggestedName) || "secret"; | ||
| 442 | |||
| 443 | if (creating) { | ||
| 444 | return ( | ||
| 445 | <InlineCreateSecret | ||
| 446 | suggestedId={suggestedId} | ||
| 447 | suggestedName={suggestedName} | ||
| 448 | onCreated={(id) => { | ||
| 449 | onSelect(id); | ||
| 450 | setCreating(false); | ||
| 451 | }} | ||
| 452 | onCancel={() => setCreating(false)} | ||
| 453 | /> | ||
| 454 | ); | ||
| 455 | } | ||
| 456 | |||
| 457 | return ( | ||
| 458 | <SecretPicker | ||
| 459 | value={value} | ||
| 460 | onSelect={onSelect} | ||
| 461 | secrets={secrets} | ||
| 462 | placeholder={placeholder} | ||
| 463 | onCreateNew={() => setCreating(true)} | ||
| 464 | /> | ||
| 465 | ); | ||
| 466 | } | ||