[codex] clean up plugin-based secret management
Unified · Split
| @@ -571,6 +571,7 @@ · 570 unmodified lines | ||
| 571 | 571 | "version": "0.0.0", |
| 572 | 572 | "dependencies": { |
| 573 | 573 | "@executor/platform-sdk": "workspace:*", |
| 574 | + "@napi-rs/keyring": "^1.2.0", | |
| 574 | 575 | "effect": "catalog:", |
| 575 | 576 | }, |
| 576 | 577 | "devDependencies": { |
| @@ -1202,6 +1203,32 @@ · 625 unmodified lines | ||
| 1202 | 1203 | |
| 1203 | 1204 | "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], |
| 1204 | 1205 | |
| 1206 | + "@napi-rs/keyring": ["@napi-rs/keyring@1.2.0", "", { "optionalDependencies": { "@napi-rs/keyring-darwin-arm64": "1.2.0", "@napi-rs/keyring-darwin-x64": "1.2.0", "@napi-rs/keyring-freebsd-x64": "1.2.0", "@napi-rs/keyring-linux-arm-gnueabihf": "1.2.0", "@napi-rs/keyring-linux-arm64-gnu": "1.2.0", "@napi-rs/keyring-linux-arm64-musl": "1.2.0", "@napi-rs/keyring-linux-riscv64-gnu": "1.2.0", "@napi-rs/keyring-linux-x64-gnu": "1.2.0", "@napi-rs/keyring-linux-x64-musl": "1.2.0", "@napi-rs/keyring-win32-arm64-msvc": "1.2.0", "@napi-rs/keyring-win32-ia32-msvc": "1.2.0", "@napi-rs/keyring-win32-x64-msvc": "1.2.0" } }, "sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg=="], | |
| 1207 | + | |
| 1208 | + "@napi-rs/keyring-darwin-arm64": ["@napi-rs/keyring-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA=="], | |
| 1209 | + | |
| 1210 | + "@napi-rs/keyring-darwin-x64": ["@napi-rs/keyring-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-dBHjtKRCj4ByfnfqIKIJLo3wueQNJhLRyuxtX/rR4K/XtcS7VLlRD01XXizjpre54vpmObj63w+ZpHG+mGM8uA=="], | |
| 1211 | + | |
| 1212 | + "@napi-rs/keyring-freebsd-x64": ["@napi-rs/keyring-freebsd-x64@1.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DPZFr11pNJSnaoh0dzSUNF+T6ORhy3CkzUT3uGixbA71cAOPJ24iG8e8QrLOkuC/StWrAku3gBnth2XMWOcR3Q=="], | |
| 1213 | + | |
| 1214 | + "@napi-rs/keyring-linux-arm-gnueabihf": ["@napi-rs/keyring-linux-arm-gnueabihf@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-8xv6DyEMlvRdqJzp4F39RLUmmTQsLcGYYv/3eIfZNZN1O5257tHxTrFYqAsny659rJJK2EKeSa7PhrSibQqRWQ=="], | |
| 1215 | + | |
| 1216 | + "@napi-rs/keyring-linux-arm64-gnu": ["@napi-rs/keyring-linux-arm64-gnu@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Pu2V6Py+PBt7inryEecirl+t+ti8bhZphjP+W68iVaXHUxLdWmkgL9KI1VkbRHbx5k8K5Tew9OP218YfmVguIA=="], | |
| 1217 | + | |
| 1218 | + "@napi-rs/keyring-linux-arm64-musl": ["@napi-rs/keyring-linux-arm64-musl@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw=="], | |
| 1219 | + | |
| 1220 | + "@napi-rs/keyring-linux-riscv64-gnu": ["@napi-rs/keyring-linux-riscv64-gnu@1.2.0", "", { "os": "linux", "cpu": "none" }, "sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw=="], | |
| 1221 | + | |
| 1222 | + "@napi-rs/keyring-linux-x64-gnu": ["@napi-rs/keyring-linux-x64-gnu@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ=="], | |
| 1223 | + | |
| 1224 | + "@napi-rs/keyring-linux-x64-musl": ["@napi-rs/keyring-linux-x64-musl@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA=="], | |
| 1225 | + | |
| 1226 | + "@napi-rs/keyring-win32-arm64-msvc": ["@napi-rs/keyring-win32-arm64-msvc@1.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ=="], | |
| 1227 | + | |
| 1228 | + "@napi-rs/keyring-win32-ia32-msvc": ["@napi-rs/keyring-win32-ia32-msvc@1.2.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EYL+EEI6bCsYi3LfwcQdnX3P/R76ENKNn+3PmpGheBsUFLuh0gQuP7aMVHM4rTw6UVe+L3vCLZSptq/oeacz0A=="], | |
| 1229 | + | |
| 1230 | + "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA=="], | |
| 1231 | + | |
| 1205 | 1232 | "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], |
| 1206 | 1233 | |
| 1207 | 1234 | "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], |
| @@ -65,6 +65,10 @@ type SecretMaterialSummary = { · 64 unmodified lines | ||
| 65 | 65 | updatedAt: number; |
| 66 | 66 | }; |
| 67 | 67 | |
| 68 | +type SqliteSecretStoredData = { | |
| 69 | + value: string; | |
| 70 | +}; | |
| 71 | + | |
| 68 | 72 | const SQLITE_SECRET_STORE_KIND = "sqlite"; |
| 69 | 73 | const SQLITE_SECRET_STORE_ID = "sts_sqlite"; |
| 70 | 74 | |
| @@ -92,7 +96,7 @@ const sourceArtifacts = sqliteTable("source_artifacts", { · 21 unmodified lines | ||
| 92 | 96 | |
| 93 | 97 | const secretMaterials = sqliteTable("secret_materials", { |
| 94 | 98 | id: text("id").primaryKey(), |
| 95 | − providerId: text("provider_id").notNull(), | |
| 99 | + storeId: text("store_id").notNull(), | |
| 96 | 100 | name: text("name"), |
| 97 | 101 | purpose: text("purpose").notNull(), |
| 98 | 102 | createdAt: integer("created_at").notNull(), |
| @@ -100,6 +104,11 @@ const secretMaterials = sqliteTable("secret_materials", { · 1 unmodified line | ||
| 100 | 104 | json: text("json").notNull(), |
| 101 | 105 | }); |
| 102 | 106 | |
| 107 | +const secretMaterialStoredData = sqliteTable("secret_material_stored_data", { | |
| 108 | + secretId: text("secret_id").primaryKey(), | |
| 109 | + json: text("json").notNull(), | |
| 110 | +}); | |
| 111 | + | |
| 103 | 112 | const executions = sqliteTable("executions", { |
| 104 | 113 | id: text("id").primaryKey(), |
| 105 | 114 | scopeId: text("scope_id").notNull(), |
| @@ -213,7 +222,7 @@ const openSqliteStore = (databasePath: string) => { · 107 unmodified lines | ||
| 213 | 222 | |
| 214 | 223 | CREATE TABLE IF NOT EXISTS secret_materials ( |
| 215 | 224 | id TEXT PRIMARY KEY NOT NULL, |
| 216 | − provider_id TEXT NOT NULL, | |
| 225 | + store_id TEXT NOT NULL, | |
| 217 | 226 | name TEXT, |
| 218 | 227 | purpose TEXT NOT NULL, |
| 219 | 228 | created_at INTEGER NOT NULL, |
| @@ -221,6 +230,11 @@ const openSqliteStore = (databasePath: string) => { · 1 unmodified line | ||
| 221 | 230 | json TEXT NOT NULL |
| 222 | 231 | ); |
| 223 | 232 | |
| 233 | + CREATE TABLE IF NOT EXISTS secret_material_stored_data ( | |
| 234 | + secret_id TEXT PRIMARY KEY NOT NULL, | |
| 235 | + json TEXT NOT NULL | |
| 236 | + ); | |
| 237 | + | |
| 224 | 238 | CREATE TABLE IF NOT EXISTS executions ( |
| 225 | 239 | id TEXT PRIMARY KEY NOT NULL, |
| 226 | 240 | scope_id TEXT NOT NULL, |
| @@ -241,6 +255,7 @@ const openSqliteStore = (databasePath: string) => { · 14 unmodified lines | ||
| 241 | 255 | json TEXT NOT NULL |
| 242 | 256 | ); |
| 243 | 257 | `); |
| 258 | + | |
| 244 | 259 | return { |
| 245 | 260 | sqlite, |
| 246 | 261 | db, |
| @@ -292,7 +307,7 @@ const createStorageDomains = ( · 45 unmodified lines | ||
| 292 | 307 | listAll: (): readonly SecretMaterialSummary[] => |
| 293 | 308 | store.db.select().from(secretMaterials).all().map((row) => ({ |
| 294 | 309 | id: row.id, |
| 295 | − storeId: row.providerId, | |
| 310 | + storeId: row.storeId, | |
| 296 | 311 | name: row.name, |
| 297 | 312 | purpose: row.purpose, |
| 298 | 313 | createdAt: row.createdAt, |
| @@ -301,7 +316,7 @@ const createStorageDomains = ( · 2 unmodified lines | ||
| 301 | 316 | upsert: (material: SecretMaterial) => |
| 302 | 317 | store.db.insert(secretMaterials).values({ |
| 303 | 318 | id: material.id, |
| 304 | − providerId: material.storeId, | |
| 319 | + storeId: material.storeId, | |
| 305 | 320 | name: material.name, |
| 306 | 321 | purpose: material.purpose, |
| 307 | 322 | createdAt: material.createdAt, |
| @@ -310,7 +325,7 @@ const createStorageDomains = ( · 2 unmodified lines | ||
| 310 | 325 | }).onConflictDoUpdate({ |
| 311 | 326 | target: secretMaterials.id, |
| 312 | 327 | set: { |
| 313 | − providerId: material.storeId, | |
| 328 | + storeId: material.storeId, | |
| 314 | 329 | name: material.name, |
| 315 | 330 | purpose: material.purpose, |
| 316 | 331 | createdAt: material.createdAt, |
| @@ -320,7 +335,7 @@ const createStorageDomains = ( · 3 unmodified lines | ||
| 320 | 335 | }).run(), |
| 321 | 336 | updateById: ( |
| 322 | 337 | id: SecretMaterial["id"], |
| 323 | − update: { name?: string | null; value?: string }, | |
| 338 | + update: { name?: string | null }, | |
| 324 | 339 | ) => { |
| 325 | 340 | const row = store.db.select().from(secretMaterials).where(eq(secretMaterials.id, id)).get(); |
| 326 | 341 | if (!row) return null; |
| @@ -328,11 +343,10 @@ const createStorageDomains = ( · 1 unmodified line | ||
| 328 | 343 | const next = { |
| 329 | 344 | ...current, |
| 330 | 345 | name: update.name === undefined ? current.name : update.name, |
| 331 | − value: update.value === undefined ? current.value : update.value, | |
| 332 | 346 | updatedAt: Date.now(), |
| 333 | 347 | }; |
| 334 | 348 | store.db.update(secretMaterials).set({ |
| 335 | − providerId: next.storeId, | |
| 349 | + storeId: next.storeId, | |
| 336 | 350 | name: next.name, |
| 337 | 351 | purpose: next.purpose, |
| 338 | 352 | createdAt: next.createdAt, |
| @@ -356,6 +370,40 @@ const createStorageDomains = ( · 17 unmodified lines | ||
| 356 | 370 | store.db.delete(secretMaterials).where(eq(secretMaterials.id, id)).run(); |
| 357 | 371 | return true; |
| 358 | 372 | }, |
| 373 | + secretMaterialStoredData: { | |
| 374 | + getBySecretId: (secretId: SecretMaterial["id"]) => { | |
| 375 | + const row = store.db.select().from(secretMaterialStoredData).where( | |
| 376 | + eq(secretMaterialStoredData.secretId, secretId), | |
| 377 | + ).get(); | |
| 378 | + return row | |
| 379 | + ? { | |
| 380 | + secretId, | |
| 381 | + data: parseJson<SqliteSecretStoredData>(row.json), | |
| 382 | + } | |
| 383 | + : null; | |
| 384 | + }, | |
| 385 | + upsert: (record: { secretId: SecretMaterial["id"]; data: unknown }) => { | |
| 386 | + store.db.insert(secretMaterialStoredData).values({ | |
| 387 | + secretId: record.secretId, | |
| 388 | + json: JSON.stringify(record.data), | |
| 389 | + }).onConflictDoUpdate({ | |
| 390 | + target: secretMaterialStoredData.secretId, | |
| 391 | + set: { | |
| 392 | + json: JSON.stringify(record.data), | |
| 393 | + }, | |
| 394 | + }).run(); | |
| 395 | + }, | |
| 396 | + removeBySecretId: (secretId: SecretMaterial["id"]) => { | |
| 397 | + const row = store.db.select({ secretId: secretMaterialStoredData.secretId }).from( | |
| 398 | + secretMaterialStoredData, | |
| 399 | + ).where(eq(secretMaterialStoredData.secretId, secretId)).get(); | |
| 400 | + if (!row) return false; | |
| 401 | + store.db.delete(secretMaterialStoredData).where( | |
| 402 | + eq(secretMaterialStoredData.secretId, secretId), | |
| 403 | + ).run(); | |
| 404 | + return true; | |
| 405 | + }, | |
| 406 | + }, | |
| 359 | 407 | }, |
| 360 | 408 | executions: { |
| 361 | 409 | runs: { |
| @@ -612,14 +660,18 @@ export const createSqliteExecutorBackend = ( · 250 unmodified lines | ||
| 612 | 660 | secrets: { |
| 613 | 661 | ...secrets, |
| 614 | 662 | secretStores: secrets.secretStores, |
| 663 | + secretMaterialStoredData: secrets.secretMaterialStoredData, | |
| 615 | 664 | resolve: ({ ref }) => { |
| 616 | 665 | const material = secrets.getById( |
| 617 | 666 | ref.secretId as SecretMaterial["id"], |
| 618 | 667 | ); |
| 619 | − if (!material || material.value === null) { | |
| 668 | + const stored = secrets.secretMaterialStoredData.getBySecretId( | |
| 669 | + ref.secretId as SecretMaterial["id"], | |
| 670 | + ); | |
| 671 | + if (!material || !stored) { | |
| 620 | 672 | throw new Error(`Missing secret material ${ref.secretId}`); |
| 621 | 673 | } |
| 622 | − return material.value; | |
| 674 | + return (stored.data as SqliteSecretStoredData).value; | |
| 623 | 675 | }, |
| 624 | 676 | store: ({ purpose, value, name, storeId }) => { |
| 625 | 677 | const now = Date.now(); |
| @@ -627,28 +679,44 @@ export const createSqliteExecutorBackend = ( · 1 unmodified line | ||
| 627 | 679 | const material: SecretMaterial = { |
| 628 | 680 | id, |
| 629 | 681 | storeId: (storeId ?? SQLITE_SECRET_STORE_ID) as SecretMaterial["storeId"], |
| 630 | − handle: id, | |
| 631 | 682 | name: name ?? null, |
| 632 | 683 | purpose, |
| 633 | − value, | |
| 634 | 684 | createdAt: now, |
| 635 | 685 | updatedAt: now, |
| 636 | 686 | }; |
| 637 | 687 | secrets.upsert(material); |
| 688 | + secrets.secretMaterialStoredData.upsert({ | |
| 689 | + secretId: id, | |
| 690 | + data: { | |
| 691 | + value, | |
| 692 | + } satisfies SqliteSecretStoredData, | |
| 693 | + }); | |
| 638 | 694 | return { |
| 639 | 695 | secretId: material.id, |
| 640 | 696 | } satisfies SecretRef; |
| 641 | 697 | }, |
| 642 | − delete: (ref) => | |
| 643 | − secrets.removeById(ref.secretId as SecretMaterial["id"]), | |
| 698 | + delete: (ref) => { | |
| 699 | + secrets.secretMaterialStoredData.removeBySecretId( | |
| 700 | + ref.secretId as SecretMaterial["id"], | |
| 701 | + ); | |
| 702 | + return secrets.removeById(ref.secretId as SecretMaterial["id"]); | |
| 703 | + }, | |
| 644 | 704 | update: ({ ref, name, value }) => { |
| 645 | 705 | const updated = secrets.updateById( |
| 646 | 706 | ref.secretId as SecretMaterial["id"], |
| 647 | − { name, value }, | |
| 707 | + { name }, | |
| 648 | 708 | ); |
| 649 | 709 | if (!updated) { |
| 650 | 710 | throw new Error(`Missing secret material ${ref.secretId}`); |
| 651 | 711 | } |
| 712 | + if (value !== undefined) { | |
| 713 | + secrets.secretMaterialStoredData.upsert({ | |
| 714 | + secretId: ref.secretId as SecretMaterial["id"], | |
| 715 | + data: { | |
| 716 | + value, | |
| 717 | + } satisfies SqliteSecretStoredData, | |
| 718 | + }); | |
| 719 | + } | |
| 652 | 720 | return updated; |
| 653 | 721 | }, |
| 654 | 722 | }, |
| @@ -11,7 +11,6 @@ import { · 10 unmodified lines | ||
| 11 | 11 | LocalExecutorConfigSchema, |
| 12 | 12 | type LocalExecutorConfig, |
| 13 | 13 | type LocalConfigPolicy, |
| 14 | − type LocalConfigSecretProvider, | |
| 15 | 14 | type LocalConfigSource, |
| 16 | 15 | } from "@executor/platform-sdk/schema"; |
| 17 | 16 | import type { LoadedLocalExecutorConfig } from "@executor/platform-sdk/runtime"; |
| @@ -249,19 +248,6 @@ const mergePolicyMaps = ( · 231 unmodified lines | ||
| 249 | 248 | }; |
| 250 | 249 | }; |
| 251 | 250 | |
| 252 | −const mergeSecretProviderMaps = ( | |
| 253 | − base: Record<string, LocalConfigSecretProvider> | undefined, | |
| 254 | − extra: Record<string, LocalConfigSecretProvider> | undefined, | |
| 255 | −): Record<string, LocalConfigSecretProvider> | undefined => { | |
| 256 | − if (!base && !extra) { | |
| 257 | − return undefined; | |
| 258 | − } | |
| 259 | − return { | |
| 260 | − ...base, | |
| 261 | − ...extra, | |
| 262 | − }; | |
| 263 | −}; | |
| 264 | − | |
| 265 | 251 | export const mergeLocalExecutorConfigs = ( |
| 266 | 252 | base: LocalExecutorConfig | null, |
| 267 | 253 | extra: LocalExecutorConfig | null, |
| @@ -278,16 +264,6 @@ export const mergeLocalExecutorConfigs = ( · 10 unmodified lines | ||
| 278 | 264 | }, |
| 279 | 265 | sources: mergeSourceMaps(base?.sources, extra?.sources), |
| 280 | 266 | policies: mergePolicyMaps(base?.policies, extra?.policies), |
| 281 | − secrets: { | |
| 282 | − providers: mergeSecretProviderMaps( | |
| 283 | − base?.secrets?.providers, | |
| 284 | − extra?.secrets?.providers, | |
| 285 | − ), | |
| 286 | − defaults: { | |
| 287 | − ...base?.secrets?.defaults, | |
| 288 | − ...extra?.secrets?.defaults, | |
| 289 | − }, | |
| 290 | − }, | |
| 291 | 267 | }); |
| 292 | 268 | }; |
| 293 | 269 | |
| @@ -26,13 +26,21 @@ import { · 25 unmodified lines | ||
| 26 | 26 | unknownLocalErrorDetails, |
| 27 | 27 | } from "./errors"; |
| 28 | 28 | |
| 29 | −const LOCAL_EXECUTOR_STATE_VERSION = 1 as const; | |
| 29 | +const LOCAL_EXECUTOR_STATE_VERSION = 2 as const; | |
| 30 | 30 | const LOCAL_EXECUTOR_STATE_BASENAME = "executor-state.json"; |
| 31 | 31 | |
| 32 | +const SecretMaterialStoredDataRecordSchema = Schema.Struct({ | |
| 33 | + secretId: Schema.String, | |
| 34 | + data: Schema.Unknown, | |
| 35 | +}); | |
| 36 | + | |
| 37 | +type SecretMaterialStoredDataRecord = typeof SecretMaterialStoredDataRecordSchema.Type; | |
| 38 | + | |
| 32 | 39 | const LocalExecutorStateSnapshotSchema = Schema.Struct({ |
| 33 | 40 | version: Schema.Literal(LOCAL_EXECUTOR_STATE_VERSION), |
| 34 | 41 | secretStores: Schema.Array(SecretStoreSchema), |
| 35 | 42 | secretMaterials: Schema.Array(SecretMaterialSchema), |
| 43 | + secretMaterialStoredData: Schema.Array(SecretMaterialStoredDataRecordSchema), | |
| 36 | 44 | executions: Schema.Array(ExecutionSchema), |
| 37 | 45 | executionInteractions: Schema.Array(ExecutionInteractionSchema), |
| 38 | 46 | executionSteps: Schema.Array(ExecutionStepSchema), |
| @@ -53,6 +61,7 @@ const defaultLocalExecutorStateSnapshot = (): LocalExecutorStateSnapshot => ({ · 14 unmodified lines | ||
| 53 | 61 | version: LOCAL_EXECUTOR_STATE_VERSION, |
| 54 | 62 | secretStores: [], |
| 55 | 63 | secretMaterials: [], |
| 64 | + secretMaterialStoredData: [], | |
| 56 | 65 | executions: [], |
| 57 | 66 | executionInteractions: [], |
| 58 | 67 | executionSteps: [], |
| @@ -60,7 +69,6 @@ const defaultLocalExecutorStateSnapshot = (): LocalExecutorStateSnapshot => ({ · 1 unmodified line | ||
| 60 | 69 | |
| 61 | 70 | const cloneValue = <T>(value: T): T => |
| 62 | 71 | JSON.parse(JSON.stringify(value)) as T; |
| 63 | − | |
| 64 | 72 | const mapFileSystemError = (path: string, action: string) => (cause: unknown) => |
| 65 | 73 | new LocalFileSystemError({ |
| 66 | 74 | message: `Failed to ${action} ${path}: ${unknownLocalErrorDetails(cause)}`, |
| @@ -156,18 +164,19 @@ export const writeLocalExecutorStateSnapshot = (input: { · 89 unmodified lines | ||
| 156 | 164 | }): Effect.Effect<void, LocalFileSystemError> => |
| 157 | 165 | bindNodeFileSystem(writeStateToDisk(input.context, input.state)); |
| 158 | 166 | |
| 159 | −const mergeById = <T extends { id: string }>( | |
| 167 | +const mergeByKey = <T>( | |
| 160 | 168 | current: readonly T[], |
| 161 | 169 | imported: readonly T[], |
| 170 | + getKey: (value: T) => string, | |
| 162 | 171 | ): T[] => { |
| 163 | 172 | const merged = new Map<string, T>(); |
| 164 | 173 | |
| 165 | 174 | for (const item of imported) { |
| 166 | − merged.set(item.id, cloneValue(item)); | |
| 175 | + merged.set(getKey(item), cloneValue(item)); | |
| 167 | 176 | } |
| 168 | 177 | |
| 169 | 178 | for (const item of current) { |
| 170 | − merged.set(item.id, cloneValue(item)); | |
| 179 | + merged.set(getKey(item), cloneValue(item)); | |
| 171 | 180 | } |
| 172 | 181 | |
| 173 | 182 | return [...merged.values()]; |
| @@ -178,22 +187,35 @@ export const mergeImportedLocalExecutorStateSnapshot = (input: { · 4 unmodified lines | ||
| 178 | 187 | imported: Partial<Omit<LocalExecutorStateSnapshot, "version">>; |
| 179 | 188 | }): LocalExecutorStateSnapshot => ({ |
| 180 | 189 | version: LOCAL_EXECUTOR_STATE_VERSION, |
| 181 | − secretStores: mergeById( | |
| 190 | + secretStores: mergeByKey( | |
| 182 | 191 | input.current.secretStores, |
| 183 | 192 | input.imported.secretStores ?? [], |
| 193 | + (item) => item.id, | |
| 184 | 194 | ), |
| 185 | − secretMaterials: mergeById( | |
| 195 | + secretMaterials: mergeByKey( | |
| 186 | 196 | input.current.secretMaterials, |
| 187 | 197 | input.imported.secretMaterials ?? [], |
| 198 | + (item) => item.id, | |
| 188 | 199 | ), |
| 189 | − executions: mergeById(input.current.executions, input.imported.executions ?? []), | |
| 190 | − executionInteractions: mergeById( | |
| 200 | + secretMaterialStoredData: mergeByKey( | |
| 201 | + input.current.secretMaterialStoredData, | |
| 202 | + input.imported.secretMaterialStoredData ?? [], | |
| 203 | + (item) => item.secretId, | |
| 204 | + ), | |
| 205 | + executions: mergeByKey( | |
| 206 | + input.current.executions, | |
| 207 | + input.imported.executions ?? [], | |
| 208 | + (item) => item.id, | |
| 209 | + ), | |
| 210 | + executionInteractions: mergeByKey( | |
| 191 | 211 | input.current.executionInteractions, |
| 192 | 212 | input.imported.executionInteractions ?? [], |
| 213 | + (item) => item.id, | |
| 193 | 214 | ), |
| 194 | − executionSteps: mergeById( | |
| 215 | + executionSteps: mergeByKey( | |
| 195 | 216 | input.current.executionSteps, |
| 196 | 217 | input.imported.executionSteps ?? [], |
| 218 | + (item) => item.id, | |
| 197 | 219 | ), |
| 198 | 220 | }); |
| 199 | 221 | |
| @@ -390,7 +412,7 @@ export const createLocalExecutorStateStore = ( · 190 unmodified lines | ||
| 390 | 412 | |
| 391 | 413 | updateById: ( |
| 392 | 414 | id: SecretMaterial["id"], |
| 393 | − update: { name?: string | null; value?: string }, | |
| 415 | + update: { name?: string | null }, | |
| 394 | 416 | ) => |
| 395 | 417 | stateManager.mutate((state) => { |
| 396 | 418 | let updated: SecretMaterial | null = null; |
| @@ -402,7 +424,6 @@ export const createLocalExecutorStateStore = ( · 5 unmodified lines | ||
| 402 | 424 | updated = { |
| 403 | 425 | ...material, |
| 404 | 426 | ...(update.name !== undefined ? { name: update.name } : {}), |
| 405 | − ...(update.value !== undefined ? { value: update.value } : {}), | |
| 406 | 427 | updatedAt: Date.now(), |
| 407 | 428 | } satisfies SecretMaterial; |
| 408 | 429 | return updated; |
| @@ -432,6 +453,48 @@ export const createLocalExecutorStateStore = ( · 23 unmodified lines | ||
| 432 | 453 | } satisfies StateMutationResult<boolean>; |
| 433 | 454 | }), |
| 434 | 455 | }, |
| 456 | + secretMaterialStoredData: { | |
| 457 | + getBySecretId: (secretId: SecretMaterial["id"]) => | |
| 458 | + stateManager.read((state) => { | |
| 459 | + const record = state.secretMaterialStoredData.find( | |
| 460 | + (candidate) => candidate.secretId === secretId, | |
| 461 | + ); | |
| 462 | + return record | |
| 463 | + ? Option.some(cloneValue(record)) | |
| 464 | + : Option.none<SecretMaterialStoredDataRecord>(); | |
| 465 | + }), | |
| 466 | + | |
| 467 | + upsert: (record: SecretMaterialStoredDataRecord) => | |
| 468 | + stateManager.mutate((state) => { | |
| 469 | + const nextRecords = state.secretMaterialStoredData.filter( | |
| 470 | + (candidate) => candidate.secretId !== record.secretId, | |
| 471 | + ); | |
| 472 | + nextRecords.push(cloneValue(record)); | |
| 473 | + | |
| 474 | + return { | |
| 475 | + state: { | |
| 476 | + ...state, | |
| 477 | + secretMaterialStoredData: nextRecords, | |
| 478 | + }, | |
| 479 | + value: undefined, | |
| 480 | + } satisfies StateMutationResult<void>; | |
| 481 | + }), | |
| 482 | + | |
| 483 | + removeBySecretId: (secretId: SecretMaterial["id"]) => | |
| 484 | + stateManager.mutate((state) => { | |
| 485 | + const nextRecords = state.secretMaterialStoredData.filter( | |
| 486 | + (candidate) => candidate.secretId !== secretId, | |
| 487 | + ); | |
| 488 | + | |
| 489 | + return { | |
| 490 | + state: { | |
| 491 | + ...state, | |
| 492 | + secretMaterialStoredData: nextRecords, | |
| 493 | + }, | |
| 494 | + value: nextRecords.length !== state.secretMaterialStoredData.length, | |
| 495 | + } satisfies StateMutationResult<boolean>; | |
| 496 | + }), | |
| 497 | + }, | |
| 435 | 498 | executions: { |
| 436 | 499 | listByScope: (scopeId: Execution["scopeId"]) => |
| 437 | 500 | stateManager.read((state) => |
| @@ -25,9 +25,9 @@ import type { · 24 unmodified lines | ||
| 25 | 25 | } from "@executor/platform-sdk/schema"; |
| 26 | 26 | import { |
| 27 | 27 | type ExecutorRuntime, |
| 28 | + emptyExecutorPluginRegistry, | |
| 28 | 29 | type ExecutorRuntimeOptions, |
| 29 | 30 | type ResolveSecretMaterial, |
| 30 | − emptyExecutorPluginRegistry, | |
| 31 | 31 | } from "@executor/platform-sdk/runtime"; |
| 32 | 32 | import * as Effect from "effect/Effect"; |
| 33 | 33 | import { |
| @@ -283,6 +283,8 @@ export const createLocalExecutorRepositoriesEffect = ( · 249 unmodified lines | ||
| 283 | 283 | }, |
| 284 | 284 | secrets: { |
| 285 | 285 | secretStores: executorStateStorage.executorState.secretStores, |
| 286 | + secretMaterialStoredData: | |
| 287 | + executorStateStorage.executorState.secretMaterialStoredData, | |
| 286 | 288 | ...executorStateStorage.executorState.secretMaterials, |
| 287 | 289 | resolve: resolveSecretMaterial, |
| 288 | 290 | store: createDefaultSecretMaterialStorer({ |
| @@ -255,15 +255,17 @@ export const createDefaultSecretMaterialStorer = (input: { · 254 unmodified lines | ||
| 255 | 255 | const material: SecretMaterial = { |
| 256 | 256 | id: `sec_${randomUUID()}` as SecretMaterial["id"], |
| 257 | 257 | storeId: store.id, |
| 258 | − handle: created.handle, | |
| 259 | 258 | name: created.name, |
| 260 | 259 | purpose, |
| 261 | − value: created.value ?? null, | |
| 262 | 260 | createdAt: now, |
| 263 | 261 | updatedAt: now, |
| 264 | 262 | }; |
| 265 | 263 | |
| 266 | 264 | yield* input.executorState.secretMaterials.upsert(material); |
| 265 | + yield* input.executorState.secretMaterialStoredData.upsert({ | |
| 266 | + secretId: material.id, | |
| 267 | + data: created.secretStored, | |
| 268 | + }); | |
| 267 | 269 | |
| 268 | 270 | return { |
| 269 | 271 | secretId: material.id, |
| @@ -309,12 +311,16 @@ export const createDefaultSecretMaterialUpdater = (input: { · 39 unmodified lines | ||
| 309 | 311 | |
| 310 | 312 | const nextMaterial: SecretMaterial = { |
| 311 | 313 | ...material, |
| 312 | − ...(updated.handle ? { handle: updated.handle } : {}), | |
| 313 | 314 | name: updated.name, |
| 314 | − value: updated.value ?? material.value, | |
| 315 | 315 | updatedAt: Date.now(), |
| 316 | 316 | }; |
| 317 | 317 | yield* input.executorState.secretMaterials.upsert(nextMaterial); |
| 318 | + if (updated.secretStored !== undefined) { | |
| 319 | + yield* input.executorState.secretMaterialStoredData.upsert({ | |
| 320 | + secretId: nextMaterial.id, | |
| 321 | + data: updated.secretStored, | |
| 322 | + }); | |
| 323 | + } | |
| 318 | 324 | |
| 319 | 325 | return { |
| 320 | 326 | id: nextMaterial.id, |
| @@ -359,6 +365,7 @@ export const createDefaultSecretMaterialDeleter = (input: { · 38 unmodified lines | ||
| 359 | 365 | return false; |
| 360 | 366 | } |
| 361 | 367 | |
| 368 | + yield* input.executorState.secretMaterialStoredData.removeBySecretId(material.id); | |
| 362 | 369 | return yield* input.executorState.secretMaterials.removeById(material.id); |
| 363 | 370 | }); |
| 364 | 371 | }; |
| @@ -442,6 +449,7 @@ export const SecretMaterialLive = (input: { · 77 unmodified lines | ||
| 442 | 449 | SecretMaterialResolverLive(input), |
| 443 | 450 | SecretMaterialStorerLive({ |
| 444 | 451 | executorState: input.executorState, |
| 452 | + pluginRegistry: input.pluginRegistry, | |
| 445 | 453 | resolveSecretMaterial: |
| 446 | 454 | input.resolveSecretMaterial |
| 447 | 455 | ?? createDefaultSecretMaterialResolver({ |
| @@ -449,11 +457,11 @@ export const SecretMaterialLive = (input: { · 1 unmodified line | ||
| 449 | 457 | pluginRegistry: input.pluginRegistry, |
| 450 | 458 | keychainServiceName: input.keychainServiceName, |
| 451 | 459 | }), |
| 452 | − pluginRegistry: input.pluginRegistry, | |
| 453 | 460 | keychainServiceName: input.keychainServiceName, |
| 454 | 461 | }), |
| 455 | 462 | SecretMaterialDeleterLive({ |
| 456 | 463 | executorState: input.executorState, |
| 464 | + pluginRegistry: input.pluginRegistry, | |
| 457 | 465 | resolveSecretMaterial: |
| 458 | 466 | input.resolveSecretMaterial |
| 459 | 467 | ?? createDefaultSecretMaterialResolver({ |
| @@ -461,11 +469,11 @@ export const SecretMaterialLive = (input: { · 1 unmodified line | ||
| 461 | 469 | pluginRegistry: input.pluginRegistry, |
| 462 | 470 | keychainServiceName: input.keychainServiceName, |
| 463 | 471 | }), |
| 464 | − pluginRegistry: input.pluginRegistry, | |
| 465 | 472 | keychainServiceName: input.keychainServiceName, |
| 466 | 473 | }), |
| 467 | 474 | SecretMaterialUpdaterLive({ |
| 468 | 475 | executorState: input.executorState, |
| 476 | + pluginRegistry: input.pluginRegistry, | |
| 469 | 477 | resolveSecretMaterial: |
| 470 | 478 | input.resolveSecretMaterial |
| 471 | 479 | ?? createDefaultSecretMaterialResolver({ |
| @@ -473,7 +481,6 @@ export const SecretMaterialLive = (input: { · 1 unmodified line | ||
| 473 | 481 | pluginRegistry: input.pluginRegistry, |
| 474 | 482 | keychainServiceName: input.keychainServiceName, |
| 475 | 483 | }), |
| 476 | − pluginRegistry: input.pluginRegistry, | |
| 477 | 484 | keychainServiceName: input.keychainServiceName, |
| 478 | 485 | }), |
| 479 | 486 | ); |
| @@ -176,6 +176,13 @@ const toSecretsBackend = ( · 175 unmodified lines | ||
| 176 | 176 | toOptionEffect(input.secretStores.updateById(id, patch)), |
| 177 | 177 | removeById: (id) => toEffect(input.secretStores.removeById(id)), |
| 178 | 178 | }, |
| 179 | + secretMaterialStoredData: { | |
| 180 | + getBySecretId: (secretId) => | |
| 181 | + toOptionEffect(input.secretMaterialStoredData.getBySecretId(secretId)), | |
| 182 | + upsert: (record) => toEffect(input.secretMaterialStoredData.upsert(record)), | |
| 183 | + removeBySecretId: (secretId) => | |
| 184 | + toEffect(input.secretMaterialStoredData.removeBySecretId(secretId)), | |
| 185 | + }, | |
| 179 | 186 | getById: (id) => toOptionEffect(input.getById(id)), |
| 180 | 187 | listAll: () => toEffect(input.listAll()), |
| 181 | 188 | upsert: (material) => toEffect(input.upsert(material)), |
| @@ -82,7 +82,7 @@ const getSecretStoreContributionOption = (kind: string) => · 81 unmodified lines | ||
| 82 | 82 | try { |
| 83 | 83 | return Option.some(getSecretStoreContribution(pluginRegistry, kind)); |
| 84 | 84 | } catch { |
| 85 | − return Option.none<typeof getSecretStoreContribution extends (registry: any, kind: string) => infer T ? T : never>(); | |
| 85 | + return Option.none<any>(); | |
| 86 | 86 | } |
| 87 | 87 | }); |
| 88 | 88 | |
| @@ -125,10 +125,9 @@ const toSecretStoreContract = ( · 36 unmodified lines | ||
| 125 | 125 | |
| 126 | 126 | const createImportedSecretRecord = (input: { |
| 127 | 127 | store: SecretStore; |
| 128 | − handle: string; | |
| 128 | + secretStored: unknown; | |
| 129 | 129 | name: string | null; |
| 130 | 130 | purpose: SecretMaterialPurpose; |
| 131 | − value?: string | null; | |
| 132 | 131 | }) => |
| 133 | 132 | Effect.gen(function* () { |
| 134 | 133 | const executorState = yield* ExecutorStateStore; |
| @@ -136,15 +135,17 @@ const createImportedSecretRecord = (input: { · 1 unmodified line | ||
| 136 | 135 | const material: SecretMaterial = { |
| 137 | 136 | id: SecretMaterialIdSchema.make(`sec_${crypto.randomUUID()}`), |
| 138 | 137 | storeId: input.store.id, |
| 139 | − handle: input.handle, | |
| 140 | 138 | name: input.name, |
| 141 | 139 | purpose: input.purpose, |
| 142 | − value: input.value ?? null, | |
| 143 | 140 | createdAt: now, |
| 144 | 141 | updatedAt: now, |
| 145 | 142 | }; |
| 146 | 143 | |
| 147 | 144 | yield* executorState.secretMaterials.upsert(material); |
| 145 | + yield* executorState.secretMaterialStoredData.upsert({ | |
| 146 | + secretId: material.id, | |
| 147 | + data: input.secretStored, | |
| 148 | + }); | |
| 148 | 149 | |
| 149 | 150 | return { |
| 150 | 151 | id: material.id, |
| @@ -384,7 +385,7 @@ export const browseLocalSecretStore = (input: { · 233 unmodified lines | ||
| 384 | 385 | cause instanceof Error ? cause.message : "Failed browsing secret store.", |
| 385 | 386 | ), |
| 386 | 387 | ), |
| 387 | − Effect.map((result) => result satisfies BrowseSecretStoreResult), | |
| 388 | + Effect.map((result) => result as BrowseSecretStoreResult), | |
| 388 | 389 | ); |
| 389 | 390 | }); |
| 390 | 391 | |
| @@ -437,9 +438,8 @@ export const importLocalSecretFromStore = (input: { · 46 unmodified lines | ||
| 437 | 438 | |
| 438 | 439 | return yield* createImportedSecretRecord({ |
| 439 | 440 | store, |
| 440 | − handle: created.handle, | |
| 441 | + secretStored: created.secretStored, | |
| 441 | 442 | name: created.name, |
| 442 | 443 | purpose: input.payload.purpose ?? "auth_material", |
| 443 | − value: created.value, | |
| 444 | 444 | }); |
| 445 | 445 | }); |
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import * as Effect from "effect/Effect"; |
| 2 | +import * as Option from "effect/Option"; | |
| 2 | 3 | |
| 3 | 4 | import type { SourceCatalogSyncResult } from "@executor/source-core"; |
| 4 | 5 | import type { |
| @@ -21,6 +22,9 @@ import type { · 16 unmodified lines | ||
| 21 | 22 | } from "@executor/source-core"; |
| 22 | 23 | import type * as Schema from "effect/Schema"; |
| 23 | 24 | import { runtimeEffectError } from "./runtime/effect-errors"; |
| 25 | +import { | |
| 26 | + ExecutorStateStore, | |
| 27 | +} from "./runtime/executor-state-store"; | |
| 24 | 28 | import { ScopeConfigStore } from "./runtime/scope/storage"; |
| 25 | 29 | |
| 26 | 30 | export type PluginCleanup = { |
| @@ -329,6 +333,7 @@ export type ExecutorSecretStorePluginDefinition< · 302 unmodified lines | ||
| 329 | 333 | TConnectInput, |
| 330 | 334 | TStoreConfig, |
| 331 | 335 | TStored, |
| 336 | + TSecretStored, | |
| 332 | 337 | _TUpdateInput extends { |
| 333 | 338 | storeId: string; |
| 334 | 339 | config: TStoreConfig; |
| @@ -369,6 +374,7 @@ export type ExecutorSecretStorePluginDefinition< · 34 unmodified lines | ||
| 369 | 374 | }) => Effect.Effect<void, Error, any>; |
| 370 | 375 | resolveSecret: (input: { |
| 371 | 376 | secret: SecretMaterial; |
| 377 | + secretStored: TSecretStored; | |
| 372 | 378 | store: SecretStore; |
| 373 | 379 | stored: TStored | null; |
| 374 | 380 | context?: { |
| @@ -382,23 +388,23 @@ export type ExecutorSecretStorePluginDefinition< · 7 unmodified lines | ||
| 382 | 388 | value: string; |
| 383 | 389 | name?: string | null; |
| 384 | 390 | }) => Effect.Effect<{ |
| 385 | − handle: string; | |
| 386 | 391 | name: string | null; |
| 387 | − value?: string | null; | |
| 392 | + secretStored: TSecretStored; | |
| 388 | 393 | }, Error, any>; |
| 389 | 394 | updateSecret?: (input: { |
| 390 | 395 | secret: SecretMaterial; |
| 396 | + secretStored: TSecretStored; | |
| 391 | 397 | store: SecretStore; |
| 392 | 398 | stored: TStored | null; |
| 393 | 399 | name?: string | null; |
| 394 | 400 | value?: string; |
| 395 | 401 | }) => Effect.Effect<{ |
| 396 | − handle?: string; | |
| 397 | 402 | name: string | null; |
| 398 | − value?: string | null; | |
| 403 | + secretStored?: TSecretStored; | |
| 399 | 404 | }, Error, any>; |
| 400 | 405 | deleteSecret?: (input: { |
| 401 | 406 | secret: SecretMaterial; |
| 407 | + secretStored: TSecretStored; | |
| 402 | 408 | store: SecretStore; |
| 403 | 409 | stored: TStored | null; |
| 404 | 410 | }) => Effect.Effect<boolean, Error, any>; |
| @@ -417,9 +423,8 @@ export type ExecutorSecretStorePluginDefinition< · 12 unmodified lines | ||
| 417 | 423 | purpose: SecretMaterialPurpose; |
| 418 | 424 | name?: string | null; |
| 419 | 425 | }) => Effect.Effect<{ |
| 420 | − handle: string; | |
| 421 | 426 | name: string | null; |
| 422 | − value?: string | null; | |
| 427 | + secretStored: TSecretStored; | |
| 423 | 428 | }, Error, any>; |
| 424 | 429 | capabilities?: (input: { |
| 425 | 430 | store: SecretStore; |
| @@ -479,6 +484,7 @@ export type ExecutorSecretStorePluginInput< · 53 unmodified lines | ||
| 479 | 484 | TConnectInput = unknown, |
| 480 | 485 | TStoreConfig = unknown, |
| 481 | 486 | TStored = unknown, |
| 487 | + TSecretStored = unknown, | |
| 482 | 488 | TUpdateInput extends { |
| 483 | 489 | storeId: string; |
| 484 | 490 | config: TStoreConfig; |
| @@ -494,6 +500,7 @@ export type ExecutorSecretStorePluginInput< · 9 unmodified lines | ||
| 494 | 500 | TConnectInput, |
| 495 | 501 | TStoreConfig, |
| 496 | 502 | TStored, |
| 503 | + TSecretStored, | |
| 497 | 504 | TUpdateInput |
| 498 | 505 | >; |
| 499 | 506 | extendExecutor?: (input: ExecutorSdkPluginContext & { |
| @@ -771,7 +778,7 @@ const createExecutorSourcePluginApi = < · 271 unmodified lines | ||
| 771 | 778 | const loadStoreOfKind = ( |
| 772 | 779 | storeId: SecretStore["id"], |
| 773 | 780 | input: { |
| 774 | − definition: ExecutorSecretStorePluginDefinition<any, any, any, any, any>; | |
| 781 | + definition: ExecutorSecretStorePluginDefinition<any, any, any, any, any, any>; | |
| 775 | 782 | host: ExecutorSecretStorePluginInternalHost; |
| 776 | 783 | }, |
| 777 | 784 | ): Effect.Effect<SecretStore, Error, any> => |
| @@ -787,11 +794,28 @@ const loadStoreOfKind = ( · 9 unmodified lines | ||
| 787 | 794 | return store; |
| 788 | 795 | }); |
| 789 | 796 | |
| 797 | +const loadSecretMaterialStoredData = <TSecretStored>( | |
| 798 | + secretId: SecretMaterial["id"], | |
| 799 | +): Effect.Effect<TSecretStored, Error, any> => | |
| 800 | + Effect.gen(function* () { | |
| 801 | + const executorState = yield* ExecutorStateStore; | |
| 802 | + const stored = yield* executorState.secretMaterialStoredData.getBySecretId(secretId); | |
| 803 | + if (Option.isNone(stored)) { | |
| 804 | + return yield* runtimeEffectError( | |
| 805 | + "plugins", | |
| 806 | + `Secret storage missing for ${secretId}.`, | |
| 807 | + ); | |
| 808 | + } | |
| 809 | + | |
| 810 | + return stored.value.data as TSecretStored; | |
| 811 | + }); | |
| 812 | + | |
| 790 | 813 | const createExecutorSecretStorePluginApi = < |
| 791 | 814 | TAddInput, |
| 792 | 815 | TConnectInput, |
| 793 | 816 | TStoreConfig, |
| 794 | 817 | TStored, |
| 818 | + TSecretStored, | |
| 795 | 819 | _TUpdateInput extends { |
| 796 | 820 | storeId: string; |
| 797 | 821 | config: TStoreConfig; |
| @@ -802,6 +826,7 @@ const createExecutorSecretStorePluginApi = < · 4 unmodified lines | ||
| 802 | 826 | TConnectInput, |
| 803 | 827 | TStoreConfig, |
| 804 | 828 | TStored, |
| 829 | + TSecretStored, | |
| 805 | 830 | _TUpdateInput |
| 806 | 831 | >, |
| 807 | 832 | host: ExecutorSecretStorePluginInternalHost, |
| @@ -962,9 +987,8 @@ type ExecutorSecretStoreContribution<TInput = unknown> = { · 154 unmodified lines | ||
| 962 | 987 | value: string; |
| 963 | 988 | name?: string | null; |
| 964 | 989 | }) => Effect.Effect<{ |
| 965 | − handle: string; | |
| 966 | 990 | name: string | null; |
| 967 | − value?: string | null; | |
| 991 | + secretStored: unknown; | |
| 968 | 992 | }, Error, any>; |
| 969 | 993 | updateSecret: (input: { |
| 970 | 994 | secret: SecretMaterial; |
| @@ -972,9 +996,8 @@ type ExecutorSecretStoreContribution<TInput = unknown> = { · 1 unmodified line | ||
| 972 | 996 | name?: string | null; |
| 973 | 997 | value?: string; |
| 974 | 998 | }) => Effect.Effect<{ |
| 975 | − handle?: string; | |
| 976 | 999 | name: string | null; |
| 977 | − value?: string | null; | |
| 1000 | + secretStored?: unknown; | |
| 978 | 1001 | }, Error, any>; |
| 979 | 1002 | deleteSecret: (input: { |
| 980 | 1003 | secret: SecretMaterial; |
| @@ -993,9 +1016,8 @@ type ExecutorSecretStoreContribution<TInput = unknown> = { · 12 unmodified lines | ||
| 993 | 1016 | purpose: SecretMaterialPurpose; |
| 994 | 1017 | name?: string | null; |
| 995 | 1018 | }) => Effect.Effect<{ |
| 996 | − handle: string; | |
| 997 | 1019 | name: string | null; |
| 998 | − value?: string | null; | |
| 1020 | + secretStored: unknown; | |
| 999 | 1021 | }, Error, any>; |
| 1000 | 1022 | }; |
| 1001 | 1023 | |
| @@ -1076,6 +1098,7 @@ const createExecutorSecretStoreContribution = < · 74 unmodified lines | ||
| 1076 | 1098 | TConnectInput, |
| 1077 | 1099 | TStoreConfig, |
| 1078 | 1100 | TStored, |
| 1101 | + TSecretStored, | |
| 1079 | 1102 | TUpdateInput extends { |
| 1080 | 1103 | storeId: string; |
| 1081 | 1104 | config: TStoreConfig; |
| @@ -1086,6 +1109,7 @@ const createExecutorSecretStoreContribution = < · 4 unmodified lines | ||
| 1086 | 1109 | TConnectInput, |
| 1087 | 1110 | TStoreConfig, |
| 1088 | 1111 | TStored, |
| 1112 | + TSecretStored, | |
| 1089 | 1113 | TUpdateInput |
| 1090 | 1114 | >, |
| 1091 | 1115 | ): ExecutorSecretStoreContribution<TAddInput> => ({ |
| @@ -1153,13 +1177,17 @@ const createExecutorSecretStoreContribution = < · 61 unmodified lines | ||
| 1153 | 1177 | ), |
| 1154 | 1178 | resolveSecret: ({ secret, store, context }) => |
| 1155 | 1179 | Effect.flatMap( |
| 1156 | − definition.storage.get({ | |
| 1157 | − scopeId: store.scopeId, | |
| 1158 | − storeId: store.id, | |
| 1180 | + Effect.all({ | |
| 1181 | + stored: definition.storage.get({ | |
| 1182 | + scopeId: store.scopeId, | |
| 1183 | + storeId: store.id, | |
| 1184 | + }), | |
| 1185 | + secretStored: loadSecretMaterialStoredData<TSecretStored>(secret.id), | |
| 1159 | 1186 | }), |
| 1160 | − (stored) => | |
| 1187 | + ({ stored, secretStored }) => | |
| 1161 | 1188 | definition.store.resolveSecret({ |
| 1162 | 1189 | secret, |
| 1190 | + secretStored, | |
| 1163 | 1191 | store, |
| 1164 | 1192 | stored, |
| 1165 | 1193 | context, |
| @@ -1188,13 +1216,17 @@ const createExecutorSecretStoreContribution = < · 22 unmodified lines | ||
| 1188 | 1216 | updateSecret: ({ secret, store, name, value }) => |
| 1189 | 1217 | definition.store.updateSecret |
| 1190 | 1218 | ? Effect.flatMap( |
| 1191 | − definition.storage.get({ | |
| 1192 | − scopeId: store.scopeId, | |
| 1193 | − storeId: store.id, | |
| 1219 | + Effect.all({ | |
| 1220 | + stored: definition.storage.get({ | |
| 1221 | + scopeId: store.scopeId, | |
| 1222 | + storeId: store.id, | |
| 1223 | + }), | |
| 1224 | + secretStored: loadSecretMaterialStoredData<TSecretStored>(secret.id), | |
| 1194 | 1225 | }), |
| 1195 | − (stored) => | |
| 1226 | + ({ stored, secretStored }) => | |
| 1196 | 1227 | definition.store.updateSecret!({ |
| 1197 | 1228 | secret, |
| 1229 | + secretStored, | |
| 1198 | 1230 | store, |
| 1199 | 1231 | stored, |
| 1200 | 1232 | name, |
| @@ -1208,13 +1240,17 @@ const createExecutorSecretStoreContribution = < · 7 unmodified lines | ||
| 1208 | 1240 | deleteSecret: ({ secret, store }) => |
| 1209 | 1241 | definition.store.deleteSecret |
| 1210 | 1242 | ? Effect.flatMap( |
| 1211 | − definition.storage.get({ | |
| 1212 | − scopeId: store.scopeId, | |
| 1213 | − storeId: store.id, | |
| 1243 | + Effect.all({ | |
| 1244 | + stored: definition.storage.get({ | |
| 1245 | + scopeId: store.scopeId, | |
| 1246 | + storeId: store.id, | |
| 1247 | + }), | |
| 1248 | + secretStored: loadSecretMaterialStoredData<TSecretStored>(secret.id), | |
| 1214 | 1249 | }), |
| 1215 | − (stored) => | |
| 1250 | + ({ stored, secretStored }) => | |
| 1216 | 1251 | definition.store.deleteSecret!({ |
| 1217 | 1252 | secret, |
| 1253 | + secretStored, | |
| 1218 | 1254 | store, |
| 1219 | 1255 | stored, |
| 1220 | 1256 | }), |
| @@ -1377,6 +1413,7 @@ export const defineExecutorSecretStorePlugin = < · 156 unmodified lines | ||
| 1377 | 1413 | TConnectInput, |
| 1378 | 1414 | TStoreConfig, |
| 1379 | 1415 | TStored, |
| 1416 | + TSecretStored, | |
| 1380 | 1417 | TUpdateInput extends { |
| 1381 | 1418 | storeId: string; |
| 1382 | 1419 | config: TStoreConfig; |
| @@ -1389,6 +1426,7 @@ export const defineExecutorSecretStorePlugin = < · 6 unmodified lines | ||
| 1389 | 1426 | TConnectInput, |
| 1390 | 1427 | TStoreConfig, |
| 1391 | 1428 | TStored, |
| 1429 | + TSecretStored, | |
| 1392 | 1430 | TUpdateInput, |
| 1393 | 1431 | TExtension |
| 1394 | 1432 | >, |
| @@ -14,9 +14,8 @@ describe("live-execution", () => { · 13 unmodified lines | ||
| 14 | 14 | action: "accept", |
| 15 | 15 | content: { |
| 16 | 16 | authKind: "bearer", |
| 17 | − tokenRef: { | |
| 18 | − providerId: "local", | |
| 19 | − handle: "sec_123", | |
| 17 | + tokenSecretRef: { | |
| 18 | + secretId: "sec_123", | |
| 20 | 19 | }, |
| 21 | 20 | }, |
| 22 | 21 | }); |
| @@ -71,7 +71,10 @@ const serializeJson = (value: unknown): string | null => { · 70 unmodified lines | ||
| 71 | 71 | }; |
| 72 | 72 | |
| 73 | 73 | const SENSITIVE_INTERACTION_CONTENT_KEYS = new Set([ |
| 74 | − "tokenRef", | |
| 74 | + "tokenSecretRef", | |
| 75 | + "accessTokenRef", | |
| 76 | + "refreshTokenRef", | |
| 77 | + "clientSecretRef", | |
| 75 | 78 | "tokenSecretMaterialId", |
| 76 | 79 | ]); |
| 77 | 80 | |
| @@ -18,6 +18,11 @@ type SecretMaterialSummary = { · 17 unmodified lines | ||
| 18 | 18 | updatedAt: number; |
| 19 | 19 | }; |
| 20 | 20 | |
| 21 | +export type SecretMaterialStoredDataRecord = { | |
| 22 | + secretId: string; | |
| 23 | + data: unknown; | |
| 24 | +}; | |
| 25 | + | |
| 21 | 26 | type SecretStoreSummary = { |
| 22 | 27 | id: string; |
| 23 | 28 | scopeId: string; |
| @@ -56,7 +61,7 @@ export type ExecutorStateStoreShape = { · 32 unmodified lines | ||
| 56 | 61 | upsert: (material: SecretMaterial) => Effect.Effect<void, Error, never>; |
| 57 | 62 | updateById: ( |
| 58 | 63 | id: SecretMaterial["id"], |
| 59 | − update: { name?: string | null; value?: string }, | |
| 64 | + update: { name?: string | null }, | |
| 60 | 65 | ) => Effect.Effect< |
| 61 | 66 | import("effect/Option").Option<SecretMaterialSummary>, |
| 62 | 67 | Error, |
| @@ -64,6 +69,21 @@ export type ExecutorStateStoreShape = { · 1 unmodified line | ||
| 64 | 69 | >; |
| 65 | 70 | removeById: (id: SecretMaterial["id"]) => Effect.Effect<boolean, Error, never>; |
| 66 | 71 | }; |
| 72 | + secretMaterialStoredData: { | |
| 73 | + getBySecretId: ( | |
| 74 | + secretId: SecretMaterial["id"], | |
| 75 | + ) => Effect.Effect< | |
| 76 | + import("effect/Option").Option<SecretMaterialStoredDataRecord>, | |
| 77 | + Error, | |
| 78 | + never | |
| 79 | + >; | |
| 80 | + upsert: ( | |
| 81 | + record: SecretMaterialStoredDataRecord, | |
| 82 | + ) => Effect.Effect<void, Error, never>; | |
| 83 | + removeBySecretId: ( | |
| 84 | + secretId: SecretMaterial["id"], | |
| 85 | + ) => Effect.Effect<boolean, Error, never>; | |
| 86 | + }; | |
| 67 | 87 | executions: { |
| 68 | 88 | listByScope: ( |
| 69 | 89 | scopeId: Execution["scopeId"], |
| @@ -112,7 +112,6 @@ export * from "./execution/service"; · 111 unmodified lines | ||
| 112 | 112 | export * from "./source-artifacts"; |
| 113 | 113 | export * from "./scope-config"; |
| 114 | 114 | export * from "./scope-state"; |
| 115 | −export * from "./scope/config-secrets"; | |
| 116 | 115 | export * from "./sources/source-store/config"; |
| 117 | 116 | export * from "./sources/source-store/plugin-local-config"; |
| 118 | 117 | export { |
| @@ -244,6 +243,7 @@ export const prewarmWorkspaceSourceCatalogToolIndex = (input: { · 125 unmodified lines | ||
| 244 | 243 | export type RuntimeSecretsStorageServices = |
| 245 | 244 | & { |
| 246 | 245 | secretStores: ExecutorStateStoreShape["secretStores"]; |
| 246 | + secretMaterialStoredData: ExecutorStateStoreShape["secretMaterialStoredData"]; | |
| 247 | 247 | } |
| 248 | 248 | & ExecutorStateStoreShape["secretMaterials"] |
| 249 | 249 | & RuntimeSecretMaterialServices; |
| @@ -343,7 +343,14 @@ const toExecutorStateStoreShape = ( · 93 unmodified lines | ||
| 343 | 343 | input: RuntimeStorageServices, |
| 344 | 344 | ): ExecutorStateStoreShape => ({ |
| 345 | 345 | secretStores: input.secrets.secretStores, |
| 346 | − secretMaterials: input.secrets, | |
| 346 | + secretMaterials: { | |
| 347 | + getById: input.secrets.getById, | |
| 348 | + listAll: input.secrets.listAll, | |
| 349 | + upsert: input.secrets.upsert, | |
| 350 | + updateById: input.secrets.updateById, | |
| 351 | + removeById: input.secrets.removeById, | |
| 352 | + }, | |
| 353 | + secretMaterialStoredData: input.secrets.secretMaterialStoredData, | |
| 347 | 354 | executions: input.executions.runs, |
| 348 | 355 | executionInteractions: input.executions.interactions, |
| 349 | 356 | executionSteps: input.executions.steps, |
| @@ -1,9 +0,0 @@ | ||
| 1 | −export const CONFIG_SECRET_PROVIDER_PREFIX = "config:"; | |
| 2 | − | |
| 3 | −export const toConfigSecretProviderId = (providerAlias: string): string => | |
| 4 | − `${CONFIG_SECRET_PROVIDER_PREFIX}${providerAlias}`; | |
| 5 | − | |
| 6 | −export const fromConfigSecretProviderId = (providerId: string): string | null => | |
| 7 | − providerId.startsWith(CONFIG_SECRET_PROVIDER_PREFIX) | |
| 8 | − ? providerId.slice(CONFIG_SECRET_PROVIDER_PREFIX.length) | |
| 9 | − : null; | |
| @@ -3,7 +3,6 @@ import type { · 2 unmodified lines | ||
| 3 | 3 | Source, |
| 4 | 4 | } from "#schema"; |
| 5 | 5 | import * as Schema from "effect/Schema"; |
| 6 | − | |
| 7 | 6 | import { |
| 8 | 7 | cloneJson, |
| 9 | 8 | configSourceBaseFromLocalSource, |
| @@ -8,54 +8,6 @@ export const LocalExecutorRuntimeSchema = Schema.Literal( · 7 unmodified lines | ||
| 8 | 8 | "deno", |
| 9 | 9 | ); |
| 10 | 10 | |
| 11 | −export const LocalConfigSecretProviderSourceSchema = Schema.Literal( | |
| 12 | − "env", | |
| 13 | − "file", | |
| 14 | − "exec", | |
| 15 | − "params", | |
| 16 | −); | |
| 17 | − | |
| 18 | −export const LocalConfigEnvSecretProviderSchema = Schema.Struct({ | |
| 19 | − source: Schema.Literal("env"), | |
| 20 | −}); | |
| 21 | − | |
| 22 | −export const LocalConfigFileSecretProviderModeSchema = Schema.Literal( | |
| 23 | − "singleValue", | |
| 24 | − "json", | |
| 25 | −); | |
| 26 | − | |
| 27 | −export const LocalConfigFileSecretProviderSchema = Schema.Struct({ | |
| 28 | − source: Schema.Literal("file"), | |
| 29 | − path: Schema.String, | |
| 30 | − mode: Schema.optional(LocalConfigFileSecretProviderModeSchema), | |
| 31 | −}); | |
| 32 | − | |
| 33 | −export const LocalConfigExecSecretProviderSchema = Schema.Struct({ | |
| 34 | − source: Schema.Literal("exec"), | |
| 35 | − command: Schema.String, | |
| 36 | − args: Schema.optional(Schema.Array(Schema.String)), | |
| 37 | − env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })), | |
| 38 | − allowSymlinkCommand: Schema.optional(Schema.Boolean), | |
| 39 | − trustedDirs: Schema.optional(Schema.Array(Schema.String)), | |
| 40 | −}); | |
| 41 | − | |
| 42 | −export const LocalConfigSecretProviderSchema = Schema.Union( | |
| 43 | − LocalConfigEnvSecretProviderSchema, | |
| 44 | − LocalConfigFileSecretProviderSchema, | |
| 45 | − LocalConfigExecSecretProviderSchema, | |
| 46 | −); | |
| 47 | − | |
| 48 | −export const LocalConfigExplicitSecretRefSchema = Schema.Struct({ | |
| 49 | − source: LocalConfigSecretProviderSourceSchema, | |
| 50 | − provider: Schema.String, | |
| 51 | − id: Schema.String, | |
| 52 | −}); | |
| 53 | − | |
| 54 | −export const LocalConfigSecretInputSchema = Schema.Union( | |
| 55 | − Schema.String, | |
| 56 | − LocalConfigExplicitSecretRefSchema, | |
| 57 | −); | |
| 58 | − | |
| 59 | 11 | export const LocalConfigSourceConnectionSchema = Schema.Struct({ |
| 60 | 12 | endpoint: Schema.optional(Schema.NullOr(Schema.String)), |
| 61 | 13 | auth: Schema.optional(Schema.Unknown), |
| @@ -90,22 +42,6 @@ export const LocalConfigPolicySchema = Schema.Struct({ · 28 unmodified lines | ||
| 90 | 42 | priority: Schema.optional(Schema.Number), |
| 91 | 43 | }); |
| 92 | 44 | |
| 93 | −export const LocalConfigSecretsSchema = Schema.Struct({ | |
| 94 | − providers: Schema.optional( | |
| 95 | − Schema.Record({ | |
| 96 | − key: Schema.String, | |
| 97 | − value: LocalConfigSecretProviderSchema, | |
| 98 | − }), | |
| 99 | − ), | |
| 100 | − defaults: Schema.optional( | |
| 101 | − Schema.Struct({ | |
| 102 | − env: Schema.optional(Schema.String), | |
| 103 | − file: Schema.optional(Schema.String), | |
| 104 | − exec: Schema.optional(Schema.String), | |
| 105 | − }), | |
| 106 | − ), | |
| 107 | −}); | |
| 108 | − | |
| 109 | 45 | export const LocalConfigWorkspaceSchema = Schema.Struct({ |
| 110 | 46 | name: Schema.optional(Schema.String), |
| 111 | 47 | }); |
| @@ -125,17 +61,8 @@ export const LocalExecutorConfigSchema = Schema.Struct({ · 13 unmodified lines | ||
| 125 | 61 | value: LocalConfigPolicySchema, |
| 126 | 62 | }), |
| 127 | 63 | ), |
| 128 | − secrets: Schema.optional(LocalConfigSecretsSchema), | |
| 129 | 64 | }); |
| 130 | 65 | |
| 131 | −export type LocalConfigSecretProviderSource = | |
| 132 | − typeof LocalConfigSecretProviderSourceSchema.Type; | |
| 133 | −export type LocalConfigSecretProvider = | |
| 134 | − typeof LocalConfigSecretProviderSchema.Type; | |
| 135 | −export type LocalConfigExplicitSecretRef = | |
| 136 | − typeof LocalConfigExplicitSecretRefSchema.Type; | |
| 137 | −export type LocalConfigSecretInput = typeof LocalConfigSecretInputSchema.Type; | |
| 138 | 66 | export type LocalConfigPolicy = typeof LocalConfigPolicySchema.Type; |
| 139 | −export type LocalConfigSecrets = typeof LocalConfigSecretsSchema.Type; | |
| 140 | 67 | export type LocalExecutorRuntime = typeof LocalExecutorRuntimeSchema.Type; |
| 141 | 68 | export type LocalExecutorConfig = typeof LocalExecutorConfigSchema.Type; |
| @@ -22,8 +22,6 @@ export const SecretMaterialSchema = Schema.Struct({ · 21 unmodified lines | ||
| 22 | 22 | name: Schema.NullOr(Schema.String), |
| 23 | 23 | purpose: SecretMaterialPurposeSchema, |
| 24 | 24 | storeId: SecretStoreIdSchema, |
| 25 | − handle: Schema.String, | |
| 26 | − value: Schema.NullOr(Schema.String), | |
| 27 | 25 | createdAt: TimestampMsSchema, |
| 28 | 26 | updatedAt: TimestampMsSchema, |
| 29 | 27 | }); |
| @@ -1,8 +1,7 @@ | ||
| 1 | 1 | import * as Schema from "effect/Schema"; |
| 2 | 2 | |
| 3 | 3 | export const SecretRefSchema = Schema.Struct({ |
| 4 | − providerId: Schema.String, | |
| 5 | − handle: Schema.String, | |
| 4 | + secretId: Schema.String, | |
| 6 | 5 | }); |
| 7 | 6 | |
| 8 | 7 | export const CredentialSlotSchema = Schema.Literal("runtime", "import"); |
| @@ -557,7 +557,6 @@ const googleDiscoveryStoredSourceDataFromLocalConfig = (input: { · 556 unmodified lines | ||
| 557 | 557 | if (Option.isSome(current)) { |
| 558 | 558 | return current.value.config; |
| 559 | 559 | } |
| 560 | − | |
| 561 | 560 | throw new Error("Unsupported Google Discovery local source config."); |
| 562 | 561 | }; |
| 563 | 562 | |
| @@ -342,7 +342,6 @@ const graphqlStoredSourceDataFromLocalConfig = (input: { · 341 unmodified lines | ||
| 342 | 342 | if (Option.isSome(current)) { |
| 343 | 343 | return normalizeStoredSourceData(current.value.config); |
| 344 | 344 | } |
| 345 | − | |
| 346 | 345 | throw new Error("Unsupported GraphQL local source config."); |
| 347 | 346 | }; |
| 348 | 347 | |
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | import { randomUUID } from "node:crypto"; |
| 2 | −import { spawn } from "node:child_process"; | |
| 2 | +import { | |
| 3 | + Entry, | |
| 4 | +} from "@napi-rs/keyring"; | |
| 3 | 5 | |
| 4 | 6 | import * as Effect from "effect/Effect"; |
| 5 | 7 | |
| @@ -13,16 +15,13 @@ import { · 7 unmodified lines | ||
| 13 | 15 | export const KEYCHAIN_SECRET_STORE_KIND = "keychain"; |
| 14 | 16 | export const KEYCHAIN_SECRET_STORE_ID = "sts_builtin_keychain"; |
| 15 | 17 | |
| 18 | +type KeychainSecretStoredData = { | |
| 19 | + account: string; | |
| 20 | +}; | |
| 21 | + | |
| 16 | 22 | const DEFAULT_KEYCHAIN_SERVICE_NAME = "executor"; |
| 17 | −const KEYCHAIN_COMMAND_TIMEOUT_MS = 5_000; | |
| 18 | 23 | const KEYCHAIN_SERVICE_NAME_ENV = "EXECUTOR_KEYCHAIN_SERVICE_NAME"; |
| 19 | 24 | |
| 20 | −type SpawnResult = { | |
| 21 | − exitCode: number; | |
| 22 | − stdout: string; | |
| 23 | − stderr: string; | |
| 24 | −}; | |
| 25 | − | |
| 26 | 25 | const toError = (cause: unknown): Error => |
| 27 | 26 | cause instanceof Error ? cause : new Error(String(cause)); |
| 28 | 27 | |
| @@ -35,280 +34,99 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 6 unmodified lines | ||
| 35 | 34 | return trimmed.length > 0 ? trimmed : null; |
| 36 | 35 | }; |
| 37 | 36 | |
| 38 | −const ensureNonEmptyString = (value: string | undefined | null): string | null => { | |
| 39 | − const trimmed = value?.trim(); | |
| 40 | − return trimmed && trimmed.length > 0 ? trimmed : null; | |
| 41 | −}; | |
| 42 | − | |
| 43 | 37 | const resolveKeychainServiceName = (value: string | undefined): string => |
| 44 | 38 | trimOrNull(value) |
| 45 | 39 | ?? trimOrNull(process.env[KEYCHAIN_SERVICE_NAME_ENV]) |
| 46 | 40 | ?? DEFAULT_KEYCHAIN_SERVICE_NAME; |
| 47 | 41 | |
| 48 | −const runCommand = (input: { | |
| 49 | − command: string; | |
| 50 | − args: ReadonlyArray<string>; | |
| 51 | − stdin?: string; | |
| 52 | − operation: string; | |
| 53 | − timeoutMs?: number; | |
| 54 | −}): Effect.Effect<SpawnResult, Error, never> => | |
| 55 | − Effect.tryPromise({ | |
| 56 | − try: () => | |
| 57 | − new Promise<SpawnResult>((resolve, reject) => { | |
| 58 | − const child = spawn(input.command, [...input.args], { | |
| 59 | − stdio: "pipe", | |
| 60 | − env: process.env, | |
| 61 | − }); | |
| 62 | − | |
| 63 | − let stdout = ""; | |
| 64 | − let stderr = ""; | |
| 65 | − let settled = false; | |
| 66 | − const timeout = input.timeoutMs === undefined | |
| 67 | − ? null | |
| 68 | − : setTimeout(() => { | |
| 69 | − if (settled) { | |
| 70 | − return; | |
| 71 | − } | |
| 72 | − | |
| 73 | − settled = true; | |
| 74 | − child.kill("SIGKILL"); | |
| 75 | − reject( | |
| 76 | − new Error( | |
| 77 | − `${input.operation}: '${input.command}' timed out after ${input.timeoutMs}ms`, | |
| 78 | − ), | |
| 79 | − ); | |
| 80 | − }, input.timeoutMs); | |
| 81 | − | |
| 82 | − child.stdout.on("data", (chunk) => { | |
| 83 | − stdout += chunk.toString("utf8"); | |
| 84 | − }); | |
| 85 | − | |
| 86 | − child.stderr.on("data", (chunk) => { | |
| 87 | − stderr += chunk.toString("utf8"); | |
| 88 | − }); | |
| 42 | +const isSupportedPlatform = () => | |
| 43 | + process.platform === "darwin" | |
| 44 | + || process.platform === "linux" | |
| 45 | + || process.platform === "win32"; | |
| 89 | 46 | |
| 90 | − child.on("error", (error) => { | |
| 91 | − if (settled) { | |
| 92 | − return; | |
| 93 | − } | |
| 94 | − settled = true; | |
| 95 | − if (timeout) { | |
| 96 | − clearTimeout(timeout); | |
| 97 | − } | |
| 98 | − reject(error); | |
| 99 | − }); | |
| 47 | +const keychainDisplayName = () => | |
| 48 | + process.platform === "darwin" | |
| 49 | + ? "macOS Keychain" | |
| 50 | + : process.platform === "win32" | |
| 51 | + ? "Windows Credential Manager" | |
| 52 | + : "Desktop Keyring"; | |
| 100 | 53 | |
| 101 | − child.on("close", (code) => { | |
| 102 | − if (settled) { | |
| 103 | − return; | |
| 104 | − } | |
| 105 | − settled = true; | |
| 106 | − if (timeout) { | |
| 107 | − clearTimeout(timeout); | |
| 108 | − } | |
| 109 | − resolve({ | |
| 110 | − exitCode: code ?? 0, | |
| 111 | − stdout, | |
| 112 | − stderr, | |
| 113 | − }); | |
| 114 | − }); | |
| 115 | − | |
| 116 | − if (input.stdin !== undefined) { | |
| 117 | − child.stdin.write(input.stdin); | |
| 118 | − } | |
| 54 | +const createKeyringEntry = (input: { | |
| 55 | + providerHandle: string; | |
| 56 | + keychainServiceName: string; | |
| 57 | +}) => | |
| 58 | + Effect.try({ | |
| 59 | + try: () => { | |
| 60 | + if (!isSupportedPlatform()) { | |
| 61 | + throw runtimeEffectError( | |
| 62 | + "plugin-keychain-secret-store", | |
| 63 | + `system-keyring: unsupported on platform '${process.platform}'`, | |
| 64 | + ); | |
| 65 | + } | |
| 119 | 66 | |
| 120 | − child.stdin.end(); | |
| 121 | − }), | |
| 67 | + return new Entry(input.keychainServiceName, input.providerHandle); | |
| 68 | + }, | |
| 122 | 69 | catch: toError, |
| 123 | 70 | }); |
| 124 | 71 | |
| 125 | −const ensureCommandSuccess = (input: { | |
| 126 | − result: SpawnResult; | |
| 127 | − operation: string; | |
| 128 | − message: string; | |
| 129 | −}): Effect.Effect<SpawnResult, Error, never> => { | |
| 130 | − if (input.result.exitCode === 0) { | |
| 131 | − return Effect.succeed(input.result); | |
| 132 | − } | |
| 133 | − | |
| 134 | − const details = ensureNonEmptyString(input.result.stderr) | |
| 135 | − ?? ensureNonEmptyString(input.result.stdout) | |
| 136 | − ?? "command returned non-zero exit code"; | |
| 137 | − | |
| 138 | − return Effect.fail( | |
| 139 | − runtimeEffectError( | |
| 140 | − "plugin-keychain-secret-store", | |
| 141 | − `${input.operation}: ${input.message}: ${details}`, | |
| 142 | − ), | |
| 143 | − ); | |
| 144 | −}; | |
| 145 | − | |
| 146 | 72 | const readKeychainSecretValue = (input: { |
| 147 | 73 | providerHandle: string; |
| 148 | 74 | keychainServiceName: string; |
| 149 | −}) => { | |
| 150 | − switch (process.platform) { | |
| 151 | − case "darwin": | |
| 152 | − return runCommand({ | |
| 153 | − command: "security", | |
| 154 | − args: [ | |
| 155 | − "find-generic-password", | |
| 156 | − "-a", | |
| 157 | − input.providerHandle, | |
| 158 | − "-s", | |
| 159 | − input.keychainServiceName, | |
| 160 | − "-w", | |
| 161 | − ], | |
| 162 | − operation: "keychain.get", | |
| 163 | − timeoutMs: KEYCHAIN_COMMAND_TIMEOUT_MS, | |
| 75 | +}) => | |
| 76 | + Effect.flatMap( | |
| 77 | + createKeyringEntry(input), | |
| 78 | + (entry) => | |
| 79 | + Effect.try({ | |
| 80 | + try: () => entry.getPassword(), | |
| 81 | + catch: toError, | |
| 164 | 82 | }).pipe( |
| 165 | − Effect.flatMap((result) => | |
| 166 | − ensureCommandSuccess({ | |
| 167 | − result, | |
| 168 | − operation: "keychain.get", | |
| 169 | − message: "Failed loading secret from macOS keychain", | |
| 170 | − })), | |
| 171 | − Effect.map((result) => result.stdout.trimEnd()), | |
| 172 | − ); | |
| 173 | − case "linux": | |
| 174 | − return runCommand({ | |
| 175 | − command: "secret-tool", | |
| 176 | − args: [ | |
| 177 | − "lookup", | |
| 178 | − "service", | |
| 179 | − input.keychainServiceName, | |
| 180 | − "account", | |
| 181 | − input.providerHandle, | |
| 182 | − ], | |
| 183 | − operation: "keychain.get", | |
| 184 | − timeoutMs: KEYCHAIN_COMMAND_TIMEOUT_MS, | |
| 185 | − }).pipe( | |
| 186 | − Effect.flatMap((result) => | |
| 187 | − ensureCommandSuccess({ | |
| 188 | − result, | |
| 189 | − operation: "keychain.get", | |
| 190 | − message: "Failed loading secret from desktop keyring", | |
| 191 | − })), | |
| 192 | − Effect.map((result) => result.stdout.trimEnd()), | |
| 193 | − ); | |
| 194 | − default: | |
| 195 | − return Effect.fail( | |
| 196 | − runtimeEffectError( | |
| 197 | − "plugin-keychain-secret-store", | |
| 198 | − `keychain.get: unsupported on platform '${process.platform}'`, | |
| 199 | − ), | |
| 200 | − ); | |
| 201 | − } | |
| 202 | −}; | |
| 83 | + Effect.flatMap((value) => | |
| 84 | + value !== null | |
| 85 | + ? Effect.succeed(value) | |
| 86 | + : Effect.fail( | |
| 87 | + runtimeEffectError( | |
| 88 | + "plugin-keychain-secret-store", | |
| 89 | + `keychain.get: secret not found for service '${input.keychainServiceName}' and account '${input.providerHandle}'`, | |
| 90 | + ), | |
| 91 | + )), | |
| 92 | + ), | |
| 93 | + ); | |
| 203 | 94 | |
| 204 | 95 | const writeKeychainSecretValue = (input: { |
| 205 | 96 | providerHandle: string; |
| 206 | 97 | name?: string | null; |
| 207 | 98 | value: string; |
| 208 | 99 | keychainServiceName: string; |
| 209 | −}) => { | |
| 210 | − const secretName = trimOrNull(input.name); | |
| 211 | − | |
| 212 | − switch (process.platform) { | |
| 213 | − case "darwin": | |
| 214 | − return runCommand({ | |
| 215 | − command: "security", | |
| 216 | − args: [ | |
| 217 | − "add-generic-password", | |
| 218 | − "-a", | |
| 219 | − input.providerHandle, | |
| 220 | − "-s", | |
| 221 | − input.keychainServiceName, | |
| 222 | − "-w", | |
| 223 | − input.value, | |
| 224 | − "-U", | |
| 225 | − ...(secretName ? ["-l", secretName] : []), | |
| 226 | − ], | |
| 227 | − operation: "keychain.put", | |
| 228 | − timeoutMs: KEYCHAIN_COMMAND_TIMEOUT_MS, | |
| 229 | − }).pipe( | |
| 230 | − Effect.flatMap((result) => | |
| 231 | − ensureCommandSuccess({ | |
| 232 | − result, | |
| 233 | − operation: "keychain.put", | |
| 234 | − message: "Failed storing secret in macOS keychain", | |
| 235 | − })), | |
| 236 | − Effect.asVoid, | |
| 237 | − ); | |
| 238 | − case "linux": | |
| 239 | − return runCommand({ | |
| 240 | − command: "secret-tool", | |
| 241 | − args: [ | |
| 242 | − "store", | |
| 243 | − "--label", | |
| 244 | − secretName ?? input.keychainServiceName, | |
| 245 | − "service", | |
| 246 | − input.keychainServiceName, | |
| 247 | − "account", | |
| 248 | − input.providerHandle, | |
| 249 | − ], | |
| 250 | − stdin: input.value, | |
| 251 | − operation: "keychain.put", | |
| 252 | − timeoutMs: KEYCHAIN_COMMAND_TIMEOUT_MS, | |
| 253 | − }).pipe( | |
| 254 | − Effect.flatMap((result) => | |
| 255 | − ensureCommandSuccess({ | |
| 256 | − result, | |
| 257 | − operation: "keychain.put", | |
| 258 | − message: "Failed storing secret in desktop keyring", | |
| 259 | − })), | |
| 260 | − Effect.asVoid, | |
| 261 | − ); | |
| 262 | − default: | |
| 263 | − return Effect.fail( | |
| 264 | − runtimeEffectError( | |
| 265 | − "plugin-keychain-secret-store", | |
| 266 | − `keychain.put: unsupported on platform '${process.platform}'`, | |
| 267 | − ), | |
| 268 | − ); | |
| 269 | − } | |
| 270 | −}; | |
| 100 | +}) => | |
| 101 | + Effect.flatMap( | |
| 102 | + createKeyringEntry(input), | |
| 103 | + (entry) => | |
| 104 | + Effect.try({ | |
| 105 | + try: () => entry.setPassword(input.value), | |
| 106 | + catch: (cause) => | |
| 107 | + runtimeEffectError( | |
| 108 | + "plugin-keychain-secret-store", | |
| 109 | + `keychain.put: Failed storing secret in ${keychainDisplayName().toLowerCase()}: ${toError(cause).message}`, | |
| 110 | + ), | |
| 111 | + }).pipe(Effect.asVoid), | |
| 112 | + ); | |
| 271 | 113 | |
| 272 | 114 | const deleteKeychainSecretValue = (input: { |
| 273 | 115 | providerHandle: string; |
| 274 | 116 | keychainServiceName: string; |
| 275 | −}) => { | |
| 276 | − switch (process.platform) { | |
| 277 | − case "darwin": | |
| 278 | − return runCommand({ | |
| 279 | − command: "security", | |
| 280 | − args: [ | |
| 281 | − "delete-generic-password", | |
| 282 | − "-a", | |
| 283 | − input.providerHandle, | |
| 284 | − "-s", | |
| 285 | − input.keychainServiceName, | |
| 286 | − ], | |
| 287 | − operation: "keychain.delete", | |
| 288 | − timeoutMs: KEYCHAIN_COMMAND_TIMEOUT_MS, | |
| 289 | − }).pipe(Effect.map((result) => result.exitCode === 0)); | |
| 290 | − case "linux": | |
| 291 | − return runCommand({ | |
| 292 | − command: "secret-tool", | |
| 293 | − args: [ | |
| 294 | − "clear", | |
| 295 | − "service", | |
| 296 | − input.keychainServiceName, | |
| 297 | − "account", | |
| 298 | − input.providerHandle, | |
| 299 | − ], | |
| 300 | − operation: "keychain.delete", | |
| 301 | − timeoutMs: KEYCHAIN_COMMAND_TIMEOUT_MS, | |
| 302 | − }).pipe(Effect.map((result) => result.exitCode === 0)); | |
| 303 | − default: | |
| 304 | − return Effect.fail( | |
| 305 | − runtimeEffectError( | |
| 306 | − "plugin-keychain-secret-store", | |
| 307 | − `keychain.delete: unsupported on platform '${process.platform}'`, | |
| 308 | − ), | |
| 309 | − ); | |
| 310 | − } | |
| 311 | −}; | |
| 117 | +}) => | |
| 118 | + Effect.flatMap( | |
| 119 | + createKeyringEntry(input), | |
| 120 | + (entry) => | |
| 121 | + Effect.try({ | |
| 122 | + try: () => entry.deletePassword(), | |
| 123 | + catch: (cause) => | |
| 124 | + runtimeEffectError( | |
| 125 | + "plugin-keychain-secret-store", | |
| 126 | + `keychain.delete: Failed deleting secret from ${keychainDisplayName().toLowerCase()}: ${toError(cause).message}`, | |
| 127 | + ), | |
| 128 | + }), | |
| 129 | + ); | |
| 312 | 130 | |
| 313 | 131 | const builtinSecretStoreStorage = <TStored>(value: TStored) => ({ |
| 314 | 132 | get: () => Effect.succeed(value), |
| @@ -316,19 +134,29 @@ const builtinSecretStoreStorage = <TStored>(value: TStored) => ({ · 1 unmodified line | ||
| 316 | 134 | remove: () => Effect.void, |
| 317 | 135 | }); |
| 318 | 136 | |
| 319 | −export const keychainSecretStoreSdkPlugin = defineExecutorSecretStorePlugin({ | |
| 137 | +export const keychainSecretStoreSdkPlugin = defineExecutorSecretStorePlugin< | |
| 138 | + typeof KEYCHAIN_SECRET_STORE_KIND, | |
| 139 | + unknown, | |
| 140 | + { name: string }, | |
| 141 | + { kind: typeof KEYCHAIN_SECRET_STORE_KIND; name: string }, | |
| 142 | + {}, | |
| 143 | + KeychainSecretStoredData, | |
| 144 | + { | |
| 145 | + storeId: string; | |
| 146 | + config: { kind: typeof KEYCHAIN_SECRET_STORE_KIND; name: string }; | |
| 147 | + } | |
| 148 | +>({ | |
| 320 | 149 | key: KEYCHAIN_SECRET_STORE_KIND, |
| 321 | 150 | secretStore: { |
| 322 | 151 | kind: KEYCHAIN_SECRET_STORE_KIND, |
| 323 | − displayName: | |
| 324 | − process.platform === "darwin" ? "macOS Keychain" : "Desktop Keyring", | |
| 152 | + displayName: keychainDisplayName(), | |
| 325 | 153 | builtin: { |
| 326 | 154 | storeId: KEYCHAIN_SECRET_STORE_ID, |
| 327 | 155 | defaultPriority: 10, |
| 328 | − enabled: () => process.platform === "darwin" || process.platform === "linux", | |
| 156 | + enabled: isSupportedPlatform, | |
| 329 | 157 | createStore: () => ({ |
| 330 | 158 | kind: KEYCHAIN_SECRET_STORE_KIND, |
| 331 | − name: process.platform === "darwin" ? "macOS Keychain" : "Desktop Keyring", | |
| 159 | + name: keychainDisplayName(), | |
| 332 | 160 | status: "connected", |
| 333 | 161 | enabled: true, |
| 334 | 162 | }), |
| @@ -352,50 +180,50 @@ export const keychainSecretStoreSdkPlugin = defineExecutorSecretStorePlugin({ · 17 unmodified lines | ||
| 352 | 180 | kind: KEYCHAIN_SECRET_STORE_KIND, |
| 353 | 181 | name: store.name, |
| 354 | 182 | }), |
| 355 | − resolveSecret: ({ secret }) => | |
| 183 | + resolveSecret: ({ secretStored }) => | |
| 356 | 184 | readKeychainSecretValue({ |
| 357 | − providerHandle: secret.handle, | |
| 185 | + providerHandle: secretStored.account, | |
| 358 | 186 | keychainServiceName: resolveKeychainServiceName(undefined), |
| 359 | 187 | }), |
| 360 | 188 | createSecret: ({ value, name }) => |
| 361 | 189 | Effect.gen(function* () { |
| 362 | − const providerHandle = randomUUID(); | |
| 190 | + const account = randomUUID(); | |
| 363 | 191 | yield* writeKeychainSecretValue({ |
| 364 | − providerHandle, | |
| 192 | + providerHandle: account, | |
| 365 | 193 | name, |
| 366 | 194 | value, |
| 367 | 195 | keychainServiceName: resolveKeychainServiceName(undefined), |
| 368 | 196 | }); |
| 369 | 197 | return { |
| 370 | − handle: providerHandle, | |
| 371 | 198 | name: trimOrNull(name), |
| 372 | − value: null, | |
| 199 | + secretStored: { | |
| 200 | + account, | |
| 201 | + } satisfies KeychainSecretStoredData, | |
| 373 | 202 | }; |
| 374 | 203 | }), |
| 375 | − updateSecret: ({ secret, name, value }) => | |
| 204 | + updateSecret: ({ secret, secretStored, name, value }) => | |
| 376 | 205 | Effect.gen(function* () { |
| 377 | 206 | const keychainServiceName = resolveKeychainServiceName(undefined); |
| 378 | 207 | const nextName = trimOrNull(name ?? secret.name); |
| 379 | 208 | const nextValue = value |
| 380 | 209 | ?? (yield* readKeychainSecretValue({ |
| 381 | − providerHandle: secret.handle, | |
| 210 | + providerHandle: secretStored.account, | |
| 382 | 211 | keychainServiceName, |
| 383 | 212 | })); |
| 384 | 213 | yield* writeKeychainSecretValue({ |
| 385 | − providerHandle: secret.handle, | |
| 214 | + providerHandle: secretStored.account, | |
| 386 | 215 | name: nextName, |
| 387 | 216 | value: nextValue, |
| 388 | 217 | keychainServiceName, |
| 389 | 218 | }); |
| 390 | 219 | return { |
| 391 | − handle: secret.handle, | |
| 392 | 220 | name: nextName, |
| 393 | − value: null, | |
| 221 | + secretStored, | |
| 394 | 222 | }; |
| 395 | 223 | }), |
| 396 | − deleteSecret: ({ secret }) => | |
| 224 | + deleteSecret: ({ secretStored }) => | |
| 397 | 225 | deleteKeychainSecretValue({ |
| 398 | − providerHandle: secret.handle, | |
| 226 | + providerHandle: secretStored.account, | |
| 399 | 227 | keychainServiceName: resolveKeychainServiceName(undefined), |
| 400 | 228 | }), |
| 401 | 229 | capabilities: () => ({ |
| @@ -13,6 +13,7 @@ · 12 unmodified lines | ||
| 13 | 13 | }, |
| 14 | 14 | "dependencies": { |
| 15 | 15 | "@executor/platform-sdk": "workspace:*", |
| 16 | + "@napi-rs/keyring": "^1.2.0", | |
| 16 | 17 | "effect": "catalog:" |
| 17 | 18 | }, |
| 18 | 19 | "devDependencies": { |
| @@ -1,17 +1,16 @@ | ||
| 1 | −import { randomUUID } from "node:crypto"; | |
| 2 | − | |
| 3 | 1 | import * as Effect from "effect/Effect"; |
| 4 | 2 | |
| 5 | 3 | import { |
| 6 | 4 | defineExecutorSecretStorePlugin, |
| 7 | 5 | } from "@executor/platform-sdk/plugins"; |
| 8 | −import { | |
| 9 | − runtimeEffectError, | |
| 10 | −} from "@executor/platform-sdk/runtime"; | |
| 11 | 6 | |
| 12 | 7 | export const LOCAL_SECRET_STORE_KIND = "local"; |
| 13 | 8 | export const LOCAL_SECRET_STORE_ID = "sts_builtin_local"; |
| 14 | 9 | |
| 10 | +type LocalSecretStoredData = { | |
| 11 | + value: string; | |
| 12 | +}; | |
| 13 | + | |
| 15 | 14 | const builtinSecretStoreStorage = <TStored>(value: TStored) => ({ |
| 16 | 15 | get: () => Effect.succeed(value), |
| 17 | 16 | put: () => Effect.void, |
| @@ -27,7 +26,18 @@ const trimOrNull = (value: string | null | undefined): string | null => { · 9 unmodified lines | ||
| 27 | 26 | return trimmed.length > 0 ? trimmed : null; |
| 28 | 27 | }; |
| 29 | 28 | |
| 30 | −export const localSecretStoreSdkPlugin = defineExecutorSecretStorePlugin({ | |
| 29 | +export const localSecretStoreSdkPlugin = defineExecutorSecretStorePlugin< | |
| 30 | + typeof LOCAL_SECRET_STORE_KIND, | |
| 31 | + unknown, | |
| 32 | + { name: string }, | |
| 33 | + { kind: typeof LOCAL_SECRET_STORE_KIND; name: string }, | |
| 34 | + {}, | |
| 35 | + LocalSecretStoredData, | |
| 36 | + { | |
| 37 | + storeId: string; | |
| 38 | + config: { kind: typeof LOCAL_SECRET_STORE_KIND; name: string }; | |
| 39 | + } | |
| 40 | +>({ | |
| 31 | 41 | key: LOCAL_SECRET_STORE_KIND, |
| 32 | 42 | secretStore: { |
| 33 | 43 | kind: LOCAL_SECRET_STORE_KIND, |
| @@ -61,29 +71,20 @@ export const localSecretStoreSdkPlugin = defineExecutorSecretStorePlugin({ · 27 unmodified lines | ||
| 61 | 71 | kind: LOCAL_SECRET_STORE_KIND, |
| 62 | 72 | name: store.name, |
| 63 | 73 | }), |
| 64 | − resolveSecret: ({ secret }) => { | |
| 65 | − if (secret.value === null) { | |
| 66 | − return Effect.fail( | |
| 67 | − runtimeEffectError( | |
| 68 | − "plugin-local-secret-store", | |
| 69 | − `Local secret ${secret.id} does not have a stored value`, | |
| 70 | − ), | |
| 71 | − ); | |
| 72 | − } | |
| 73 | − | |
| 74 | − return Effect.succeed(secret.value); | |
| 75 | − }, | |
| 74 | + resolveSecret: ({ secretStored }) => Effect.succeed(secretStored.value), | |
| 76 | 75 | createSecret: ({ value, name }) => |
| 77 | 76 | Effect.succeed({ |
| 78 | − handle: `local:${randomUUID()}`, | |
| 79 | 77 | name: trimOrNull(name), |
| 80 | − value, | |
| 78 | + secretStored: { | |
| 79 | + value, | |
| 80 | + } satisfies LocalSecretStoredData, | |
| 81 | 81 | }), |
| 82 | − updateSecret: ({ secret, name, value }) => | |
| 82 | + updateSecret: ({ secret, secretStored, name, value }) => | |
| 83 | 83 | Effect.succeed({ |
| 84 | − handle: secret.handle, | |
| 85 | 84 | name: trimOrNull(name ?? secret.name), |
| 86 | − ...(value !== undefined ? { value } : {}), | |
| 85 | + secretStored: { | |
| 86 | + value: value ?? secretStored.value, | |
| 87 | + } satisfies LocalSecretStoredData, | |
| 87 | 88 | }), |
| 88 | 89 | deleteSecret: () => Effect.succeed(true), |
| 89 | 90 | capabilities: () => ({ |
| @@ -58,6 +58,10 @@ export type OnePasswordStoreStorage = { · 57 unmodified lines | ||
| 58 | 58 | }) => Effect.Effect<void, Error, never>; |
| 59 | 59 | }; |
| 60 | 60 | |
| 61 | +type OnePasswordSecretStoredData = { | |
| 62 | + uri: string; | |
| 63 | +}; | |
| 64 | + | |
| 61 | 65 | const ONEPASSWORD_REQUEST_TIMEOUT_MS = 15_000; |
| 62 | 66 | |
| 63 | 67 | const timedOutOnePasswordRequestError = (operation: string) => |
| @@ -373,9 +377,10 @@ const importSecretFromSelection = (input: { · 309 unmodified lines | ||
| 373 | 377 | } |
| 374 | 378 | |
| 375 | 379 | return { |
| 376 | − handle: `op://${input.stored!.vaultId}/${item.id}/${field.id}`, | |
| 380 | + secretStored: { | |
| 381 | + uri: `op://${input.stored!.vaultId}/${item.id}/${field.id}`, | |
| 382 | + } satisfies OnePasswordSecretStoredData, | |
| 377 | 383 | name: input.name?.trim() || `${item.title} · ${field.title}`, |
| 378 | − value: null, | |
| 379 | 384 | }; |
| 380 | 385 | }); |
| 381 | 386 | |
| @@ -385,29 +390,37 @@ const createImportedSecretRecord = (input: { · 3 unmodified lines | ||
| 385 | 390 | storage: { |
| 386 | 391 | secrets: { |
| 387 | 392 | upsert: (value: SecretMaterial) => Effect.Effect<void, Error, never>; |
| 393 | + secretMaterialStoredData: { | |
| 394 | + upsert: (value: { | |
| 395 | + secretId: SecretMaterial["id"]; | |
| 396 | + data: OnePasswordSecretStoredData; | |
| 397 | + }) => Effect.Effect<void, Error, never>; | |
| 398 | + }; | |
| 388 | 399 | }; |
| 389 | 400 | }; |
| 390 | 401 | }; |
| 391 | 402 | }; |
| 392 | 403 | store: SecretStore; |
| 393 | − handle: string; | |
| 404 | + secretStored: OnePasswordSecretStoredData; | |
| 394 | 405 | name: string | null; |
| 395 | 406 | purpose?: SecretMaterialPurpose; |
| 396 | −}): Effect.Effect<OnePasswordImportSecretResult, Error, never> => | |
| 407 | +}): Effect.Effect<OnePasswordImportSecretResult, Error, any> => | |
| 397 | 408 | Effect.gen(function* () { |
| 398 | 409 | const now = Date.now(); |
| 399 | 410 | const secret: SecretMaterial = { |
| 400 | 411 | id: SecretMaterialIdSchema.make(`sec_${crypto.randomUUID()}`), |
| 401 | 412 | storeId: input.store.id, |
| 402 | − handle: input.handle, | |
| 403 | 413 | name: input.name, |
| 404 | 414 | purpose: input.purpose ?? "auth_material", |
| 405 | − value: null, | |
| 406 | 415 | createdAt: now, |
| 407 | 416 | updatedAt: now, |
| 408 | 417 | }; |
| 409 | 418 | |
| 410 | 419 | yield* input.executor.runtime.storage.secrets.upsert(secret); |
| 420 | + yield* input.executor.runtime.storage.secrets.secretMaterialStoredData.upsert({ | |
| 421 | + secretId: secret.id, | |
| 422 | + data: input.secretStored, | |
| 423 | + }); | |
| 411 | 424 | |
| 412 | 425 | return { |
| 413 | 426 | id: secret.id, |
| @@ -419,14 +432,14 @@ const createImportedSecretRecord = (input: { · 5 unmodified lines | ||
| 419 | 432 | }; |
| 420 | 433 | }); |
| 421 | 434 | |
| 422 | −const parseSecretReference = (handle: string): { | |
| 435 | +const parseSecretReference = (uri: string): { | |
| 423 | 436 | vaultId: string; |
| 424 | 437 | itemId: string; |
| 425 | 438 | fieldId: string; |
| 426 | 439 | } => { |
| 427 | − const match = /^op:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(handle); | |
| 440 | + const match = /^op:\/\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(uri); | |
| 428 | 441 | if (!match) { |
| 429 | − throw new Error(`Invalid 1Password secret reference: ${handle}`); | |
| 442 | + throw new Error(`Invalid 1Password secret reference: ${uri}`); | |
| 430 | 443 | } |
| 431 | 444 | |
| 432 | 445 | return { |
| @@ -461,6 +474,7 @@ export const onePasswordSdkPlugin = (input: { · 28 unmodified lines | ||
| 461 | 474 | OnePasswordConnectInput, |
| 462 | 475 | OnePasswordStoreConfigPayload, |
| 463 | 476 | OnePasswordStoredStoreData, |
| 477 | + OnePasswordSecretStoredData, | |
| 464 | 478 | OnePasswordUpdateStoreInput |
| 465 | 479 | >({ |
| 466 | 480 | key: ONEPASSWORD_SECRET_STORE_KIND, |
| @@ -502,12 +516,12 @@ export const onePasswordSdkPlugin = (input: { · 35 unmodified lines | ||
| 502 | 516 | vaultId: stored.vaultId, |
| 503 | 517 | auth: stored.auth, |
| 504 | 518 | } satisfies OnePasswordStoreConfigPayload), |
| 505 | − resolveSecret: ({ secret, stored }) => | |
| 519 | + resolveSecret: ({ secretStored, stored }) => | |
| 506 | 520 | Effect.flatMap(makeClient(stored), (client) => |
| 507 | 521 | Effect.tryPromise({ |
| 508 | 522 | try: () => |
| 509 | 523 | runOnePasswordRequest("secret resolution", () => |
| 510 | − client.secrets.resolve(secret.handle) | |
| 524 | + client.secrets.resolve(secretStored.uri) | |
| 511 | 525 | ), |
| 512 | 526 | catch: (cause) => |
| 513 | 527 | cause instanceof Error ? cause : new Error(String(cause)), |
| @@ -533,18 +547,20 @@ export const onePasswordSdkPlugin = (input: { · 19 unmodified lines | ||
| 533 | 547 | ); |
| 534 | 548 | |
| 535 | 549 | return { |
| 536 | − handle: `op://${stored!.vaultId}/${item.id}/${ONEPASSWORD_SECRET_FIELD_ID}`, | |
| 550 | + secretStored: { | |
| 551 | + uri: `op://${stored!.vaultId}/${item.id}/${ONEPASSWORD_SECRET_FIELD_ID}`, | |
| 552 | + } satisfies OnePasswordSecretStoredData, | |
| 537 | 553 | name: item.title, |
| 538 | 554 | }; |
| 539 | 555 | }, |
| 540 | 556 | catch: (cause) => |
| 541 | 557 | cause instanceof Error ? cause : new Error(String(cause)), |
| 542 | 558 | })), |
| 543 | − updateSecret: ({ secret, stored, name, value }) => | |
| 559 | + updateSecret: ({ secret, secretStored, stored, name, value }) => | |
| 544 | 560 | Effect.flatMap(makeClient(stored), (client) => |
| 545 | 561 | Effect.tryPromise({ |
| 546 | 562 | try: async () => { |
| 547 | − const parsed = parseSecretReference(secret.handle); | |
| 563 | + const parsed = parseSecretReference(secretStored.uri); | |
| 548 | 564 | const item = await runOnePasswordRequest("secret update", () => |
| 549 | 565 | client.items.get(parsed.vaultId, parsed.itemId) |
| 550 | 566 | ); |
| @@ -559,16 +575,17 @@ export const onePasswordSdkPlugin = (input: { · 8 unmodified lines | ||
| 559 | 575 | ); |
| 560 | 576 | return { |
| 561 | 577 | name: updated.title, |
| 578 | + secretStored, | |
| 562 | 579 | }; |
| 563 | 580 | }, |
| 564 | 581 | catch: (cause) => |
| 565 | 582 | cause instanceof Error ? cause : new Error(String(cause)), |
| 566 | 583 | })), |
| 567 | − deleteSecret: ({ secret, stored }) => | |
| 584 | + deleteSecret: ({ secretStored, stored }) => | |
| 568 | 585 | Effect.flatMap(makeClient(stored), (client) => |
| 569 | 586 | Effect.tryPromise({ |
| 570 | 587 | try: async () => { |
| 571 | − const parsed = parseSecretReference(secret.handle); | |
| 588 | + const parsed = parseSecretReference(secretStored.uri); | |
| 572 | 589 | await runOnePasswordRequest("secret deletion", () => |
| 573 | 590 | client.items.delete(parsed.vaultId, parsed.itemId) |
| 574 | 591 | ); |
| @@ -662,7 +679,7 @@ export const onePasswordSdkPlugin = (input: { · 87 unmodified lines | ||
| 662 | 679 | return yield* createImportedSecretRecord({ |
| 663 | 680 | executor, |
| 664 | 681 | store, |
| 665 | − handle: imported.handle, | |
| 682 | + secretStored: imported.secretStored, | |
| 666 | 683 | name: imported.name, |
| 667 | 684 | }); |
| 668 | 685 | }), |
| @@ -202,7 +202,6 @@ const openApiStoredSourceDataFromLocalConfig = (input: { · 201 unmodified lines | ||
| 202 | 202 | lastSyncAt: null, |
| 203 | 203 | }); |
| 204 | 204 | } |
| 205 | − | |
| 206 | 205 | throw new Error("Unsupported OpenAPI local source config."); |
| 207 | 206 | }; |
| 208 | 207 | |