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
10} from "@executor-js/sdk/core";10} from "@executor-js/sdk/core";
11 11
12import { OnePasswordGroup } from "../api/group";12import { OnePasswordGroup } from "../api/group";
13import {13import { OnePasswordExtensionService, OnePasswordHandlers } from "../api/handlers";
14 OnePasswordExtensionService,
15 OnePasswordHandlers,
16} from "../api/handlers";
17 14
18import { OnePasswordConfig, Vault, ConnectionStatus } from "./types";15import { OnePasswordConfig, Vault, ConnectionStatus } from "./types";
19import type { OnePasswordAuth } from "./types";16import type { OnePasswordAuth } from "./types";
20import { OnePasswordError } from "./errors";17import { OnePasswordError } from "./errors";
21import {18import { makeOnePasswordService, type ResolvedAuth, type OnePasswordService } from "./service";
22 makeOnePasswordService,
23 type ResolvedAuth,
24 type OnePasswordService,
25} from "./service";
26 19
27// ---------------------------------------------------------------------------20// ---------------------------------------------------------------------------
28// Constants21// Constants
@@ -50,38 +43,6 @@ export type OnePasswordExtensionFailure = OnePasswordError | StorageFailure; · 21 unmodified lines
50// Plugin extension — public API on executor.onepassword43// Plugin extension — public API on executor.onepassword
51// ---------------------------------------------------------------------------44// ---------------------------------------------------------------------------
52 45
53export 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
85// ---------------------------------------------------------------------------46// ---------------------------------------------------------------------------
86// Typed config store — single blob, JSON encoded. Blob I/O failures surface47// Typed config store — single blob, JSON encoded. Blob I/O failures surface
87// as `StorageError` (HTTP edge translates to `InternalError`); decode48// as `StorageError` (HTTP edge translates to `InternalError`); decode
@@ -94,20 +55,17 @@ export interface OnePasswordStore { · 6 unmodified lines
94 OnePasswordConfig | null,55 OnePasswordConfig | null,
95 StorageError | OnePasswordError56 StorageError | OnePasswordError
96 >;57 >;
97 readonly saveConfig: (58 readonly saveConfig: (config: OnePasswordConfig) => Effect.Effect<void, StorageError>;
98 config: OnePasswordConfig,
99 ) => Effect.Effect<void, StorageError>;
100 readonly deleteConfig: () => Effect.Effect<void, StorageError>;59 readonly deleteConfig: () => Effect.Effect<void, StorageError>;
101}60}
102 61
103const decodeConfig = Schema.decodeUnknownSync(OnePasswordConfig);62const decodeConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(OnePasswordConfig));
104 63
105const blobStorageError = (operation: string) =>64const blobStorageError =
65 (operation: string) =>
106 (cause: unknown): StorageError =>66 (cause: unknown): StorageError =>
107 new StorageError({67 new StorageError({
108 message: `onepassword blob ${operation}: ${68 message: `onepassword blob ${operation} failed`,
109 cause instanceof Error ? cause.message : String(cause)
110 }`,
111 cause,69 cause,
112 });70 });
113 71
@@ -123,14 +81,15 @@ export const makeOnePasswordStore = ( · 9 unmodified lines
123 Effect.mapError(blobStorageError("read")),81 Effect.mapError(blobStorageError("read")),
124 Effect.flatMap((raw) => {82 Effect.flatMap((raw) => {
125 if (raw === null) return Effect.succeed(null);83 if (raw === null) return Effect.succeed(null);
126 return Effect.try({84 return decodeConfig(raw).pipe(
127 try: () => decodeConfig(JSON.parse(raw)),85 Effect.mapError(
128 catch: (cause) =>86 () =>
129 new OnePasswordError({87 new OnePasswordError({
130 operation: "config decode",88 operation: "config decode",
131 message: cause instanceof Error ? cause.message : String(cause),89 message: "Failed to decode 1Password config",
132 }),90 }),
133 });91 ),
92 );
134 }),93 }),
135 ),94 ),
136 95
@@ -168,13 +127,13 @@ const resolveAuth = ( · 31 unmodified lines
168 });127 });
169 }128 }
170 return ctx.secrets.get(auth.tokenSecretId).pipe(129 return ctx.secrets.get(auth.tokenSecretId).pipe(
171 Effect.mapError((err) =>130 Effect.catchTag("SecretOwnedByConnectionError", () =>
172 "_tag" in err && err._tag === "SecretOwnedByConnectionError"131 Effect.fail(
173 ? new OnePasswordError({132 new OnePasswordError({
174 operation: "auth resolution",133 operation: "auth resolution",
175 message: `Service account token secret "${auth.tokenSecretId}" not found`,134 message: `Service account token secret "${auth.tokenSecretId}" not found`,
176 })135 }),
177 : err,136 ),
178 ),137 ),
179 Effect.flatMap((token) => {138 Effect.flatMap((token) => {
180 if (token === null) {139 if (token === null) {
@@ -200,9 +159,7 @@ const getServiceFromConfig = ( · 19 unmodified lines
200 preferSdk: boolean | undefined,159 preferSdk: boolean | undefined,
201): Effect.Effect<OnePasswordService, OnePasswordError | StorageFailure> =>160): Effect.Effect<OnePasswordService, OnePasswordError | StorageFailure> =>
202 resolveAuth(config.auth, ctx).pipe(161 resolveAuth(config.auth, ctx).pipe(
203 Effect.flatMap((resolved) =>162 Effect.flatMap((resolved) => makeOnePasswordService(resolved, { timeoutMs, preferSdk })),
204 makeOnePasswordService(resolved, { timeoutMs, preferSdk }),
205 ),
206 );163 );
207 164
208// ---------------------------------------------------------------------------165// ---------------------------------------------------------------------------
@@ -241,10 +198,7 @@ const makeProvider = ( · 32 unmodified lines
241 list: () =>198 list: () =>
242 ctx.storage.getConfig().pipe(199 ctx.storage.getConfig().pipe(
243 Effect.flatMap((config) => {200 Effect.flatMap((config) => {
244 if (!config)201 if (!config) return Effect.succeed([] as ReadonlyArray<{ id: string; name: string }>);
245 return Effect.succeed(
246 [] as ReadonlyArray<{ id: string; name: string }>,
247 );
248 return getServiceFromConfig(config, ctx, timeoutMs, preferSdk).pipe(202 return getServiceFromConfig(config, ctx, timeoutMs, preferSdk).pipe(
249 Effect.flatMap((svc) => svc.listItems(config.vaultId)),203 Effect.flatMap((svc) => svc.listItems(config.vaultId)),
250 Effect.map(204 Effect.map(
@@ -253,12 +207,70 @@ const makeProvider = ( · 2 unmodified lines
253 ),207 ),
254 );208 );
255 }),209 }),
256 Effect.orElseSucceed(210 Effect.orElseSucceed(() => [] as ReadonlyArray<{ id: string; name: string }>),
257 () => [] as ReadonlyArray<{ id: string; name: string }>,
258 ),
259 ),211 ),
260});212});
261 213
214const 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
272export type OnePasswordExtension = ReturnType<typeof makeOnePasswordExtension>;
273
262// ---------------------------------------------------------------------------274// ---------------------------------------------------------------------------
263// Plugin factory275// Plugin factory
264// ---------------------------------------------------------------------------276// ---------------------------------------------------------------------------
@@ -270,88 +282,21 @@ export interface OnePasswordPluginOptions { · 5 unmodified lines
270 readonly preferSdk?: boolean;282 readonly preferSdk?: boolean;
271}283}
272 284
273export const onepasswordPlugin = definePlugin(285export const onepasswordPlugin = definePlugin((options?: OnePasswordPluginOptions) => {
274 (options?: OnePasswordPluginOptions) => {286 const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
275 const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;287 const preferSdk = options?.preferSdk;
276 const preferSdk = options?.preferSdk;288
277 289 return {
278 return {290 id: "onepassword" as const,
279 id: "onepassword" as const,291 packageName: "@executor-js/plugin-onepassword",
280 packageName: "@executor-js/plugin-onepassword",292 storage: ({ blobs, scopes }) => makeOnePasswordStore(blobs, scopes.at(-1)!.id),
281 storage: ({ blobs, scopes }) =>293
282 makeOnePasswordStore(blobs, scopes.at(-1)!.id as string),294 extension: (ctx) => makeOnePasswordExtension(ctx, timeoutMs, preferSdk),
283 295
284 extension: (ctx) => {296 secretProviders: (ctx) => [makeProvider(ctx, timeoutMs, preferSdk)],
285 return {297
286 configure: (config) => ctx.storage.saveConfig(config),298 routes: () => OnePasswordGroup,
287 299 handlers: () => OnePasswordHandlers,
288 getConfig: () => ctx.storage.getConfig(),300 extensionService: OnePasswordExtensionService,
289 301 };
290 removeConfig: () => ctx.storage.deleteConfig(),302});
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);
packages/plugins/onepassword/src/sdk/service.ts+9−11
@@ -53,10 +53,10 @@ type OnePasswordSdkModule = typeof import("@1password/sdk"); · 52 unmodified lines
53const loadOnePasswordSdk = (): Effect.Effect<OnePasswordSdkModule, OnePasswordError> =>53const loadOnePasswordSdk = (): Effect.Effect<OnePasswordSdkModule, OnePasswordError> =>
54 Effect.tryPromise({54 Effect.tryPromise({
55 try: () => import("@1password/sdk"),55 try: () => import("@1password/sdk"),
56 catch: (cause) =>56 catch: () =>
57 new OnePasswordError({57 new OnePasswordError({
58 operation: "sdk module load",58 operation: "sdk module load",
59 message: cause instanceof Error ? cause.message : String(cause),59 message: "Failed to load 1Password SDK",
60 }),60 }),
61 });61 });
62 62
@@ -99,22 +99,20 @@ export const makeNativeSdkService = ( · 36 unmodified lines
99 integrationName: "Executor",99 integrationName: "Executor",
100 integrationVersion: "0.0.0",100 integrationVersion: "0.0.0",
101 }),101 }),
102 catch: (cause) =>102 catch: () =>
103 new OnePasswordError({103 new OnePasswordError({
104 operation: "client setup",104 operation: "client setup",
105 message: cause instanceof Error ? cause.message : String(cause),105 message: "Failed to set up 1Password client",
106 }),106 }),
107 }).pipe(107 }).pipe(timeoutWithOnePasswordError("client setup", timeoutMs));
108 timeoutWithOnePasswordError("client setup", timeoutMs),
109 );
110 108
111 const wrap = <A>(fn: () => Promise<A>, operation: string): Effect.Effect<A, OnePasswordError> =>109 const wrap = <A>(fn: () => Promise<A>, operation: string): Effect.Effect<A, OnePasswordError> =>
112 Effect.tryPromise({110 Effect.tryPromise({
113 try: fn,111 try: fn,
114 catch: (cause) =>112 catch: () =>
115 new OnePasswordError({113 new OnePasswordError({
116 operation,114 operation,
117 message: cause instanceof Error ? cause.message : String(cause),115 message: `1Password SDK ${operation} failed`,
118 }),116 }),
119 }).pipe(117 }).pipe(
120 timeoutWithOnePasswordError(operation, timeoutMs),118 timeoutWithOnePasswordError(operation, timeoutMs),
@@ -154,10 +152,10 @@ export const makeCliService = ( · 33 unmodified lines
154 const wrapSync = <A>(fn: () => A, operation: string): Effect.Effect<A, OnePasswordError> =>152 const wrapSync = <A>(fn: () => A, operation: string): Effect.Effect<A, OnePasswordError> =>
155 Effect.try({153 Effect.try({
156 try: fn,154 try: fn,
157 catch: (cause) =>155 catch: () =>
158 new OnePasswordError({156 new OnePasswordError({
159 operation,157 operation,
160 message: cause instanceof Error ? cause.message : String(cause),158 message: `1Password CLI ${operation} failed`,
161 }),159 }),
162 }).pipe(Effect.withSpan(`onepassword.cli.${operation}`));160 }).pipe(Effect.withSpan(`onepassword.cli.${operation}`));
163 161