every-plugin 2.7.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/build/rspack/dev-server-middleware.cjs +4 -2
  2. package/dist/build/rspack/dev-server-middleware.cjs.map +1 -1
  3. package/dist/build/rspack/dev-server-middleware.mjs +4 -2
  4. package/dist/build/rspack/dev-server-middleware.mjs.map +1 -1
  5. package/dist/plugin.cjs +2 -1
  6. package/dist/plugin.cjs.map +1 -1
  7. package/dist/plugin.d.cts.map +1 -1
  8. package/dist/plugin.d.mts.map +1 -1
  9. package/dist/plugin.mjs +2 -1
  10. package/dist/plugin.mjs.map +1 -1
  11. package/dist/runtime/errors.cjs +3 -42
  12. package/dist/runtime/errors.cjs.map +1 -1
  13. package/dist/runtime/errors.d.cts +1 -1
  14. package/dist/runtime/errors.d.cts.map +1 -1
  15. package/dist/runtime/errors.d.mts +1 -1
  16. package/dist/runtime/errors.d.mts.map +1 -1
  17. package/dist/runtime/errors.mjs +3 -42
  18. package/dist/runtime/errors.mjs.map +1 -1
  19. package/dist/runtime/index.cjs +4 -4
  20. package/dist/runtime/index.cjs.map +1 -1
  21. package/dist/runtime/index.mjs +4 -4
  22. package/dist/runtime/index.mjs.map +1 -1
  23. package/dist/runtime/mf-config.cjs +1 -1
  24. package/dist/runtime/mf-config.mjs +1 -1
  25. package/dist/runtime/services/module-federation.service.cjs +6 -5
  26. package/dist/runtime/services/module-federation.service.cjs.map +1 -1
  27. package/dist/runtime/services/module-federation.service.mjs +6 -5
  28. package/dist/runtime/services/module-federation.service.mjs.map +1 -1
  29. package/dist/runtime/services/plugin-loader.service.cjs +4 -4
  30. package/dist/runtime/services/plugin-loader.service.cjs.map +1 -1
  31. package/dist/runtime/services/plugin-loader.service.d.cts.map +1 -1
  32. package/dist/runtime/services/plugin-loader.service.d.mts.map +1 -1
  33. package/dist/runtime/services/plugin-loader.service.mjs +4 -4
  34. package/dist/runtime/services/plugin-loader.service.mjs.map +1 -1
  35. package/package.json +2 -2
  36. package/skills/plugin-client/SKILL.md +450 -0
  37. package/skills/plugin-client/references/api-keys.md +145 -0
  38. package/src/build/rspack/dev-server-middleware.ts +4 -2
  39. package/src/plugin.ts +2 -1
  40. package/src/runtime/errors.ts +4 -60
  41. package/src/runtime/index.ts +4 -4
  42. package/src/runtime/services/module-federation.service.ts +7 -5
  43. package/src/runtime/services/plugin-loader.service.ts +17 -7
@@ -0,0 +1,145 @@
1
+ # API Keys Reference
2
+
3
+ ## Route Table
4
+
5
+ All routes are exposed via the Better-Auth `apiKeyClient()` plugin (not the oRPC `apiClient`). Call them through `authClient.apiKey.*`:
6
+
7
+ | Method | Input | Output | Notes |
8
+ |--------|-------|--------|-------|
9
+ | `list` | `{ organizationId?, limit?, offset?, sortBy?, sortDirection?, configId? }` | `ApiKey[]` | Returns metadata only — no secret |
10
+ | `create` | `{ name?, prefix?, expiresIn?, permissions?, metadata?, rateLimit?, organizationId?, configId? }` | `ApiKey & { key: string }` | Full secret returned **once** |
11
+ | `update` | `{ id, name?, enabled?, permissions?, metadata?, expiresIn?, rateLimit? }` | `ApiKey` | No secret returned |
12
+ | `delete` | `{ id }` | `{ success: boolean }` | Irreversible |
13
+
14
+ ### UI Usage
15
+
16
+ ```ts
17
+ const authClient = useAuthClient();
18
+
19
+ // List keys for an organization
20
+ const { data } = await authClient.apiKey.list({
21
+ query: { configId: "org-keys", organizationId: orgId },
22
+ });
23
+ const keys = data?.apiKeys ?? [];
24
+
25
+ // Create a key
26
+ const { data, error } = await authClient.apiKey.create({
27
+ configId: "org-keys",
28
+ organizationId: orgId,
29
+ name: "CI deploy key",
30
+ expiresIn: 60 * 60 * 24 * 30, // 30 days
31
+ });
32
+ if (data) {
33
+ saveSecret(data.apiKey.key); // "edk_xxxxx..." — only available here
34
+ }
35
+
36
+ // Delete a key
37
+ const { error } = await authClient.apiKey.delete({
38
+ keyId: "key_abc123",
39
+ configId: "org-keys",
40
+ });
41
+ ```
42
+
43
+ ## ApiKey Shape
44
+
45
+ ```ts
46
+ type ApiKey = {
47
+ id: string;
48
+ name: string | null;
49
+ prefix: string; // first chars for identification (e.g., "edk_abc")
50
+ enabled: boolean;
51
+ permissions: Record<string, string[]> | null; // resource → actions
52
+ requestCount: number;
53
+ expiresAt: number | null; // epoch seconds
54
+ metadata: Record<string, unknown> | null;
55
+ rateLimit: { limit: number; window: number } | null;
56
+ };
57
+ ```
58
+
59
+ ## Permission Schema
60
+
61
+ Permissions are a map of resource name to allowed action arrays:
62
+
63
+ ```ts
64
+ {
65
+ things: ["create", "read", "delete"],
66
+ projects: ["read"],
67
+ registry: ["publish", "relay"],
68
+ }
69
+ ```
70
+
71
+ ### Server-Side Permission Checks
72
+
73
+ `requireApiKey(requiredPermissions)` validates that the presented key's `permissions` cover all required entries:
74
+
75
+ ```ts
76
+ const { requireApiKey } = createAuthMiddleware(builder);
77
+
78
+ builder.deleteThing
79
+ .use(requireApiKey({ things: ["delete"] }))
80
+ .handler(async ({ context }) => {
81
+ context.apiKey; // ApiKeyContext — non-null, narrowed by middleware
82
+ context.apiKey.permissions; // Record<string, string[]>
83
+ });
84
+ ```
85
+
86
+ If any required action is missing, throws `FORBIDDEN` with:
87
+
88
+ ```ts
89
+ {
90
+ message: "API key lacks permission: things:delete",
91
+ data: { requiredPermissions, keyPermissions },
92
+ }
93
+ ```
94
+
95
+ ### Multi-Resource Permissions
96
+
97
+ ```ts
98
+ builder.deployApp
99
+ .use(requireApiKey({
100
+ registry: ["publish"],
101
+ projects: ["update"],
102
+ }))
103
+ .handler(...)
104
+ ```
105
+
106
+ All specified resources must be satisfied — partial matches are rejected.
107
+
108
+ ## Key Lifecycle
109
+
110
+ 1. **Create** — `authClient.apiKey.create()` returns `{ ...apiKey, key }`. The `key` field is the full secret (`edk_...`). Save it now — it is never returned again.
111
+ 2. **Identify** — subsequent `list` / `update` calls return only `prefix` (first few chars) for identification.
112
+ 3. **Disable** — `update({ id, enabled: false })` revokes the key without deleting it.
113
+ 4. **Delete** — `delete({ id })` permanently removes the key.
114
+
115
+ ## Scoping
116
+
117
+ ### `configId`
118
+
119
+ Keys can be grouped under a config ID (e.g., `"org-keys"`). This is an opaque label for organizing keys by purpose. Pass the same `configId` on create, list, and delete.
120
+
121
+ ### `organizationId`
122
+
123
+ Keys scoped to an organization inherit the org's context. The host's session middleware resolves the active organization from the session, and `requireOrganization` / `requireOrgRole` middlewares check org membership.
124
+
125
+ ## Browser vs Server Limitations
126
+
127
+ Permissions, rate limits, and refill configuration are **server-only** — they cannot be set from the browser client. The `create` call from the UI accepts `name`, `expiresIn`, `permissions` (if allowed), and `metadata`. Server-side key management (e.g., via a plugin route) can set the full set of options.
128
+
129
+ ## Auth Context
130
+
131
+ When an API key is validated, the host's `getContext()` populates:
132
+
133
+ ```ts
134
+ {
135
+ authMethod: "apiKey",
136
+ apiKey: {
137
+ id: "key_abc123",
138
+ name: "CI deploy key",
139
+ permissions: { things: ["create", "read"], ... },
140
+ },
141
+ // user, organization, etc. are null unless the key is tied to a user/org
142
+ }
143
+ ```
144
+
145
+ The `authMethod` field is one of `"session" | "anonymous" | "apiKey" | "none"`. Plugin routes can branch on this to customize responses for programmatic vs interactive access.
@@ -74,7 +74,8 @@ export function setupPluginMiddleware(
74
74
  handlers.rpc = new RPCHandler(loaded.router, {
75
75
  interceptors: [
76
76
  onError((error: any) => {
77
- formatORPCError(error);
77
+ const formatted = formatORPCError(error);
78
+ if (formatted) console.error(formatted);
78
79
  }),
79
80
  ],
80
81
  });
@@ -87,7 +88,8 @@ export function setupPluginMiddleware(
87
88
  ],
88
89
  interceptors: [
89
90
  onError((error: any) => {
90
- formatORPCError(error);
91
+ const formatted = formatORPCError(error);
92
+ if (formatted) console.error(formatted);
91
93
  }),
92
94
  ],
93
95
  });
package/src/plugin.ts CHANGED
@@ -173,7 +173,8 @@ export const createPlugin: CreatePluginFn = function createPlugin<
173
173
  throw unwrapped;
174
174
  }
175
175
 
176
- formatORPCError(error);
176
+ const formatted = formatORPCError(error);
177
+ if (formatted) console.error(formatted);
177
178
  throw error;
178
179
  }) as any;
179
180
 
@@ -116,15 +116,14 @@ const formatORPCValidationError = (error: any): string[] | null => {
116
116
  return lines;
117
117
  };
118
118
 
119
- export const formatORPCError = (error: any): void => {
119
+ export const formatORPCError = (error: any): string | null => {
120
120
  if (!(error instanceof ORPCError)) {
121
- return;
121
+ return null;
122
122
  }
123
123
 
124
124
  const validationLines = formatORPCValidationError(error);
125
125
  if (validationLines) {
126
- console.error(validationLines.join("\n"));
127
- return;
126
+ return validationLines.join("\n");
128
127
  }
129
128
 
130
129
  const lines: string[] = [];
@@ -182,49 +181,7 @@ export const formatORPCError = (error: any): void => {
182
181
  }
183
182
 
184
183
  lines.push(`╰${"─".repeat(50)}\n`);
185
- console.error(lines.join("\n"));
186
- };
187
-
188
- const formatPluginError = (
189
- pluginId: string | undefined,
190
- operation: string | undefined,
191
- message: string,
192
- ): void => {
193
- const lines: string[] = [];
194
-
195
- lines.push(`\n╭─ Plugin Error ${"─".repeat(40)}`);
196
- if (pluginId) lines.push(`│ Plugin: ${pluginId}`);
197
- if (operation) lines.push(`│ During: ${operation}`);
198
- lines.push(`│`);
199
-
200
- if (message.includes("ECONNREFUSED")) {
201
- lines.push(`│ ❌ Connection refused`);
202
- lines.push(`│ `);
203
- lines.push(`│ A required service is not running.`);
204
- lines.push(`│ → Run: docker compose up -d`);
205
- } else if (message.includes("ENOTFOUND")) {
206
- lines.push(`│ ❌ Host not found`);
207
- lines.push(`│ `);
208
- lines.push(`│ Check your connection URL or network settings.`);
209
- } else if (message.includes("ETIMEDOUT") || message.includes("timeout")) {
210
- lines.push(`│ ❌ Connection timeout`);
211
- lines.push(`│ `);
212
- lines.push(`│ The service took too long to respond.`);
213
- } else if (message.includes("EACCES") || message.includes("permission")) {
214
- lines.push(`│ ❌ Permission denied`);
215
- lines.push(`│ `);
216
- lines.push(`│ Check credentials or access permissions.`);
217
- } else if (message.includes("401") || message.includes("unauthorized")) {
218
- lines.push(`│ ❌ Authentication failed`);
219
- lines.push(`│ `);
220
- lines.push(`│ Check your API key or credentials.`);
221
- } else {
222
- lines.push(`│ ❌ ${message}`);
223
- }
224
-
225
- lines.push(`╰${"─".repeat(50)}\n`);
226
-
227
- console.error(lines.join("\n"));
184
+ return lines.join("\n");
228
185
  };
229
186
 
230
187
  const isRetryableError = (message: string): boolean => {
@@ -253,11 +210,6 @@ export const wrapORPCError = (
253
210
  procedureName?: string,
254
211
  operation?: string,
255
212
  ): PluginRuntimeError => {
256
- const validationLines = formatORPCValidationError(orpcError);
257
- if (validationLines) {
258
- console.error(validationLines.join("\n"));
259
- }
260
-
261
213
  return new PluginRuntimeError({
262
214
  pluginId,
263
215
  operation,
@@ -314,14 +266,6 @@ export const toPluginRuntimeError = (
314
266
  return error;
315
267
  }
316
268
 
317
- const validationLines = formatORPCValidationError(error);
318
- if (validationLines) {
319
- console.error(validationLines.join("\n"));
320
- } else {
321
- const message = extractErrorMessage(error);
322
- formatPluginError(pluginId, operation, message);
323
- }
324
-
325
269
  return new PluginRuntimeError({
326
270
  pluginId,
327
271
  operation,
@@ -84,7 +84,7 @@ export class PluginRuntime<R = RegisteredPlugins> {
84
84
  const initialized = yield* pluginService.initializePlugin(instance, config, plugins);
85
85
 
86
86
  return initialized;
87
- }).pipe(Effect.provide(this.runtime));
87
+ }).pipe(Effect.annotateLogs({ plugin: pluginId }), Effect.provide(this.runtime));
88
88
 
89
89
  cachedPlugin = Effect.cached(operation).pipe(Effect.flatten);
90
90
  this.pluginCache.set(cacheKey, cachedPlugin);
@@ -112,7 +112,7 @@ export class PluginRuntime<R = RegisteredPlugins> {
112
112
  const effect = Effect.gen(function* () {
113
113
  const pluginService = yield* PluginService;
114
114
  return yield* pluginService.loadPlugin(pluginId);
115
- });
115
+ }).pipe(Effect.annotateLogs({ plugin: pluginId }));
116
116
  return this.runPromise(effect) as Promise<LoadedPlugin<RegisteredPlugin<K, R>>>;
117
117
  }
118
118
 
@@ -123,7 +123,7 @@ export class PluginRuntime<R = RegisteredPlugins> {
123
123
  const effect = Effect.gen(function* () {
124
124
  const pluginService = yield* PluginService;
125
125
  return yield* pluginService.instantiatePlugin(pluginId, loadedPlugin);
126
- });
126
+ }).pipe(Effect.annotateLogs({ plugin: pluginId }));
127
127
  return this.runPromise(effect) as Promise<PluginInstance<RegisteredPlugin<K, R>>>;
128
128
  }
129
129
 
@@ -135,7 +135,7 @@ export class PluginRuntime<R = RegisteredPlugins> {
135
135
  const effect = Effect.gen(function* () {
136
136
  const pluginService = yield* PluginService;
137
137
  return yield* pluginService.initializePlugin(instance, config, plugins);
138
- });
138
+ }).pipe(Effect.annotateLogs({ plugin: instance.plugin.id }));
139
139
  return this.runPromise(effect);
140
140
  }
141
141
 
@@ -86,7 +86,7 @@ export class ModuleFederationService extends Effect.Service<ModuleFederationServ
86
86
  return {
87
87
  registerRemote: (pluginId: string, url: string) =>
88
88
  Effect.gen(function* () {
89
- console.log(`[MF] Registering ${pluginId}`);
89
+ yield* Effect.logDebug(`[MF] Registering ${pluginId}`);
90
90
 
91
91
  const remoteName = getNormalizedRemoteName(pluginId);
92
92
  const type = url.endsWith("/mf-manifest.json")
@@ -112,16 +112,16 @@ export class ModuleFederationService extends Effect.Service<ModuleFederationServ
112
112
  }),
113
113
  });
114
114
 
115
- console.log(`[MF] ✅ Registered ${pluginId}`);
115
+ yield* Effect.logInfo(`[MF] ✅ Registered ${pluginId}`);
116
116
  }),
117
117
 
118
118
  loadRemoteConstructor: (pluginId: string, url: string) =>
119
119
  Effect.gen(function* () {
120
120
  const remoteName = getNormalizedRemoteName(pluginId);
121
- console.log(`[MF] Loading remote ${remoteName}`);
121
+ yield* Effect.logDebug(`[MF] Loading remote ${remoteName}`);
122
122
  const modulePath = `${remoteName}/plugin`;
123
123
 
124
- return yield* Effect.tryPromise({
124
+ const pluginConstructor = yield* Effect.tryPromise({
125
125
  try: async () => {
126
126
  const container = await mf.loadRemote<RemoteModule>(modulePath);
127
127
  if (!container) {
@@ -181,7 +181,6 @@ export class ModuleFederationService extends Effect.Service<ModuleFederationServ
181
181
  );
182
182
  }
183
183
 
184
- console.log(`[MF] ✅ Loaded constructor for ${pluginId}`);
185
184
  return Constructor;
186
185
  },
187
186
  catch: (error): ModuleFederationError =>
@@ -191,6 +190,9 @@ export class ModuleFederationService extends Effect.Service<ModuleFederationServ
191
190
  cause: error instanceof Error ? error : new Error(String(error)),
192
191
  }),
193
192
  });
193
+
194
+ yield* Effect.logInfo(`[MF] ✅ Loaded constructor for ${pluginId}`);
195
+ return pluginConstructor;
194
196
  }),
195
197
  };
196
198
  }),
@@ -111,19 +111,23 @@ export class PluginLoaderService extends Effect.Service<PluginLoaderService>()(
111
111
 
112
112
  const resolvedUrl = resolveUrl(url);
113
113
 
114
- yield* moduleFederationService
115
- .registerRemote(pluginId, resolvedUrl)
116
- .pipe(
117
- Effect.mapError((error) =>
118
- toPluginRuntimeError(error, pluginId, undefined, "register-remote", true),
119
- ),
120
- );
114
+ yield* moduleFederationService.registerRemote(pluginId, resolvedUrl).pipe(
115
+ Effect.tapError((_error) =>
116
+ Effect.logError(`Plugin ${pluginId} failed during register-remote`),
117
+ ),
118
+ Effect.mapError((error) =>
119
+ toPluginRuntimeError(error, pluginId, undefined, "register-remote", true),
120
+ ),
121
+ );
121
122
 
122
123
  yield* Effect.logDebug("Loading plugin from remote", { pluginId, url: resolvedUrl });
123
124
 
124
125
  const ctor = yield* moduleFederationService
125
126
  .loadRemoteConstructor(pluginId, resolvedUrl)
126
127
  .pipe(
128
+ Effect.tapError((_error) =>
129
+ Effect.logError(`Plugin ${pluginId} failed during load-remote`),
130
+ ),
127
131
  Effect.mapError((error) =>
128
132
  toPluginRuntimeError(error, pluginId, undefined, "load-remote", false),
129
133
  ),
@@ -138,6 +142,9 @@ export class PluginLoaderService extends Effect.Service<PluginLoaderService>()(
138
142
  instantiatePlugin: <T extends AnyPlugin>(pluginId: string, loadedPlugin: LoadedPlugin<T>) =>
139
143
  Effect.gen(function* () {
140
144
  const instance = yield* Effect.try(() => new loadedPlugin.ctor()).pipe(
145
+ Effect.tapError((_error) =>
146
+ Effect.logError(`Plugin ${pluginId} failed during instantiate-plugin`),
147
+ ),
141
148
  Effect.mapError((error) =>
142
149
  toPluginRuntimeError(error, pluginId, undefined, "instantiate-plugin", false),
143
150
  ),
@@ -235,6 +242,9 @@ export class PluginLoaderService extends Effect.Service<PluginLoaderService>()(
235
242
  .initialize({ variables: _variables, secrets: hydratedConfig.secrets }, plugins ?? {})
236
243
  .pipe(
237
244
  Effect.provideService(Scope.Scope, scope),
245
+ Effect.tapError((_error) =>
246
+ Effect.logError(`Plugin ${plugin.id} failed during initialize-plugin`),
247
+ ),
238
248
  Effect.mapError((error) =>
239
249
  toPluginRuntimeError(error, plugin.id, undefined, "initialize-plugin", false),
240
250
  ),