History

Merge remote-tracking branch 'origin/codex/batch17-onepassword-boundaries' into codex/effect-lint-integration

Rhys Sullivan committed 6 months ago

Commit 422a4242 parents 0477d35 81e97c32 files · +113 −170Browse files

Unified · Split

2 files
  1. packages/plugins/onepassword/src/sdk/plugin.ts +104 −159
  2. packages/plugins/onepassword/src/sdk/service.ts +9 −11
packages/plugins/onepassword/src/sdk/plugin.ts+104−159
@@ -10,19 +10,12 @@ import { · 9 unmodified lines
1010 } from "@executor-js/sdk/core";
1111
1212 import { OnePasswordGroup } from "../api/group";
13−import {
14− OnePasswordExtensionService,
15− OnePasswordHandlers,
16−} from "../api/handlers";
13+import { OnePasswordExtensionService, OnePasswordHandlers } from "../api/handlers";
1714
1815 import { OnePasswordConfig, Vault, ConnectionStatus } from "./types";
1916 import type { OnePasswordAuth } from "./types";
2017 import { OnePasswordError } from "./errors";
21−import {
22− makeOnePasswordService,
23− type ResolvedAuth,
24− type OnePasswordService,
25−} from "./service";
18+import { makeOnePasswordService, type ResolvedAuth, type OnePasswordService } from "./service";
2619
2720 // ---------------------------------------------------------------------------
2821 // Constants
@@ -50,38 +43,6 @@ export type OnePasswordExtensionFailure = OnePasswordError | StorageFailure; · 21 unmodified lines
5043 // Plugin extension — public API on executor.onepassword
5144 // ---------------------------------------------------------------------------
5245
53−export interface OnePasswordExtension {
54− /** Configure the 1Password connection */
55− readonly configure: (
56− config: OnePasswordConfig,
57− ) => Effect.Effect<void, StorageFailure>;
58−
59− /** Get current configuration (if any) */
60− readonly getConfig: () => Effect.Effect<
61− OnePasswordConfig | null,
62− OnePasswordExtensionFailure
63− >;
64−
65− /** Remove the 1Password configuration */
66− readonly removeConfig: () => Effect.Effect<void, StorageFailure>;
67−
68− /** Check connection status */
69− readonly status: () => Effect.Effect<
70− ConnectionStatus,
71− OnePasswordExtensionFailure
72− >;
73−
74− /** List accessible vaults (requires auth) */
75− readonly listVaults: (
76− auth: OnePasswordAuth,
77− ) => Effect.Effect<ReadonlyArray<Vault>, OnePasswordExtensionFailure>;
78−
79− /** Resolve a secret directly by op:// URI */
80− readonly resolve: (
81− uri: string,
82− ) => Effect.Effect<string, OnePasswordExtensionFailure>;
83−}
84−
8546 // ---------------------------------------------------------------------------
8647 // Typed config store — single blob, JSON encoded. Blob I/O failures surface
8748 // as `StorageError` (HTTP edge translates to `InternalError`); decode
@@ -94,20 +55,17 @@ export interface OnePasswordStore { · 6 unmodified lines
9455 OnePasswordConfig | null,
9556 StorageError | OnePasswordError
9657 >;
97− readonly saveConfig: (
98− config: OnePasswordConfig,
99− ) => Effect.Effect<void, StorageError>;
58+ readonly saveConfig: (config: OnePasswordConfig) => Effect.Effect<void, StorageError>;
10059 readonly deleteConfig: () => Effect.Effect<void, StorageError>;
10160 }
10261
103−const decodeConfig = Schema.decodeUnknownSync(OnePasswordConfig);
62+const decodeConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(OnePasswordConfig));
10463
105−const blobStorageError = (operation: string) =>
64+const blobStorageError =
65+ (operation: string) =>
10666 (cause: unknown): StorageError =>
10767 new StorageError({
108− message: `onepassword blob ${operation}: ${
109− cause instanceof Error ? cause.message : String(cause)
110− }`,
68+ message: `onepassword blob ${operation} failed`,
11169 cause,
11270 });
11371
@@ -123,14 +81,15 @@ export const makeOnePasswordStore = ( · 9 unmodified lines
12381 Effect.mapError(blobStorageError("read")),
12482 Effect.flatMap((raw) => {
12583 if (raw === null) return Effect.succeed(null);
126− return Effect.try({
127− try: () => decodeConfig(JSON.parse(raw)),
128− catch: (cause) =>
129− new OnePasswordError({
130− operation: "config decode",
131− message: cause instanceof Error ? cause.message : String(cause),
132− }),
133− });
84+ return decodeConfig(raw).pipe(
85+ Effect.mapError(
86+ () =>
87+ new OnePasswordError({
88+ operation: "config decode",
89+ message: "Failed to decode 1Password config",
90+ }),
91+ ),
92+ );
13493 }),
13594 ),
13695
@@ -168,13 +127,13 @@ const resolveAuth = ( · 31 unmodified lines
168127 });
169128 }
170129 return ctx.secrets.get(auth.tokenSecretId).pipe(
171− Effect.mapError((err) =>
172− "_tag" in err && err._tag === "SecretOwnedByConnectionError"
173− ? new OnePasswordError({
174− operation: "auth resolution",
175− message: `Service account token secret "${auth.tokenSecretId}" not found`,
176− })
177− : err,
130+ Effect.catchTag("SecretOwnedByConnectionError", () =>
131+ Effect.fail(
132+ new OnePasswordError({
133+ operation: "auth resolution",
134+ message: `Service account token secret "${auth.tokenSecretId}" not found`,
135+ }),
136+ ),
178137 ),
179138 Effect.flatMap((token) => {
180139 if (token === null) {
@@ -200,9 +159,7 @@ const getServiceFromConfig = ( · 19 unmodified lines
200159 preferSdk: boolean | undefined,
201160 ): Effect.Effect<OnePasswordService, OnePasswordError | StorageFailure> =>
202161 resolveAuth(config.auth, ctx).pipe(
203− Effect.flatMap((resolved) =>
204− makeOnePasswordService(resolved, { timeoutMs, preferSdk }),
205− ),
162+ Effect.flatMap((resolved) => makeOnePasswordService(resolved, { timeoutMs, preferSdk })),
206163 );
207164
208165 // ---------------------------------------------------------------------------
@@ -241,10 +198,7 @@ const makeProvider = ( · 32 unmodified lines
241198 list: () =>
242199 ctx.storage.getConfig().pipe(
243200 Effect.flatMap((config) => {
244− if (!config)
245− return Effect.succeed(
246− [] as ReadonlyArray<{ id: string; name: string }>,
247− );
201+ if (!config) return Effect.succeed([] as ReadonlyArray<{ id: string; name: string }>);
248202 return getServiceFromConfig(config, ctx, timeoutMs, preferSdk).pipe(
249203 Effect.flatMap((svc) => svc.listItems(config.vaultId)),
250204 Effect.map(
@@ -253,12 +207,70 @@ const makeProvider = ( · 2 unmodified lines
253207 ),
254208 );
255209 }),
256− Effect.orElseSucceed(
257− () => [] as ReadonlyArray<{ id: string; name: string }>,
258− ),
210+ Effect.orElseSucceed(() => [] as ReadonlyArray<{ id: string; name: string }>),
259211 ),
260212 });
261213
214+const makeOnePasswordExtension = (
215+ ctx: PluginCtx<OnePasswordStore>,
216+ timeoutMs: number,
217+ preferSdk: boolean | undefined,
218+) => {
219+ return {
220+ configure: (config: OnePasswordConfig) => ctx.storage.saveConfig(config),
221+
222+ getConfig: () => ctx.storage.getConfig(),
223+
224+ removeConfig: () => ctx.storage.deleteConfig(),
225+
226+ status: () =>
227+ Effect.gen(function* () {
228+ const config = yield* ctx.storage.getConfig();
229+ if (!config) {
230+ return new ConnectionStatus({
231+ connected: false,
232+ error: "Not configured",
233+ });
234+ }
235+ const svc = yield* getServiceFromConfig(config, ctx, timeoutMs, preferSdk);
236+ const vaults = yield* svc.listVaults();
237+ const vault = vaults.find((v) => v.id === config.vaultId);
238+ return new ConnectionStatus({
239+ connected: true,
240+ vaultName: vault?.title,
241+ });
242+ }),
243+
244+ listVaults: (auth: OnePasswordAuth) =>
245+ Effect.gen(function* () {
246+ const resolved = yield* resolveAuth(auth, ctx);
247+ const svc = yield* makeOnePasswordService(resolved, {
248+ timeoutMs,
249+ preferSdk,
250+ });
251+ const vaults = yield* svc.listVaults();
252+ return vaults
253+ .map((v) => new Vault({ id: v.id, name: v.title }))
254+ .sort((a, b) => a.name.localeCompare(b.name));
255+ }),
256+
257+ resolve: (uri: string) =>
258+ Effect.gen(function* () {
259+ const config = yield* ctx.storage.getConfig();
260+ if (!config) {
261+ return yield* new OnePasswordError({
262+ operation: "resolve",
263+ message: "1Password is not configured",
264+ });
265+ }
266+ const svc = yield* getServiceFromConfig(config, ctx, timeoutMs, preferSdk);
267+ return yield* svc.resolveSecret(uri);
268+ }),
269+ };
270+};
271+
272+export type OnePasswordExtension = ReturnType<typeof makeOnePasswordExtension>;
273+
262274 // ---------------------------------------------------------------------------
263275 // Plugin factory
264276 // ---------------------------------------------------------------------------
@@ -270,88 +282,21 @@ export interface OnePasswordPluginOptions { · 5 unmodified lines
270282 readonly preferSdk?: boolean;
271283 }
272284
273−export const onepasswordPlugin = definePlugin(
274− (options?: OnePasswordPluginOptions) => {
275− const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
276− const preferSdk = options?.preferSdk;
277−
278− return {
279− id: "onepassword" as const,
280− packageName: "@executor-js/plugin-onepassword",
281− storage: ({ blobs, scopes }) =>
282− makeOnePasswordStore(blobs, scopes.at(-1)!.id as string),
283−
284− extension: (ctx) => {
285− return {
286− configure: (config) => ctx.storage.saveConfig(config),
287−
288− getConfig: () => ctx.storage.getConfig(),
289−
290− removeConfig: () => ctx.storage.deleteConfig(),
291−
292− status: () =>
293− Effect.gen(function* () {
294− const config = yield* ctx.storage.getConfig();
295− if (!config) {
296− return new ConnectionStatus({
297− connected: false,
298− error: "Not configured",
299− });
300− }
301− const svc = yield* getServiceFromConfig(
302− config,
303− ctx,
304− timeoutMs,
305− preferSdk,
306− );
307− const vaults = yield* svc.listVaults();
308− const vault = vaults.find((v) => v.id === config.vaultId);
309− return new ConnectionStatus({
310− connected: true,
311− vaultName: vault?.title,
312− });
313− }),
314−
315− listVaults: (auth) =>
316− Effect.gen(function* () {
317− const resolved = yield* resolveAuth(auth, ctx);
318− const svc = yield* makeOnePasswordService(resolved, {
319− timeoutMs,
320− preferSdk,
321− });
322− const vaults = yield* svc.listVaults();
323− return vaults
324− .map((v) => new Vault({ id: v.id, name: v.title }))
325− .sort((a, b) => a.name.localeCompare(b.name));
326− }),
327−
328− resolve: (uri) =>
329− Effect.gen(function* () {
330− const config = yield* ctx.storage.getConfig();
331− if (!config) {
332− return yield* Effect.fail(
333− new OnePasswordError({
334− operation: "resolve",
335− message: "1Password is not configured",
336− }),
337− );
338− }
339− const svc = yield* getServiceFromConfig(
340− config,
341− ctx,
342− timeoutMs,
343− preferSdk,
344− );
345− return yield* svc.resolveSecret(uri);
346− }),
347− } satisfies OnePasswordExtension;
348− },
349−
350− secretProviders: (ctx) => [makeProvider(ctx, timeoutMs, preferSdk)],
351−
352− routes: () => OnePasswordGroup,
353− handlers: () => OnePasswordHandlers,
354− extensionService: OnePasswordExtensionService,
355− };
356− },
357−);
285+export const onepasswordPlugin = definePlugin((options?: OnePasswordPluginOptions) => {
286+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
287+ const preferSdk = options?.preferSdk;
288+
289+ return {
290+ id: "onepassword" as const,
291+ packageName: "@executor-js/plugin-onepassword",
292+ storage: ({ blobs, scopes }) => makeOnePasswordStore(blobs, scopes.at(-1)!.id),
293+
294+ extension: (ctx) => makeOnePasswordExtension(ctx, timeoutMs, preferSdk),
295+
296+ secretProviders: (ctx) => [makeProvider(ctx, timeoutMs, preferSdk)],
297+
298+ routes: () => OnePasswordGroup,
299+ handlers: () => OnePasswordHandlers,
300+ extensionService: OnePasswordExtensionService,
301+ };
302+});
packages/plugins/onepassword/src/sdk/service.ts+9−11
@@ -53,10 +53,10 @@ type OnePasswordSdkModule = typeof import("@1password/sdk"); · 52 unmodified lines
5353 const loadOnePasswordSdk = (): Effect.Effect<OnePasswordSdkModule, OnePasswordError> =>
5454 Effect.tryPromise({
5555 try: () => import("@1password/sdk"),
56− catch: (cause) =>
56+ catch: () =>
5757 new OnePasswordError({
5858 operation: "sdk module load",
59− message: cause instanceof Error ? cause.message : String(cause),
59+ message: "Failed to load 1Password SDK",
6060 }),
6161 });
6262
@@ -99,22 +99,20 @@ export const makeNativeSdkService = ( · 36 unmodified lines
9999 integrationName: "Executor",
100100 integrationVersion: "0.0.0",
101101 }),
102− catch: (cause) =>
102+ catch: () =>
103103 new OnePasswordError({
104104 operation: "client setup",
105− message: cause instanceof Error ? cause.message : String(cause),
105+ message: "Failed to set up 1Password client",
106106 }),
107− }).pipe(
108− timeoutWithOnePasswordError("client setup", timeoutMs),
109− );
107+ }).pipe(timeoutWithOnePasswordError("client setup", timeoutMs));
110108
111109 const wrap = <A>(fn: () => Promise<A>, operation: string): Effect.Effect<A, OnePasswordError> =>
112110 Effect.tryPromise({
113111 try: fn,
114− catch: (cause) =>
112+ catch: () =>
115113 new OnePasswordError({
116114 operation,
117− message: cause instanceof Error ? cause.message : String(cause),
115+ message: `1Password SDK ${operation} failed`,
118116 }),
119117 }).pipe(
120118 timeoutWithOnePasswordError(operation, timeoutMs),
@@ -154,10 +152,10 @@ export const makeCliService = ( · 33 unmodified lines
154152 const wrapSync = <A>(fn: () => A, operation: string): Effect.Effect<A, OnePasswordError> =>
155153 Effect.try({
156154 try: fn,
157− catch: (cause) =>
155+ catch: () =>
158156 new OnePasswordError({
159157 operation,
160− message: cause instanceof Error ? cause.message : String(cause),
158+ message: `1Password CLI ${operation} failed`,
161159 }),
162160 }).pipe(Effect.withSpan(`onepassword.cli.${operation}`));
163161