every-plugin 3.0.0-rc.0 → 3.0.0-rc.3
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.
- package/dist/errors.cjs +1 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +2 -2
- package/dist/errors.d.mts +2 -2
- package/dist/errors.mjs +2 -2
- package/dist/errors.mjs.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +16 -2
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +16 -2
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/runtime/errors.cjs +20 -0
- package/dist/runtime/errors.cjs.map +1 -1
- package/dist/runtime/errors.d.cts +23 -1
- package/dist/runtime/errors.d.cts.map +1 -1
- package/dist/runtime/errors.d.mts +23 -1
- package/dist/runtime/errors.d.mts.map +1 -1
- package/dist/runtime/errors.mjs +20 -1
- package/dist/runtime/errors.mjs.map +1 -1
- package/dist/runtime/mf-config.cjs +1 -1
- package/dist/runtime/mf-config.mjs +1 -1
- package/dist/runtime/services/module-federation.service.cjs +1 -7
- package/dist/runtime/services/module-federation.service.cjs.map +1 -1
- package/dist/runtime/services/module-federation.service.mjs +1 -7
- package/dist/runtime/services/module-federation.service.mjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.cjs +26 -1
- package/dist/runtime/services/plugin-loader.service.cjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.mjs +26 -1
- package/dist/runtime/services/plugin-loader.service.mjs.map +1 -1
- package/dist/runtime/services/shared-identity.cjs +1 -1
- package/dist/runtime/services/shared-identity.cjs.map +1 -1
- package/dist/runtime/services/shared-identity.mjs +1 -1
- package/dist/runtime/services/shared-identity.mjs.map +1 -1
- package/dist/types.d.cts +3 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +3 -0
- package/dist/types.d.mts.map +1 -1
- package/dist/version-manifest.cjs +1 -1
- package/dist/version-manifest.cjs.map +1 -1
- package/dist/version-manifest.mjs +1 -1
- package/dist/version-manifest.mjs.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +2 -1
- package/src/plugin.ts +20 -1
- package/src/runtime/errors.ts +44 -0
- package/src/runtime/services/module-federation.service.ts +4 -7
- package/src/runtime/services/plugin-loader.service.ts +32 -0
- package/src/runtime/services/shared-identity.ts +1 -1
- package/src/types.ts +3 -0
- package/src/version-manifest.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-loader.service.mjs","names":[],"sources":["../../../src/runtime/services/plugin-loader.service.ts"],"sourcesContent":["import type { InferSchemaInput, InferSchemaOutput } from \"@orpc/contract\";\nimport { Context, Effect, Exit, Layer, Scope } from \"effect\";\nimport type { z } from \"zod\";\nimport { PluginIdTag } from \"../../plugin\";\nimport type {\n AnyPlugin,\n AnyPluginConstructor,\n InitializedPlugin,\n LoadedPlugin,\n PluginInstance,\n PluginMetadata,\n PluginRegistry,\n} from \"../../types\";\nimport { PluginRuntimeError, toPluginRuntimeError } from \"../errors\";\nimport { validate } from \"../validation\";\nimport { ModuleFederationService } from \"./module-federation.service\";\nimport { SecretsService } from \"./secrets.service\";\n\nexport class PluginRegistryTag extends Context.Service<PluginRegistryTag, PluginRegistry>()(\n \"PluginRegistry\",\n) {}\n\nexport class PluginMapTag extends Context.Service<\n PluginMapTag,\n Record<string, AnyPluginConstructor>\n>()(\"PluginMap\") {}\n\nexport interface RegistryServiceShape {\n get: (\n pluginId: string,\n ) => Effect.Effect<\n { constructor: (new () => AnyPlugin) | null; metadata: PluginMetadata },\n PluginRuntimeError\n >;\n getModule: (pluginId: string) => Effect.Effect<AnyPluginConstructor | null>;\n}\n\nexport class RegistryService extends Context.Service<RegistryService, RegistryServiceShape>()(\n \"RegistryService\",\n) {}\n\nexport const RegistryServiceDefault = Layer.effect(\n RegistryService,\n Effect.gen(function* () {\n const registry = yield* PluginRegistryTag;\n const pluginMap = yield* PluginMapTag;\n\n return {\n get: (pluginId: string) =>\n Effect.gen(function* () {\n const entry = registry[pluginId];\n\n if (!entry) {\n return yield* new PluginRuntimeError({\n pluginId,\n operation: \"validate-plugin-id\",\n cause: new Error(`Plugin ${pluginId} not found in registry`),\n });\n }\n\n if (\"module\" in entry) {\n return {\n constructor: entry.module,\n metadata: {\n remoteUrl: entry.remote || \"\",\n version: entry.version,\n description: entry.description,\n } as PluginMetadata,\n };\n }\n\n return {\n constructor: null,\n metadata: {\n remoteUrl: entry.remote,\n version: entry.version,\n description: entry.description,\n } as PluginMetadata,\n };\n }),\n\n getModule: (pluginId: string) => Effect.succeed(pluginMap[pluginId] || null),\n };\n }),\n);\n\nexport interface PluginLoaderServiceShape {\n loadPlugin: (pluginId: string) => Effect.Effect<LoadedPlugin, PluginRuntimeError>;\n instantiatePlugin: <T extends AnyPlugin>(\n pluginId: string,\n loadedPlugin: LoadedPlugin<T>,\n ) => Effect.Effect<PluginInstance<T>, PluginRuntimeError>;\n initializePlugin: <T extends AnyPlugin>(\n pluginInstance: PluginInstance<T>,\n config: {\n variables: InferSchemaInput<T[\"configSchema\"][\"variables\"]>;\n secrets: InferSchemaInput<T[\"configSchema\"][\"secrets\"]>;\n },\n plugins?: Record<string, unknown>,\n ) => Effect.Effect<InitializedPlugin<T>, PluginRuntimeError>;\n}\n\nexport class PluginLoaderService extends Context.Service<\n PluginLoaderService,\n PluginLoaderServiceShape\n>()(\"PluginLoaderService\") {}\n\nexport const PluginLoaderServiceDefault = Layer.effect(\n PluginLoaderService,\n Effect.gen(function* () {\n const moduleFederationService = yield* ModuleFederationService;\n const secretsService = yield* SecretsService;\n const registryService = yield* RegistryService;\n\n const resolveUrl = (baseUrl: string, version?: string): string =>\n version && version !== \"latest\" ? baseUrl.replace(\"@latest\", `@${version}`) : baseUrl;\n\n return {\n loadPlugin: (pluginId: string) =>\n Effect.gen(function* () {\n const entry = yield* registryService.get(pluginId);\n\n if (entry.constructor) {\n yield* Effect.logDebug(\"Loading plugin from direct module\", { pluginId });\n\n return {\n ctor: entry.constructor,\n metadata: entry.metadata,\n } satisfies LoadedPlugin;\n }\n\n const url = entry.metadata.remoteUrl;\n if (!url) {\n return yield* new PluginRuntimeError({\n pluginId,\n operation: \"load-plugin\",\n cause: new Error(`Plugin ${pluginId} has no module or remote URL configured`),\n });\n }\n\n const resolvedUrl = resolveUrl(url);\n\n yield* moduleFederationService.registerRemote(pluginId, resolvedUrl).pipe(\n Effect.tapError((error) =>\n Effect.logError(`Plugin ${pluginId} failed during register-remote: ${String(error)}`),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, pluginId, undefined, \"register-remote\"),\n ),\n );\n\n yield* Effect.logDebug(\"Loading plugin from remote\", { pluginId, url: resolvedUrl });\n\n const ctor = yield* moduleFederationService\n .loadRemoteConstructor(pluginId, resolvedUrl)\n .pipe(\n Effect.tapError((error) =>\n Effect.logError(`Plugin ${pluginId} failed during load-remote: ${String(error)}`),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, pluginId, undefined, \"load-remote\"),\n ),\n );\n\n return {\n ctor,\n metadata: entry.metadata,\n } satisfies LoadedPlugin;\n }),\n\n instantiatePlugin: <T extends AnyPlugin>(pluginId: string, loadedPlugin: LoadedPlugin<T>) =>\n Effect.gen(function* () {\n const instance = yield* Effect.try(() => new loadedPlugin.ctor()).pipe(\n Effect.tapError((error) =>\n Effect.logError(`Plugin ${pluginId} failed during instantiate-plugin: ${error}`),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, pluginId, undefined, \"instantiate-plugin\"),\n ),\n );\n\n (instance.id as string) = pluginId;\n\n return {\n plugin: instance,\n metadata: loadedPlugin.metadata,\n } satisfies PluginInstance<T>;\n }),\n\n initializePlugin: <T extends AnyPlugin>(\n pluginInstance: PluginInstance<T>,\n config: {\n variables: InferSchemaInput<T[\"configSchema\"][\"variables\"]>;\n secrets: InferSchemaInput<T[\"configSchema\"][\"secrets\"]>;\n },\n plugins?: Record<string, unknown>,\n ) =>\n Effect.gen(function* () {\n const { plugin } = pluginInstance;\n\n // Validate and hydrate config\n const validatedVariables = yield* validate(\n plugin.configSchema.variables as z.ZodSchema<\n InferSchemaOutput<T[\"configSchema\"][\"variables\"]>\n >,\n config.variables,\n plugin.id,\n \"config\",\n ).pipe(\n Effect.mapError(\n (validationError) =>\n new PluginRuntimeError({\n pluginId: plugin.id,\n operation: \"validate-config\",\n cause: validationError.zodError,\n }),\n ),\n );\n\n // Validate secrets\n const validatedSecrets = yield* validate(\n plugin.configSchema.secrets as z.ZodSchema<\n InferSchemaOutput<T[\"configSchema\"][\"secrets\"]>\n >,\n config.secrets,\n plugin.id,\n \"config\",\n ).pipe(\n Effect.mapError(\n (validationError) =>\n new PluginRuntimeError({\n pluginId: plugin.id,\n operation: \"validate-secrets\",\n cause: validationError.zodError,\n }),\n ),\n );\n\n // Hydrate secrets in variables\n const hydratedConfig = yield* secretsService.hydrateSecrets({\n variables: validatedVariables,\n secrets: validatedSecrets,\n });\n\n const _variables = yield* validate(\n plugin.configSchema.variables as z.ZodSchema<\n InferSchemaOutput<T[\"configSchema\"][\"variables\"]>\n >,\n hydratedConfig.variables,\n plugin.id,\n \"config\",\n ).pipe(\n Effect.mapError(\n (validationError) =>\n new PluginRuntimeError({\n pluginId: plugin.id,\n operation: \"validate-hydrated-config\",\n cause: validationError.zodError,\n }),\n ),\n );\n\n // Create a long-lived scope for this plugin instance. Initialize\n // returns a Layer which is built against this scope, so scoped\n // resources (DB pools, caches) release on plugin shutdown.\n const scope = yield* Scope.make();\n\n // Build the plugin's Layer within the scope.\n // On failure, close the scope immediately so scoped resources\n // (DB pools, caches) are released — otherwise every failed\n // initialization leaks until the process exits.\n const effectContext = yield* Effect.gen(function* () {\n const layer = yield* plugin\n .initialize({ variables: _variables, secrets: hydratedConfig.secrets }, plugins ?? {})\n .pipe(\n Effect.provideService(PluginIdTag, plugin.id),\n Effect.provideService(Scope.Scope, scope),\n );\n\n return yield* Layer.buildWithScope(layer, scope).pipe(\n Effect.provideService(PluginIdTag, plugin.id),\n Effect.provideService(Scope.Scope, scope),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, plugin.id, undefined, \"initialize-layer\"),\n ),\n );\n }).pipe(\n Effect.onExit((exit) =>\n Exit.isSuccess(exit)\n ? Effect.void\n : Scope.close(scope, exit).pipe(\n Effect.catchCause((closeCause) =>\n Effect.logWarning(\n `Failed to close scope for plugin ${plugin.id} after initialize error`,\n closeCause,\n ),\n ),\n ),\n ),\n Effect.tapError((error) =>\n Effect.logError(\n `Plugin ${plugin.id} failed during initialize-plugin: ${String(error)}`,\n ),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, plugin.id, undefined, \"initialize-plugin\"),\n ),\n );\n\n return {\n plugin,\n metadata: pluginInstance.metadata,\n config: { variables: _variables, secrets: hydratedConfig.secrets },\n effectContext,\n scope,\n } satisfies InitializedPlugin<T>;\n }),\n };\n }),\n);\n"],"mappings":";;;;;;;;AAkBA,IAAa,oBAAb,cAAuC,QAAQ,QAA2C,CAAC,CACzF,gBACF,CAAC,CAAC,CAAC;AAEH,IAAa,eAAb,cAAkC,QAAQ,QAGxC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;AAYlB,IAAa,kBAAb,cAAqC,QAAQ,QAA+C,CAAC,CAC3F,iBACF,CAAC,CAAC,CAAC;AAEH,MAAa,yBAAyB,MAAM,OAC1C,iBACA,OAAO,IAAI,aAAa;CACtB,MAAM,WAAW,OAAO;CACxB,MAAM,YAAY,OAAO;CAEzB,OAAO;EACL,MAAM,aACJ,OAAO,IAAI,aAAa;GACtB,MAAM,QAAQ,SAAS;GAEvB,IAAI,CAAC,OACH,OAAO,OAAO,IAAI,mBAAmB;IACnC;IACA,WAAW;IACX,uBAAO,IAAI,MAAM,UAAU,SAAS,uBAAuB;GAC7D,CAAC;GAGH,IAAI,YAAY,OACd,OAAO;IACL,aAAa,MAAM;IACnB,UAAU;KACR,WAAW,MAAM,UAAU;KAC3B,SAAS,MAAM;KACf,aAAa,MAAM;IACrB;GACF;GAGF,OAAO;IACL,aAAa;IACb,UAAU;KACR,WAAW,MAAM;KACjB,SAAS,MAAM;KACf,aAAa,MAAM;IACrB;GACF;EACF,CAAC;EAEH,YAAY,aAAqB,OAAO,QAAQ,UAAU,aAAa,IAAI;CAC7E;AACF,CAAC,CACH;AAkBA,IAAa,sBAAb,cAAyC,QAAQ,QAG/C,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC;AAE5B,MAAa,6BAA6B,MAAM,OAC9C,qBACA,OAAO,IAAI,aAAa;CACtB,MAAM,0BAA0B,OAAO;CACvC,MAAM,iBAAiB,OAAO;CAC9B,MAAM,kBAAkB,OAAO;CAE/B,MAAM,cAAc,SAAiB,YACnC,WAAW,YAAY,WAAW,QAAQ,QAAQ,WAAW,IAAI,SAAS,IAAI;CAEhF,OAAO;EACL,aAAa,aACX,OAAO,IAAI,aAAa;GACtB,MAAM,QAAQ,OAAO,gBAAgB,IAAI,QAAQ;GAEjD,IAAI,MAAM,aAAa;IACrB,OAAO,OAAO,SAAS,qCAAqC,EAAE,SAAS,CAAC;IAExE,OAAO;KACL,MAAM,MAAM;KACZ,UAAU,MAAM;IAClB;GACF;GAEA,MAAM,MAAM,MAAM,SAAS;GAC3B,IAAI,CAAC,KACH,OAAO,OAAO,IAAI,mBAAmB;IACnC;IACA,WAAW;IACX,uBAAO,IAAI,MAAM,UAAU,SAAS,wCAAwC;GAC9E,CAAC;GAGH,MAAM,cAAc,WAAW,GAAG;GAElC,OAAO,wBAAwB,eAAe,UAAU,WAAW,CAAC,CAAC,KACnE,OAAO,UAAU,UACf,OAAO,SAAS,UAAU,SAAS,kCAAkC,OAAO,KAAK,GAAG,CACtF,GACA,OAAO,UAAU,UACf,qBAAqB,OAAO,UAAU,QAAW,iBAAiB,CACpE,CACF;GAEA,OAAO,OAAO,SAAS,8BAA8B;IAAE;IAAU,KAAK;GAAY,CAAC;GAanF,OAAO;IACL,aAZkB,wBACjB,sBAAsB,UAAU,WAAW,CAAC,CAC5C,KACC,OAAO,UAAU,UACf,OAAO,SAAS,UAAU,SAAS,8BAA8B,OAAO,KAAK,GAAG,CAClF,GACA,OAAO,UAAU,UACf,qBAAqB,OAAO,UAAU,QAAW,aAAa,CAChE,CACF;IAIA,UAAU,MAAM;GAClB;EACF,CAAC;EAEH,oBAAyC,UAAkB,iBACzD,OAAO,IAAI,aAAa;GACtB,MAAM,WAAW,OAAO,OAAO,UAAU,IAAI,aAAa,KAAK,CAAC,CAAC,CAAC,KAChE,OAAO,UAAU,UACf,OAAO,SAAS,UAAU,SAAS,qCAAqC,OAAO,CACjF,GACA,OAAO,UAAU,UACf,qBAAqB,OAAO,UAAU,QAAW,oBAAoB,CACvE,CACF;GAEA,AAAC,SAAS,KAAgB;GAE1B,OAAO;IACL,QAAQ;IACR,UAAU,aAAa;GACzB;EACF,CAAC;EAEH,mBACE,gBACA,QAIA,YAEA,OAAO,IAAI,aAAa;GACtB,MAAM,EAAE,WAAW;GAGnB,MAAM,qBAAqB,OAAO,SAChC,OAAO,aAAa,WAGpB,OAAO,WACP,OAAO,IACP,QACF,CAAC,CAAC,KACA,OAAO,UACJ,oBACC,IAAI,mBAAmB;IACrB,UAAU,OAAO;IACjB,WAAW;IACX,OAAO,gBAAgB;GACzB,CAAC,CACL,CACF;GAGA,MAAM,mBAAmB,OAAO,SAC9B,OAAO,aAAa,SAGpB,OAAO,SACP,OAAO,IACP,QACF,CAAC,CAAC,KACA,OAAO,UACJ,oBACC,IAAI,mBAAmB;IACrB,UAAU,OAAO;IACjB,WAAW;IACX,OAAO,gBAAgB;GACzB,CAAC,CACL,CACF;GAGA,MAAM,iBAAiB,OAAO,eAAe,eAAe;IAC1D,WAAW;IACX,SAAS;GACX,CAAC;GAED,MAAM,aAAa,OAAO,SACxB,OAAO,aAAa,WAGpB,eAAe,WACf,OAAO,IACP,QACF,CAAC,CAAC,KACA,OAAO,UACJ,oBACC,IAAI,mBAAmB;IACrB,UAAU,OAAO;IACjB,WAAW;IACX,OAAO,gBAAgB;GACzB,CAAC,CACL,CACF;GAKA,MAAM,QAAQ,OAAO,MAAM,KAAK;GAMhC,MAAM,gBAAgB,OAAO,OAAO,IAAI,aAAa;IACnD,MAAM,QAAQ,OAAO,OAClB,WAAW;KAAE,WAAW;KAAY,SAAS,eAAe;IAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,CACrF,KACC,OAAO,eAAe,aAAa,OAAO,EAAE,GAC5C,OAAO,eAAe,MAAM,OAAO,KAAK,CAC1C;IAEF,OAAO,OAAO,MAAM,eAAe,OAAO,KAAK,CAAC,CAAC,KAC/C,OAAO,eAAe,aAAa,OAAO,EAAE,GAC5C,OAAO,eAAe,MAAM,OAAO,KAAK,GACxC,OAAO,UAAU,UACf,qBAAqB,OAAO,OAAO,IAAI,QAAW,kBAAkB,CACtE,CACF;GACF,CAAC,CAAC,CAAC,KACD,OAAO,QAAQ,SACb,KAAK,UAAU,IAAI,IACf,OAAO,OACP,MAAM,MAAM,OAAO,IAAI,CAAC,CAAC,KACvB,OAAO,YAAY,eACjB,OAAO,WACL,oCAAoC,OAAO,GAAG,0BAC9C,UACF,CACF,CACF,CACN,GACA,OAAO,UAAU,UACf,OAAO,SACL,UAAU,OAAO,GAAG,oCAAoC,OAAO,KAAK,GACtE,CACF,GACA,OAAO,UAAU,UACf,qBAAqB,OAAO,OAAO,IAAI,QAAW,mBAAmB,CACvE,CACF;GAEA,OAAO;IACL;IACA,UAAU,eAAe;IACzB,QAAQ;KAAE,WAAW;KAAY,SAAS,eAAe;IAAQ;IACjE;IACA;GACF;EACF,CAAC;CACL;AACF,CAAC,CACH"}
|
|
1
|
+
{"version":3,"file":"plugin-loader.service.mjs","names":[],"sources":["../../../src/runtime/services/plugin-loader.service.ts"],"sourcesContent":["import type { InferSchemaInput, InferSchemaOutput } from \"@orpc/contract\";\nimport { Context, Effect, Exit, Layer, Scope } from \"effect\";\nimport type { z } from \"zod\";\nimport { PluginIdTag } from \"../../plugin\";\nimport type {\n AnyPlugin,\n AnyPluginConstructor,\n InitializedPlugin,\n LoadedPlugin,\n PluginInstance,\n PluginMetadata,\n PluginRegistry,\n} from \"../../types\";\nimport type { PluginWarningInfo } from \"../errors\";\nimport { PluginRuntimeError, toPluginRuntimeError } from \"../errors\";\nimport { validate } from \"../validation\";\nimport { ModuleFederationService } from \"./module-federation.service\";\nimport { SecretsService } from \"./secrets.service\";\n\n/**\n * Evaluate a plugin's `warnings` function against the validated config.\n * Fail-safe: a throwing warnings function becomes a warning itself rather\n * than failing initialization. Strings are normalized to `PluginWarningInfo`\n * with the plugin's registry key.\n */\nconst collectPluginWarnings = (\n plugin: AnyPlugin,\n config: { variables: unknown; secrets: unknown },\n): PluginWarningInfo[] => {\n const warningsFn = plugin.warnings;\n if (!warningsFn) return [];\n try {\n const raw = warningsFn(config as never);\n return raw.map((warning) =>\n typeof warning === \"string\" ? { pluginKey: plugin.id, message: warning } : warning,\n );\n } catch (error) {\n return [\n {\n pluginKey: plugin.id,\n message: `Plugin warnings check failed: ${error instanceof Error ? error.message : String(error)}`,\n },\n ];\n }\n};\n\nexport class PluginRegistryTag extends Context.Service<PluginRegistryTag, PluginRegistry>()(\n \"PluginRegistry\",\n) {}\n\nexport class PluginMapTag extends Context.Service<\n PluginMapTag,\n Record<string, AnyPluginConstructor>\n>()(\"PluginMap\") {}\n\nexport interface RegistryServiceShape {\n get: (\n pluginId: string,\n ) => Effect.Effect<\n { constructor: (new () => AnyPlugin) | null; metadata: PluginMetadata },\n PluginRuntimeError\n >;\n getModule: (pluginId: string) => Effect.Effect<AnyPluginConstructor | null>;\n}\n\nexport class RegistryService extends Context.Service<RegistryService, RegistryServiceShape>()(\n \"RegistryService\",\n) {}\n\nexport const RegistryServiceDefault = Layer.effect(\n RegistryService,\n Effect.gen(function* () {\n const registry = yield* PluginRegistryTag;\n const pluginMap = yield* PluginMapTag;\n\n return {\n get: (pluginId: string) =>\n Effect.gen(function* () {\n const entry = registry[pluginId];\n\n if (!entry) {\n return yield* new PluginRuntimeError({\n pluginId,\n operation: \"validate-plugin-id\",\n cause: new Error(`Plugin ${pluginId} not found in registry`),\n });\n }\n\n if (\"module\" in entry) {\n return {\n constructor: entry.module,\n metadata: {\n remoteUrl: entry.remote || \"\",\n version: entry.version,\n description: entry.description,\n } as PluginMetadata,\n };\n }\n\n return {\n constructor: null,\n metadata: {\n remoteUrl: entry.remote,\n version: entry.version,\n description: entry.description,\n } as PluginMetadata,\n };\n }),\n\n getModule: (pluginId: string) => Effect.succeed(pluginMap[pluginId] || null),\n };\n }),\n);\n\nexport interface PluginLoaderServiceShape {\n loadPlugin: (pluginId: string) => Effect.Effect<LoadedPlugin, PluginRuntimeError>;\n instantiatePlugin: <T extends AnyPlugin>(\n pluginId: string,\n loadedPlugin: LoadedPlugin<T>,\n ) => Effect.Effect<PluginInstance<T>, PluginRuntimeError>;\n initializePlugin: <T extends AnyPlugin>(\n pluginInstance: PluginInstance<T>,\n config: {\n variables: InferSchemaInput<T[\"configSchema\"][\"variables\"]>;\n secrets: InferSchemaInput<T[\"configSchema\"][\"secrets\"]>;\n },\n plugins?: Record<string, unknown>,\n ) => Effect.Effect<InitializedPlugin<T>, PluginRuntimeError>;\n}\n\nexport class PluginLoaderService extends Context.Service<\n PluginLoaderService,\n PluginLoaderServiceShape\n>()(\"PluginLoaderService\") {}\n\nexport const PluginLoaderServiceDefault = Layer.effect(\n PluginLoaderService,\n Effect.gen(function* () {\n const moduleFederationService = yield* ModuleFederationService;\n const secretsService = yield* SecretsService;\n const registryService = yield* RegistryService;\n\n const resolveUrl = (baseUrl: string, version?: string): string =>\n version && version !== \"latest\" ? baseUrl.replace(\"@latest\", `@${version}`) : baseUrl;\n\n return {\n loadPlugin: (pluginId: string) =>\n Effect.gen(function* () {\n const entry = yield* registryService.get(pluginId);\n\n if (entry.constructor) {\n yield* Effect.logDebug(\"Loading plugin from direct module\", { pluginId });\n\n return {\n ctor: entry.constructor,\n metadata: entry.metadata,\n } satisfies LoadedPlugin;\n }\n\n const url = entry.metadata.remoteUrl;\n if (!url) {\n return yield* new PluginRuntimeError({\n pluginId,\n operation: \"load-plugin\",\n cause: new Error(`Plugin ${pluginId} has no module or remote URL configured`),\n });\n }\n\n const resolvedUrl = resolveUrl(url);\n\n yield* moduleFederationService.registerRemote(pluginId, resolvedUrl).pipe(\n Effect.tapError((error) =>\n Effect.logError(`Plugin ${pluginId} failed during register-remote: ${String(error)}`),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, pluginId, undefined, \"register-remote\"),\n ),\n );\n\n yield* Effect.logDebug(\"Loading plugin from remote\", { pluginId, url: resolvedUrl });\n\n const ctor = yield* moduleFederationService\n .loadRemoteConstructor(pluginId, resolvedUrl)\n .pipe(\n Effect.tapError((error) =>\n Effect.logError(`Plugin ${pluginId} failed during load-remote: ${String(error)}`),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, pluginId, undefined, \"load-remote\"),\n ),\n );\n\n return {\n ctor,\n metadata: entry.metadata,\n } satisfies LoadedPlugin;\n }),\n\n instantiatePlugin: <T extends AnyPlugin>(pluginId: string, loadedPlugin: LoadedPlugin<T>) =>\n Effect.gen(function* () {\n const instance = yield* Effect.try(() => new loadedPlugin.ctor()).pipe(\n Effect.tapError((error) =>\n Effect.logError(`Plugin ${pluginId} failed during instantiate-plugin: ${error}`),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, pluginId, undefined, \"instantiate-plugin\"),\n ),\n );\n\n (instance.id as string) = pluginId;\n\n return {\n plugin: instance,\n metadata: loadedPlugin.metadata,\n } satisfies PluginInstance<T>;\n }),\n\n initializePlugin: <T extends AnyPlugin>(\n pluginInstance: PluginInstance<T>,\n config: {\n variables: InferSchemaInput<T[\"configSchema\"][\"variables\"]>;\n secrets: InferSchemaInput<T[\"configSchema\"][\"secrets\"]>;\n },\n plugins?: Record<string, unknown>,\n ) =>\n Effect.gen(function* () {\n const { plugin } = pluginInstance;\n\n // Validate and hydrate config\n const validatedVariables = yield* validate(\n plugin.configSchema.variables as z.ZodSchema<\n InferSchemaOutput<T[\"configSchema\"][\"variables\"]>\n >,\n config.variables,\n plugin.id,\n \"config\",\n ).pipe(\n Effect.mapError(\n (validationError) =>\n new PluginRuntimeError({\n pluginId: plugin.id,\n operation: \"validate-config\",\n cause: validationError.zodError,\n }),\n ),\n );\n\n // Validate secrets\n const validatedSecrets = yield* validate(\n plugin.configSchema.secrets as z.ZodSchema<\n InferSchemaOutput<T[\"configSchema\"][\"secrets\"]>\n >,\n config.secrets,\n plugin.id,\n \"config\",\n ).pipe(\n Effect.mapError(\n (validationError) =>\n new PluginRuntimeError({\n pluginId: plugin.id,\n operation: \"validate-secrets\",\n cause: validationError.zodError,\n }),\n ),\n );\n\n // Hydrate secrets in variables\n const hydratedConfig = yield* secretsService.hydrateSecrets({\n variables: validatedVariables,\n secrets: validatedSecrets,\n });\n\n const _variables = yield* validate(\n plugin.configSchema.variables as z.ZodSchema<\n InferSchemaOutput<T[\"configSchema\"][\"variables\"]>\n >,\n hydratedConfig.variables,\n plugin.id,\n \"config\",\n ).pipe(\n Effect.mapError(\n (validationError) =>\n new PluginRuntimeError({\n pluginId: plugin.id,\n operation: \"validate-hydrated-config\",\n cause: validationError.zodError,\n }),\n ),\n );\n\n // Create a long-lived scope for this plugin instance. Initialize\n // returns a Layer which is built against this scope, so scoped\n // resources (DB pools, caches) release on plugin shutdown.\n const scope = yield* Scope.make();\n\n // Build the plugin's Layer within the scope.\n // On failure, close the scope immediately so scoped resources\n // (DB pools, caches) are released — otherwise every failed\n // initialization leaks until the process exits.\n const effectContext = yield* Effect.gen(function* () {\n const layer = yield* plugin\n .initialize({ variables: _variables, secrets: hydratedConfig.secrets }, plugins ?? {})\n .pipe(\n Effect.provideService(PluginIdTag, plugin.id),\n Effect.provideService(Scope.Scope, scope),\n );\n\n return yield* Layer.buildWithScope(layer, scope).pipe(\n Effect.provideService(PluginIdTag, plugin.id),\n Effect.provideService(Scope.Scope, scope),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, plugin.id, undefined, \"initialize-layer\"),\n ),\n );\n }).pipe(\n Effect.onExit((exit) =>\n Exit.isSuccess(exit)\n ? Effect.void\n : Scope.close(scope, exit).pipe(\n Effect.catchCause((closeCause) =>\n Effect.logWarning(\n `Failed to close scope for plugin ${plugin.id} after initialize error`,\n closeCause,\n ),\n ),\n ),\n ),\n Effect.tapError((error) =>\n Effect.logError(\n `Plugin ${plugin.id} failed during initialize-plugin: ${String(error)}`,\n ),\n ),\n Effect.mapError((error) =>\n toPluginRuntimeError(error, plugin.id, undefined, \"initialize-plugin\"),\n ),\n );\n\n return {\n plugin,\n metadata: pluginInstance.metadata,\n config: { variables: _variables, secrets: hydratedConfig.secrets },\n effectContext,\n scope,\n warnings: collectPluginWarnings(plugin, {\n variables: _variables,\n secrets: hydratedConfig.secrets,\n }),\n } satisfies InitializedPlugin<T>;\n }),\n };\n }),\n);\n"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ function manifestUrl(remoteUrl) {
|
|
|
6
6
|
return remoteUrl.endsWith("/") ? `${remoteUrl}mf-manifest.json` : `${remoteUrl}/mf-manifest.json`;
|
|
7
7
|
}
|
|
8
8
|
function normalizeBaseUrl(remoteUrl) {
|
|
9
|
-
return (remoteUrl.split("#")[0] ?? remoteUrl).replace(/\/(mf-manifest\.json|remoteEntry
|
|
9
|
+
return (remoteUrl.split("#")[0] ?? remoteUrl).replace(/\/(mf-manifest\.json|remoteEntry(?:\.[^/]+)?\.js)$/i, "");
|
|
10
10
|
}
|
|
11
11
|
function compareSharedIdentity(remoteShared, expectedVersions) {
|
|
12
12
|
const mismatches = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-identity.cjs","names":["isEffectCriticalSharedDep"],"sources":["../../../src/runtime/services/shared-identity.ts"],"sourcesContent":["import { isEffectCriticalSharedDep } from \"../../shared-deps-spec\";\n\ninterface IdentityManifest {\n metaData?: { pluginVersion?: string; name?: string };\n shared?: Array<{ name: string; version?: string }>;\n}\n\nexport interface SharedIdentityMismatch {\n name: string;\n expected: string;\n actual: string | null;\n}\n\nconst MANIFEST_TIMEOUT_MS = 15_000;\n\nfunction manifestUrl(remoteUrl: string): string {\n return remoteUrl.endsWith(\"/\") ? `${remoteUrl}mf-manifest.json` : `${remoteUrl}/mf-manifest.json`;\n}\n\nfunction normalizeBaseUrl(remoteUrl: string): string {\n const withoutHash = remoteUrl.split(\"#\")[0] ?? remoteUrl;\n return withoutHash.replace(/\\/(mf-manifest\\.json|remoteEntry
|
|
1
|
+
{"version":3,"file":"shared-identity.cjs","names":["isEffectCriticalSharedDep"],"sources":["../../../src/runtime/services/shared-identity.ts"],"sourcesContent":["import { isEffectCriticalSharedDep } from \"../../shared-deps-spec\";\n\ninterface IdentityManifest {\n metaData?: { pluginVersion?: string; name?: string };\n shared?: Array<{ name: string; version?: string }>;\n}\n\nexport interface SharedIdentityMismatch {\n name: string;\n expected: string;\n actual: string | null;\n}\n\nconst MANIFEST_TIMEOUT_MS = 15_000;\n\nfunction manifestUrl(remoteUrl: string): string {\n return remoteUrl.endsWith(\"/\") ? `${remoteUrl}mf-manifest.json` : `${remoteUrl}/mf-manifest.json`;\n}\n\nfunction normalizeBaseUrl(remoteUrl: string): string {\n const withoutHash = remoteUrl.split(\"#\")[0] ?? remoteUrl;\n return withoutHash.replace(/\\/(mf-manifest\\.json|remoteEntry(?:\\.[^/]+)?\\.js)$/i, \"\");\n}\n\nexport function compareSharedIdentity(\n remoteShared: Array<{ name: string; version?: string }> | undefined,\n expectedVersions: Map<string, string>,\n): SharedIdentityMismatch[] {\n const mismatches: SharedIdentityMismatch[] = [];\n const remoteByName = new Map((remoteShared ?? []).map((s) => [s.name, s]));\n\n for (const [name, expected] of expectedVersions.entries()) {\n if (!isEffectCriticalSharedDep(name)) continue;\n const remoteEntry = remoteByName.get(name);\n if (!remoteEntry) continue;\n if (remoteEntry.version && remoteEntry.version !== expected) {\n mismatches.push({ name, expected, actual: remoteEntry.version });\n }\n }\n\n return mismatches;\n}\n\nexport function describeSharedIdentityMismatch(\n pluginId: string,\n mismatches: SharedIdentityMismatch[],\n hostExpectation: string,\n): string {\n const lines = mismatches.map(\n (m) =>\n ` ${m.name}: expected ${m.expected} (from ${hostExpectation}), got ${m.actual ?? \"missing\"}`,\n );\n return (\n `[SharedIdentity] Refusing to load plugin \"${pluginId}\": shared dependency version skew.\\n` +\n `${lines.join(\"\\n\")}\\n` +\n `Run \"bos mf check\" for the full report. Fix: rebuild and redeploy the host and this plugin together (single release train, e.g. \"bos publish --deploy --packages local\").`\n );\n}\n\nexport async function fetchRemoteIdentityManifest(\n remoteUrl: string,\n): Promise<IdentityManifest | null> {\n const base = normalizeBaseUrl(remoteUrl);\n const url = manifestUrl(base);\n const res = await fetch(url, { signal: AbortSignal.timeout(MANIFEST_TIMEOUT_MS) });\n if (!res.ok) return null;\n return (await res.json()) as IdentityManifest;\n}\n"],"mappings":";;;AAaA,MAAM,sBAAsB;AAE5B,SAAS,YAAY,WAA2B;CAC9C,OAAO,UAAU,SAAS,GAAG,IAAI,GAAG,UAAU,oBAAoB,GAAG,UAAU;AACjF;AAEA,SAAS,iBAAiB,WAA2B;CAEnD,QADoB,UAAU,MAAM,GAAG,CAAC,CAAC,MAAM,UAC7B,CAAC,QAAQ,uDAAuD,EAAE;AACtF;AAEA,SAAgB,sBACd,cACA,kBAC0B;CAC1B,MAAM,aAAuC,CAAC;CAC9C,MAAM,eAAe,IAAI,KAAK,gBAAgB,CAAC,EAAC,CAAE,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CAEzE,KAAK,MAAM,CAAC,MAAM,aAAa,iBAAiB,QAAQ,GAAG;EACzD,IAAI,CAACA,mDAA0B,IAAI,GAAG;EACtC,MAAM,cAAc,aAAa,IAAI,IAAI;EACzC,IAAI,CAAC,aAAa;EAClB,IAAI,YAAY,WAAW,YAAY,YAAY,UACjD,WAAW,KAAK;GAAE;GAAM;GAAU,QAAQ,YAAY;EAAQ,CAAC;CAEnE;CAEA,OAAO;AACT;AAEA,SAAgB,+BACd,UACA,YACA,iBACQ;CAKR,OACE,6CAA6C,SAAS,sCAL1C,WAAW,KACtB,MACC,KAAK,EAAE,KAAK,aAAa,EAAE,SAAS,SAAS,gBAAgB,SAAS,EAAE,UAAU,WAI7E,CAAC,CAAC,KAAK,IAAI,EAAE;AAGxB;AAEA,eAAsB,4BACpB,WACkC;CAElC,MAAM,MAAM,YADC,iBAAiB,SACH,CAAC;CAC5B,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,QAAQ,mBAAmB,EAAE,CAAC;CACjF,IAAI,CAAC,IAAI,IAAI,OAAO;CACpB,OAAQ,MAAM,IAAI,KAAK;AACzB"}
|
|
@@ -6,7 +6,7 @@ function manifestUrl(remoteUrl) {
|
|
|
6
6
|
return remoteUrl.endsWith("/") ? `${remoteUrl}mf-manifest.json` : `${remoteUrl}/mf-manifest.json`;
|
|
7
7
|
}
|
|
8
8
|
function normalizeBaseUrl(remoteUrl) {
|
|
9
|
-
return (remoteUrl.split("#")[0] ?? remoteUrl).replace(/\/(mf-manifest\.json|remoteEntry
|
|
9
|
+
return (remoteUrl.split("#")[0] ?? remoteUrl).replace(/\/(mf-manifest\.json|remoteEntry(?:\.[^/]+)?\.js)$/i, "");
|
|
10
10
|
}
|
|
11
11
|
function compareSharedIdentity(remoteShared, expectedVersions) {
|
|
12
12
|
const mismatches = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared-identity.mjs","names":[],"sources":["../../../src/runtime/services/shared-identity.ts"],"sourcesContent":["import { isEffectCriticalSharedDep } from \"../../shared-deps-spec\";\n\ninterface IdentityManifest {\n metaData?: { pluginVersion?: string; name?: string };\n shared?: Array<{ name: string; version?: string }>;\n}\n\nexport interface SharedIdentityMismatch {\n name: string;\n expected: string;\n actual: string | null;\n}\n\nconst MANIFEST_TIMEOUT_MS = 15_000;\n\nfunction manifestUrl(remoteUrl: string): string {\n return remoteUrl.endsWith(\"/\") ? `${remoteUrl}mf-manifest.json` : `${remoteUrl}/mf-manifest.json`;\n}\n\nfunction normalizeBaseUrl(remoteUrl: string): string {\n const withoutHash = remoteUrl.split(\"#\")[0] ?? remoteUrl;\n return withoutHash.replace(/\\/(mf-manifest\\.json|remoteEntry
|
|
1
|
+
{"version":3,"file":"shared-identity.mjs","names":[],"sources":["../../../src/runtime/services/shared-identity.ts"],"sourcesContent":["import { isEffectCriticalSharedDep } from \"../../shared-deps-spec\";\n\ninterface IdentityManifest {\n metaData?: { pluginVersion?: string; name?: string };\n shared?: Array<{ name: string; version?: string }>;\n}\n\nexport interface SharedIdentityMismatch {\n name: string;\n expected: string;\n actual: string | null;\n}\n\nconst MANIFEST_TIMEOUT_MS = 15_000;\n\nfunction manifestUrl(remoteUrl: string): string {\n return remoteUrl.endsWith(\"/\") ? `${remoteUrl}mf-manifest.json` : `${remoteUrl}/mf-manifest.json`;\n}\n\nfunction normalizeBaseUrl(remoteUrl: string): string {\n const withoutHash = remoteUrl.split(\"#\")[0] ?? remoteUrl;\n return withoutHash.replace(/\\/(mf-manifest\\.json|remoteEntry(?:\\.[^/]+)?\\.js)$/i, \"\");\n}\n\nexport function compareSharedIdentity(\n remoteShared: Array<{ name: string; version?: string }> | undefined,\n expectedVersions: Map<string, string>,\n): SharedIdentityMismatch[] {\n const mismatches: SharedIdentityMismatch[] = [];\n const remoteByName = new Map((remoteShared ?? []).map((s) => [s.name, s]));\n\n for (const [name, expected] of expectedVersions.entries()) {\n if (!isEffectCriticalSharedDep(name)) continue;\n const remoteEntry = remoteByName.get(name);\n if (!remoteEntry) continue;\n if (remoteEntry.version && remoteEntry.version !== expected) {\n mismatches.push({ name, expected, actual: remoteEntry.version });\n }\n }\n\n return mismatches;\n}\n\nexport function describeSharedIdentityMismatch(\n pluginId: string,\n mismatches: SharedIdentityMismatch[],\n hostExpectation: string,\n): string {\n const lines = mismatches.map(\n (m) =>\n ` ${m.name}: expected ${m.expected} (from ${hostExpectation}), got ${m.actual ?? \"missing\"}`,\n );\n return (\n `[SharedIdentity] Refusing to load plugin \"${pluginId}\": shared dependency version skew.\\n` +\n `${lines.join(\"\\n\")}\\n` +\n `Run \"bos mf check\" for the full report. Fix: rebuild and redeploy the host and this plugin together (single release train, e.g. \"bos publish --deploy --packages local\").`\n );\n}\n\nexport async function fetchRemoteIdentityManifest(\n remoteUrl: string,\n): Promise<IdentityManifest | null> {\n const base = normalizeBaseUrl(remoteUrl);\n const url = manifestUrl(base);\n const res = await fetch(url, { signal: AbortSignal.timeout(MANIFEST_TIMEOUT_MS) });\n if (!res.ok) return null;\n return (await res.json()) as IdentityManifest;\n}\n"],"mappings":";;;AAaA,MAAM,sBAAsB;AAE5B,SAAS,YAAY,WAA2B;CAC9C,OAAO,UAAU,SAAS,GAAG,IAAI,GAAG,UAAU,oBAAoB,GAAG,UAAU;AACjF;AAEA,SAAS,iBAAiB,WAA2B;CAEnD,QADoB,UAAU,MAAM,GAAG,CAAC,CAAC,MAAM,UAC7B,CAAC,QAAQ,uDAAuD,EAAE;AACtF;AAEA,SAAgB,sBACd,cACA,kBAC0B;CAC1B,MAAM,aAAuC,CAAC;CAC9C,MAAM,eAAe,IAAI,KAAK,gBAAgB,CAAC,EAAC,CAAE,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CAEzE,KAAK,MAAM,CAAC,MAAM,aAAa,iBAAiB,QAAQ,GAAG;EACzD,IAAI,CAAC,0BAA0B,IAAI,GAAG;EACtC,MAAM,cAAc,aAAa,IAAI,IAAI;EACzC,IAAI,CAAC,aAAa;EAClB,IAAI,YAAY,WAAW,YAAY,YAAY,UACjD,WAAW,KAAK;GAAE;GAAM;GAAU,QAAQ,YAAY;EAAQ,CAAC;CAEnE;CAEA,OAAO;AACT;AAEA,SAAgB,+BACd,UACA,YACA,iBACQ;CAKR,OACE,6CAA6C,SAAS,sCAL1C,WAAW,KACtB,MACC,KAAK,EAAE,KAAK,aAAa,EAAE,SAAS,SAAS,gBAAgB,SAAS,EAAE,UAAU,WAI7E,CAAC,CAAC,KAAK,IAAI,EAAE;AAGxB;AAEA,eAAsB,4BACpB,WACkC;CAElC,MAAM,MAAM,YADC,iBAAiB,SACH,CAAC;CAC5B,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,YAAY,QAAQ,mBAAmB,EAAE,CAAC;CACjF,IAAI,CAAC,IAAI,IAAI,OAAO;CACpB,OAAQ,MAAM,IAAI,KAAK;AACzB"}
|
package/dist/types.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PluginWarningInfo } from "./runtime/errors.cjs";
|
|
1
2
|
import { Plugin } from "./plugin.cjs";
|
|
2
3
|
import { ContractedRouter, RouterClient } from "@orpc/server";
|
|
3
4
|
import { Context, Scope } from "effect";
|
|
@@ -181,6 +182,8 @@ interface InitializedPlugin<T extends AnyPlugin = AnyPlugin> {
|
|
|
181
182
|
*/
|
|
182
183
|
readonly effectContext: Context.Context<any>;
|
|
183
184
|
readonly scope: Scope.Closeable;
|
|
185
|
+
/** Non-fatal config warnings collected during initialization (empty when none). */
|
|
186
|
+
readonly warnings: PluginWarningInfo[];
|
|
184
187
|
}
|
|
185
188
|
/**
|
|
186
189
|
* Helper type to detect type errors when looking up RegisteredPlugins
|
package/dist/types.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.cts","names":[],"sources":["../src/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.cts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAwBiB;;;;KAKL,YAAY,OAAO,mBAAmB,WAAW,WAAW;;;;KAK5D;UACF;EACR;IACE,UAAU;IACV,WAAW;IACX,SAAS;IACT,SAAS;;;;;;KAOD;EACN,QAAQ;EAAsB;EAAiB;EAAkB;;EACjE;EAAgB;EAAkB;;;;;KAK5B,kBAAkB,KAAK;EAAY,cAAc;IAAM;;;;KAKvD,yBAAyB,UAAU,eAAe,2BAC3D,WAAW,IAAI,EAAE;EAAa,cAAc;IACzC,IACA,gBAAgB,oBACd,kBAAkB;;;;KAOd,eAAe,KAAK;EAC9B;IAAW,gBAAgB,UAAU;;IAEnC;;;;KAMQ,gBAAgB,KAAK;EAAY;IAAW,iBAAiB,UAAU;;IAC/E;;;;KAMQ,cAAc,KAAK;EAAY;IAAW,eAAe,UAAU;;IAC3E;;;;KAMQ,cAAc,KAAK;EAC7B;IAAW,eAAe,UAAU;;IAElC;;;;;KAOQ,iBAAiB,KAAK,iBAAiB,eAAe;;;;KAKtD,iBAAiB,KAAK,aAAa,iBAAiB;;;;;;KAOpD,oBAAoB;EAC9B,SAAS,UAAU,mBAAmB,OAAO,iBAAiB;EAC9D,QAAQ,iBAAiB;;;;;;;KAQf,iBAAiB,gBAAgB,GAAG,IAAI,qBAAqB,EAAE;EACzE,eAAe;IAEb;EACE,gBAAgB,UAAU;EAC1B,iBAAiB,UAAU;EAC3B,eAAe,UAAU;EACzB,eAAe,wBAAwB;IAEvC,OAAO,GAAG,GAAG,GAAG;;;;KAOV,mBAAmB,KAC7B,cAAc,WAAW,YAAY,iBAAiB,cAAc;;;;KAK1D,kBAAkB;EAC5B,WAAW,iBAAiB,gBAAgB;EAC5C,SAAS,iBAAiB,cAAc;;;;;KAM9B;WACD;WACA;WACA;;;;;KAMC,iBAAiB,eAAe;;;;;UAM3B;GACd;;;;;UAMc,aAAa,UAAU,YAAY;WACzC,gBAAgB;WAChB,UAAU;;;;;UAMJ,eAAe,UAAU,YAAY;WAC3C,QAAQ;WACR,UAAU;;;;;UAMJ,kBAAkB,UAAU,YAAY;WAC9C,QAAQ;WACR,UAAU;WACV;IACP,WAAW,kBAAkB;IAC7B,SAAS,kBAAkB;;;;;;;WAOpB,eAAe,QAAQ;WACvB,OAAO,MAAM;;WAEb,UAAU;;;;;KAMhB,oBAAoB,gBAAgB,GAAG,IAAI,qBAAqB,EAAE;EACrE,eAAe;IAEb;EACE,UAAU;EACV,WAAW;EACX,SAAS;EACT,SAAS;wBAGI,4IACF;;;;;;KAOL,gBAAgB,gBAAgB,GAAG,IAAI,qBACjD,oBAAoB,GAAG;WAER,eAAe,UAAU,mBAAmB,EAAE,QAAQ,iBAAiB,EAAE;WACzE,QAAQ,iBAAiB,EAAE;WAC3B,UAAU;WACV,aAAa,kBAAkB,iBAAiB,GAAG;IAE9D,oBAAoB,GAAG;;;;;;;KAQxB,WAAW,KAAK;EAAY,uBAAuB;IAAM,IAAI;;;;kBAKjD;;;;;;;;;;;;;;;;;;OAkBH,MACV,kBACA,SAAS,qBACP,gBAAgB,WAAW,UAAU,gBAAgB,GAAG,WAAW;;;;;UAMxD,oBACf,UAAU,eAAe,uBAAuB,eAAe;EAE/D,UAAU;EACV,UAAU"}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PluginWarningInfo } from "./runtime/errors.mjs";
|
|
1
2
|
import { Plugin } from "./plugin.mjs";
|
|
2
3
|
import { Context, Scope } from "effect";
|
|
3
4
|
import { ContractedRouter, RouterClient } from "@orpc/server";
|
|
@@ -181,6 +182,8 @@ interface InitializedPlugin<T extends AnyPlugin = AnyPlugin> {
|
|
|
181
182
|
*/
|
|
182
183
|
readonly effectContext: Context.Context<any>;
|
|
183
184
|
readonly scope: Scope.Closeable;
|
|
185
|
+
/** Non-fatal config warnings collected during initialization (empty when none). */
|
|
186
|
+
readonly warnings: PluginWarningInfo[];
|
|
184
187
|
}
|
|
185
188
|
/**
|
|
186
189
|
* Helper type to detect type errors when looking up RegisteredPlugins
|
package/dist/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAwBiB;;;;KAKL,YAAY,OAAO,mBAAmB,WAAW,WAAW;;;;KAK5D;UACF;EACR;IACE,UAAU;IACV,WAAW;IACX,SAAS;IACT,SAAS;;;;;;KAOD;EACN,QAAQ;EAAsB;EAAiB;EAAkB;;EACjE;EAAgB;EAAkB;;;;;KAK5B,kBAAkB,KAAK;EAAY,cAAc;IAAM;;;;KAKvD,yBAAyB,UAAU,eAAe,2BAC3D,WAAW,IAAI,EAAE;EAAa,cAAc;IACzC,IACA,gBAAgB,oBACd,kBAAkB;;;;KAOd,eAAe,KAAK;EAC9B;IAAW,gBAAgB,UAAU;;IAEnC;;;;KAMQ,gBAAgB,KAAK;EAAY;IAAW,iBAAiB,UAAU;;IAC/E;;;;KAMQ,cAAc,KAAK;EAAY;IAAW,eAAe,UAAU;;IAC3E;;;;KAMQ,cAAc,KAAK;EAC7B;IAAW,eAAe,UAAU;;IAElC;;;;;KAOQ,iBAAiB,KAAK,iBAAiB,eAAe;;;;KAKtD,iBAAiB,KAAK,aAAa,iBAAiB;;;;;;KAOpD,oBAAoB;EAC9B,SAAS,UAAU,mBAAmB,OAAO,iBAAiB;EAC9D,QAAQ,iBAAiB;;;;;;;KAQf,iBAAiB,gBAAgB,GAAG,IAAI,qBAAqB,EAAE;EACzE,eAAe;IAEb;EACE,gBAAgB,UAAU;EAC1B,iBAAiB,UAAU;EAC3B,eAAe,UAAU;EACzB,eAAe,wBAAwB;IAEvC,OAAO,GAAG,GAAG,GAAG;;;;KAOV,mBAAmB,KAC7B,cAAc,WAAW,YAAY,iBAAiB,cAAc;;;;KAK1D,kBAAkB;EAC5B,WAAW,iBAAiB,gBAAgB;EAC5C,SAAS,iBAAiB,cAAc;;;;;KAM9B;WACD;WACA;WACA;;;;;KAMC,iBAAiB,eAAe;;;;;UAM3B;GACd;;;;;UAMc,aAAa,UAAU,YAAY;WACzC,gBAAgB;WAChB,UAAU;;;;;UAMJ,eAAe,UAAU,YAAY;WAC3C,QAAQ;WACR,UAAU;;;;;UAMJ,kBAAkB,UAAU,YAAY;WAC9C,QAAQ;WACR,UAAU;WACV;IACP,WAAW,kBAAkB;IAC7B,SAAS,kBAAkB;;;;;;;WAOpB,eAAe,QAAQ;WACvB,OAAO,MAAM;;WAEb,UAAU;;;;;KAMhB,oBAAoB,gBAAgB,GAAG,IAAI,qBAAqB,EAAE;EACrE,eAAe;IAEb;EACE,UAAU;EACV,WAAW;EACX,SAAS;EACT,SAAS;wBAGI,4IACF;;;;;;KAOL,gBAAgB,gBAAgB,GAAG,IAAI,qBACjD,oBAAoB,GAAG;WAER,eAAe,UAAU,mBAAmB,EAAE,QAAQ,iBAAiB,EAAE;WACzE,QAAQ,iBAAiB,EAAE;WAC3B,UAAU;WACV,aAAa,kBAAkB,iBAAiB,GAAG;IAE9D,oBAAoB,GAAG;;;;;;;KAQxB,WAAW,KAAK;EAAY,uBAAuB;IAAM,IAAI;;;;kBAKjD;;;;;;;;;;;;;;;;;;OAkBH,MACV,kBACA,SAAS,qBACP,gBAAgB,WAAW,UAAU,gBAAgB,GAAG,WAAW;;;;;UAMxD,oBACf,UAAU,eAAe,uBAAuB,eAAe;EAE/D,UAAU;EACV,UAAU"}
|
|
@@ -46,7 +46,7 @@ const WorkspaceVersionManifestSchema = zod.z.object({
|
|
|
46
46
|
* coordinate → a new version (and a new file).
|
|
47
47
|
*/
|
|
48
48
|
function composeVersionManifest(input) {
|
|
49
|
-
const { builtAt, ...hashedInputs } = input;
|
|
49
|
+
const { builtAt: _builtAt, ...hashedInputs } = input;
|
|
50
50
|
const version = (0, node_crypto.createHash)("sha256").update(stableStringify(hashedInputs)).digest("hex").slice(0, 16);
|
|
51
51
|
return WorkspaceVersionManifestSchema.parse({
|
|
52
52
|
...input,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-manifest.cjs","names":["z","createHash"],"sources":["../src/version-manifest.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\n\n/**\n * WorkspaceVersionManifest — the immutable per-deploy document at\n * `bundles/<account>/<gateway>/<workspace>/versions/<version>.json`\n * (ADR 0020, as amended; atomic-deploys MAP decision 1). The published\n * config's slot pins this document via its `pin` (`manifest` + the\n * manifest's own SRI); every artifact coordinate lives here, so the config\n * stops growing with artifact kinds and old versions stay fully servable.\n */\n\nconst SRI = z.string().regex(/^sha384-[A-Za-z0-9+/=]+$/);\n\nexport const WorkspaceVersionManifestSchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n builtAt: z.string().optional(),\n gitSha: z.string().optional(),\n entry: z.string().min(1),\n entryIntegrity: SRI,\n ssr: z\n .object({\n entry: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n browserManifest: z\n .object({\n file: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n /** extra assets (css, icons) — logical name → served-object SRI */\n assets: z.record(z.string(), SRI).optional(),\n /** shared-dependency versions this build was built against */\n shared: z.record(z.string(), z.string()).optional(),\n /**\n * Full per-file digest map of the dist (object path → served-object SRI).\n * Written by `every-plugin`-based deploys so the next deploy can diff against\n * it and skip re-uploading unchanged files; readers that predate the field\n * ignore it (zod strips unknown keys). Participates in the version hash.\n */\n files: z.record(z.string(), SRI).optional(),\n});\nexport type WorkspaceVersionManifest = z.infer<typeof WorkspaceVersionManifestSchema>;\n\nexport interface VersionManifestInput {\n /** Omit: a per-build timestamp makes the same version id map to different\n * manifest bytes per machine, breaking the pin's SRI against the image's\n * staged copy and the CDN's immutability for republished versions. */\n builtAt?: string;\n gitSha?: string;\n entry: string;\n entryIntegrity: string;\n ssr?: { entry: string; integrity: string };\n browserManifest?: { file: string; integrity: string };\n assets?: Record<string, string>;\n shared?: Record<string, string>;\n files?: Record<string, string>;\n}\n\n/**\n * Compose the manifest with its `version` derived from the canonical content\n * of the artifact coordinates alone (key-sorted, `builtAt` excluded) —\n * identical bytes → identical version id regardless of when they were built,\n * so republishing unchanged content is a pointer no-op. Any changed\n * coordinate → a new version (and a new file).\n */\nexport function composeVersionManifest(input: VersionManifestInput): WorkspaceVersionManifest {\n const { builtAt, ...hashedInputs } = input;\n const version = createHash(\"sha256\")\n .update(stableStringify(hashedInputs))\n .digest(\"hex\")\n .slice(0, 16);\n return WorkspaceVersionManifestSchema.parse({ ...input, version });\n}\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n if (value && typeof value === \"object\") {\n return `{${Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n .sort()\n .join(\",\")}}`;\n }\n return JSON.stringify(value) ?? \"null\";\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,MAAM,MAAMA,MAAE,OAAO,CAAC,CAAC,MAAM,0BAA0B;AAEvD,MAAa,iCAAiCA,MAAE,OAAO;CACrD,SAASA,MAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB;CAC1C,SAASA,MAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,QAAQA,MAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,OAAOA,MAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,gBAAgB;CAChB,KAAKA,MACF,OAAO;EACN,OAAOA,MAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACvB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;CACZ,iBAAiBA,MACd,OAAO;EACN,MAAMA,MAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACtB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;;CAEZ,QAAQA,MAAE,OAAOA,MAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;;CAE3C,QAAQA,MAAE,OAAOA,MAAE,OAAO,GAAGA,MAAE,OAAO,CAAC,CAAC,CAAC,SAAS;;;;;;;CAOlD,OAAOA,MAAE,OAAOA,MAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;AAC5C,CAAC;;;;;;;;AAyBD,SAAgB,uBAAuB,OAAuD;CAC5F,MAAM,EAAE,SAAS,GAAG,iBAAiB;
|
|
1
|
+
{"version":3,"file":"version-manifest.cjs","names":["z","createHash"],"sources":["../src/version-manifest.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\n\n/**\n * WorkspaceVersionManifest — the immutable per-deploy document at\n * `bundles/<account>/<gateway>/<workspace>/versions/<version>.json`\n * (ADR 0020, as amended; atomic-deploys MAP decision 1). The published\n * config's slot pins this document via its `pin` (`manifest` + the\n * manifest's own SRI); every artifact coordinate lives here, so the config\n * stops growing with artifact kinds and old versions stay fully servable.\n */\n\nconst SRI = z.string().regex(/^sha384-[A-Za-z0-9+/=]+$/);\n\nexport const WorkspaceVersionManifestSchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n builtAt: z.string().optional(),\n gitSha: z.string().optional(),\n entry: z.string().min(1),\n entryIntegrity: SRI,\n ssr: z\n .object({\n entry: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n browserManifest: z\n .object({\n file: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n /** extra assets (css, icons) — logical name → served-object SRI */\n assets: z.record(z.string(), SRI).optional(),\n /** shared-dependency versions this build was built against */\n shared: z.record(z.string(), z.string()).optional(),\n /**\n * Full per-file digest map of the dist (object path → served-object SRI).\n * Written by `every-plugin`-based deploys so the next deploy can diff against\n * it and skip re-uploading unchanged files; readers that predate the field\n * ignore it (zod strips unknown keys). Participates in the version hash.\n */\n files: z.record(z.string(), SRI).optional(),\n});\nexport type WorkspaceVersionManifest = z.infer<typeof WorkspaceVersionManifestSchema>;\n\nexport interface VersionManifestInput {\n /** Omit: a per-build timestamp makes the same version id map to different\n * manifest bytes per machine, breaking the pin's SRI against the image's\n * staged copy and the CDN's immutability for republished versions. */\n builtAt?: string;\n gitSha?: string;\n entry: string;\n entryIntegrity: string;\n ssr?: { entry: string; integrity: string };\n browserManifest?: { file: string; integrity: string };\n assets?: Record<string, string>;\n shared?: Record<string, string>;\n files?: Record<string, string>;\n}\n\n/**\n * Compose the manifest with its `version` derived from the canonical content\n * of the artifact coordinates alone (key-sorted, `builtAt` excluded) —\n * identical bytes → identical version id regardless of when they were built,\n * so republishing unchanged content is a pointer no-op. Any changed\n * coordinate → a new version (and a new file).\n */\nexport function composeVersionManifest(input: VersionManifestInput): WorkspaceVersionManifest {\n const { builtAt: _builtAt, ...hashedInputs } = input;\n const version = createHash(\"sha256\")\n .update(stableStringify(hashedInputs))\n .digest(\"hex\")\n .slice(0, 16);\n return WorkspaceVersionManifestSchema.parse({ ...input, version });\n}\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n if (value && typeof value === \"object\") {\n return `{${Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n .sort()\n .join(\",\")}}`;\n }\n return JSON.stringify(value) ?? \"null\";\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,MAAM,MAAMA,MAAE,OAAO,CAAC,CAAC,MAAM,0BAA0B;AAEvD,MAAa,iCAAiCA,MAAE,OAAO;CACrD,SAASA,MAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB;CAC1C,SAASA,MAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,QAAQA,MAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,OAAOA,MAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,gBAAgB;CAChB,KAAKA,MACF,OAAO;EACN,OAAOA,MAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACvB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;CACZ,iBAAiBA,MACd,OAAO;EACN,MAAMA,MAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACtB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;;CAEZ,QAAQA,MAAE,OAAOA,MAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;;CAE3C,QAAQA,MAAE,OAAOA,MAAE,OAAO,GAAGA,MAAE,OAAO,CAAC,CAAC,CAAC,SAAS;;;;;;;CAOlD,OAAOA,MAAE,OAAOA,MAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;AAC5C,CAAC;;;;;;;;AAyBD,SAAgB,uBAAuB,OAAuD;CAC5F,MAAM,EAAE,SAAS,UAAU,GAAG,iBAAiB;CAC/C,MAAM,cAAUC,wBAAW,QAAQ,CAAC,CACjC,OAAO,gBAAgB,YAAY,CAAC,CAAC,CACrC,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EAAE;CACd,OAAO,+BAA+B,MAAM;EAAE,GAAG;EAAO;CAAQ,CAAC;AACnE;AAEA,SAAS,gBAAgB,OAAwB;CAC/C,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,IAAI,MAAM,IAAI,eAAe,CAAC,CAAC,KAAK,GAAG,EAAE;CAC1E,IAAI,SAAS,OAAO,UAAU,UAC5B,OAAO,IAAI,OAAO,QAAQ,KAAK,CAAC,CAC7B,QAAQ,GAAG,OAAO,MAAM,MAAS,CAAC,CAClC,KAAK,CAAC,GAAG,OAAO,GAAG,KAAK,UAAU,CAAC,EAAE,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAC7D,KAAK,CAAC,CACN,KAAK,GAAG,EAAE;CAEf,OAAO,KAAK,UAAU,KAAK,KAAK;AAClC"}
|
|
@@ -45,7 +45,7 @@ const WorkspaceVersionManifestSchema = z.object({
|
|
|
45
45
|
* coordinate → a new version (and a new file).
|
|
46
46
|
*/
|
|
47
47
|
function composeVersionManifest(input) {
|
|
48
|
-
const { builtAt, ...hashedInputs } = input;
|
|
48
|
+
const { builtAt: _builtAt, ...hashedInputs } = input;
|
|
49
49
|
const version = createHash("sha256").update(stableStringify(hashedInputs)).digest("hex").slice(0, 16);
|
|
50
50
|
return WorkspaceVersionManifestSchema.parse({
|
|
51
51
|
...input,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-manifest.mjs","names":[],"sources":["../src/version-manifest.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\n\n/**\n * WorkspaceVersionManifest — the immutable per-deploy document at\n * `bundles/<account>/<gateway>/<workspace>/versions/<version>.json`\n * (ADR 0020, as amended; atomic-deploys MAP decision 1). The published\n * config's slot pins this document via its `pin` (`manifest` + the\n * manifest's own SRI); every artifact coordinate lives here, so the config\n * stops growing with artifact kinds and old versions stay fully servable.\n */\n\nconst SRI = z.string().regex(/^sha384-[A-Za-z0-9+/=]+$/);\n\nexport const WorkspaceVersionManifestSchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n builtAt: z.string().optional(),\n gitSha: z.string().optional(),\n entry: z.string().min(1),\n entryIntegrity: SRI,\n ssr: z\n .object({\n entry: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n browserManifest: z\n .object({\n file: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n /** extra assets (css, icons) — logical name → served-object SRI */\n assets: z.record(z.string(), SRI).optional(),\n /** shared-dependency versions this build was built against */\n shared: z.record(z.string(), z.string()).optional(),\n /**\n * Full per-file digest map of the dist (object path → served-object SRI).\n * Written by `every-plugin`-based deploys so the next deploy can diff against\n * it and skip re-uploading unchanged files; readers that predate the field\n * ignore it (zod strips unknown keys). Participates in the version hash.\n */\n files: z.record(z.string(), SRI).optional(),\n});\nexport type WorkspaceVersionManifest = z.infer<typeof WorkspaceVersionManifestSchema>;\n\nexport interface VersionManifestInput {\n /** Omit: a per-build timestamp makes the same version id map to different\n * manifest bytes per machine, breaking the pin's SRI against the image's\n * staged copy and the CDN's immutability for republished versions. */\n builtAt?: string;\n gitSha?: string;\n entry: string;\n entryIntegrity: string;\n ssr?: { entry: string; integrity: string };\n browserManifest?: { file: string; integrity: string };\n assets?: Record<string, string>;\n shared?: Record<string, string>;\n files?: Record<string, string>;\n}\n\n/**\n * Compose the manifest with its `version` derived from the canonical content\n * of the artifact coordinates alone (key-sorted, `builtAt` excluded) —\n * identical bytes → identical version id regardless of when they were built,\n * so republishing unchanged content is a pointer no-op. Any changed\n * coordinate → a new version (and a new file).\n */\nexport function composeVersionManifest(input: VersionManifestInput): WorkspaceVersionManifest {\n const { builtAt, ...hashedInputs } = input;\n const version = createHash(\"sha256\")\n .update(stableStringify(hashedInputs))\n .digest(\"hex\")\n .slice(0, 16);\n return WorkspaceVersionManifestSchema.parse({ ...input, version });\n}\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n if (value && typeof value === \"object\") {\n return `{${Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n .sort()\n .join(\",\")}}`;\n }\n return JSON.stringify(value) ?? \"null\";\n}\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,0BAA0B;AAEvD,MAAa,iCAAiC,EAAE,OAAO;CACrD,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB;CAC1C,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,gBAAgB;CAChB,KAAK,EACF,OAAO;EACN,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACvB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;CACZ,iBAAiB,EACd,OAAO;EACN,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACtB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;;CAEZ,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;;CAE3C,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS;;;;;;;CAOlD,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;AAC5C,CAAC;;;;;;;;AAyBD,SAAgB,uBAAuB,OAAuD;CAC5F,MAAM,EAAE,SAAS,GAAG,iBAAiB;
|
|
1
|
+
{"version":3,"file":"version-manifest.mjs","names":[],"sources":["../src/version-manifest.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\n\n/**\n * WorkspaceVersionManifest — the immutable per-deploy document at\n * `bundles/<account>/<gateway>/<workspace>/versions/<version>.json`\n * (ADR 0020, as amended; atomic-deploys MAP decision 1). The published\n * config's slot pins this document via its `pin` (`manifest` + the\n * manifest's own SRI); every artifact coordinate lives here, so the config\n * stops growing with artifact kinds and old versions stay fully servable.\n */\n\nconst SRI = z.string().regex(/^sha384-[A-Za-z0-9+/=]+$/);\n\nexport const WorkspaceVersionManifestSchema = z.object({\n version: z.string().regex(/^[0-9a-f]{16}$/),\n builtAt: z.string().optional(),\n gitSha: z.string().optional(),\n entry: z.string().min(1),\n entryIntegrity: SRI,\n ssr: z\n .object({\n entry: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n browserManifest: z\n .object({\n file: z.string().min(1),\n integrity: SRI,\n })\n .optional(),\n /** extra assets (css, icons) — logical name → served-object SRI */\n assets: z.record(z.string(), SRI).optional(),\n /** shared-dependency versions this build was built against */\n shared: z.record(z.string(), z.string()).optional(),\n /**\n * Full per-file digest map of the dist (object path → served-object SRI).\n * Written by `every-plugin`-based deploys so the next deploy can diff against\n * it and skip re-uploading unchanged files; readers that predate the field\n * ignore it (zod strips unknown keys). Participates in the version hash.\n */\n files: z.record(z.string(), SRI).optional(),\n});\nexport type WorkspaceVersionManifest = z.infer<typeof WorkspaceVersionManifestSchema>;\n\nexport interface VersionManifestInput {\n /** Omit: a per-build timestamp makes the same version id map to different\n * manifest bytes per machine, breaking the pin's SRI against the image's\n * staged copy and the CDN's immutability for republished versions. */\n builtAt?: string;\n gitSha?: string;\n entry: string;\n entryIntegrity: string;\n ssr?: { entry: string; integrity: string };\n browserManifest?: { file: string; integrity: string };\n assets?: Record<string, string>;\n shared?: Record<string, string>;\n files?: Record<string, string>;\n}\n\n/**\n * Compose the manifest with its `version` derived from the canonical content\n * of the artifact coordinates alone (key-sorted, `builtAt` excluded) —\n * identical bytes → identical version id regardless of when they were built,\n * so republishing unchanged content is a pointer no-op. Any changed\n * coordinate → a new version (and a new file).\n */\nexport function composeVersionManifest(input: VersionManifestInput): WorkspaceVersionManifest {\n const { builtAt: _builtAt, ...hashedInputs } = input;\n const version = createHash(\"sha256\")\n .update(stableStringify(hashedInputs))\n .digest(\"hex\")\n .slice(0, 16);\n return WorkspaceVersionManifestSchema.parse({ ...input, version });\n}\n\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n if (value && typeof value === \"object\") {\n return `{${Object.entries(value)\n .filter(([, v]) => v !== undefined)\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n .sort()\n .join(\",\")}}`;\n }\n return JSON.stringify(value) ?? \"null\";\n}\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,0BAA0B;AAEvD,MAAa,iCAAiC,EAAE,OAAO;CACrD,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,gBAAgB;CAC1C,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,gBAAgB;CAChB,KAAK,EACF,OAAO;EACN,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACvB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;CACZ,iBAAiB,EACd,OAAO;EACN,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EACtB,WAAW;CACb,CAAC,CAAC,CACD,SAAS;;CAEZ,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;;CAE3C,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS;;;;;;;CAOlD,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC,SAAS;AAC5C,CAAC;;;;;;;;AAyBD,SAAgB,uBAAuB,OAAuD;CAC5F,MAAM,EAAE,SAAS,UAAU,GAAG,iBAAiB;CAC/C,MAAM,UAAU,WAAW,QAAQ,CAAC,CACjC,OAAO,gBAAgB,YAAY,CAAC,CAAC,CACrC,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EAAE;CACd,OAAO,+BAA+B,MAAM;EAAE,GAAG;EAAO;CAAQ,CAAC;AACnE;AAEA,SAAS,gBAAgB,OAAwB;CAC/C,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO,IAAI,MAAM,IAAI,eAAe,CAAC,CAAC,KAAK,GAAG,EAAE;CAC1E,IAAI,SAAS,OAAO,UAAU,UAC5B,OAAO,IAAI,OAAO,QAAQ,KAAK,CAAC,CAC7B,QAAQ,GAAG,OAAO,MAAM,MAAS,CAAC,CAClC,KAAK,CAAC,GAAG,OAAO,GAAG,KAAK,UAAU,CAAC,EAAE,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAC7D,KAAK,CAAC,CACN,KAAK,GAAG,EAAE;CAEf,OAAO,KAAK,UAAU,KAAK,KAAK;AAClC"}
|
package/package.json
CHANGED
package/src/errors.ts
CHANGED
|
@@ -125,9 +125,10 @@ export const PLUGIN_ERROR_STATUS_MAP: Record<string, number> = {
|
|
|
125
125
|
CONNECTION_ERROR: 502,
|
|
126
126
|
} as const;
|
|
127
127
|
|
|
128
|
-
export type { PluginLoadFailureInfo } from "./runtime/errors";
|
|
128
|
+
export type { PluginLoadFailureInfo, PluginWarningInfo } from "./runtime/errors";
|
|
129
129
|
export {
|
|
130
130
|
classifyPluginFailure,
|
|
131
|
+
defaultMissingSecretWarnings,
|
|
131
132
|
extractFromFiberFailure,
|
|
132
133
|
formatORPCError,
|
|
133
134
|
ModuleFederationError,
|
package/src/plugin.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ContractedRouter, Implementer } from "@orpc/server";
|
|
|
5
5
|
import { implement } from "@orpc/server";
|
|
6
6
|
import type { Scope } from "effect";
|
|
7
7
|
import { Context, Effect, Layer } from "effect";
|
|
8
|
+
import type { PluginWarningInfo } from "./runtime/errors";
|
|
8
9
|
import { type PluginRuntimeError, toPluginRuntimeError } from "./runtime/errors";
|
|
9
10
|
|
|
10
11
|
type ContextOutput<T> = T extends AnySchema ? InferSchemaOutput<T> : Record<string, never>;
|
|
@@ -19,11 +20,20 @@ export type PluginConfigFor<
|
|
|
19
20
|
context: TRequestContext;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
|
|
23
|
+
export type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
|
|
23
24
|
variables: InferSchemaOutput<V>;
|
|
24
25
|
secrets: InferSchemaOutput<S>;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A plugin's own config-derived warnings, evaluated once with the validated
|
|
30
|
+
* config after initialization. Declaring `warnings` opts the plugin out of
|
|
31
|
+
* the loader's default missing-secret warning — these messages replace it.
|
|
32
|
+
*/
|
|
33
|
+
export type PluginWarningsFn<V extends AnySchema, S extends AnySchema> = (
|
|
34
|
+
config: PluginInitializeInput<V, S>,
|
|
35
|
+
) => PluginWarningInfo[] | string[];
|
|
36
|
+
|
|
27
37
|
export class PluginIdTag extends Context.Service<PluginIdTag, string>()("PluginId") {}
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -50,6 +60,13 @@ type PluginDefinition<
|
|
|
50
60
|
* module statically.
|
|
51
61
|
*/
|
|
52
62
|
servicesTag?: Context.Key<any, any>;
|
|
63
|
+
/**
|
|
64
|
+
* Optional warnings function evaluated once with the validated config after
|
|
65
|
+
* initialization. Returns structured non-fatal warnings (e.g. an optional
|
|
66
|
+
* secret was not set, so the feature depending on it is disabled).
|
|
67
|
+
* Declaring it opts out of the loader's default missing-secret warning.
|
|
68
|
+
*/
|
|
69
|
+
warnings?: PluginWarningsFn<V, S>;
|
|
53
70
|
/**
|
|
54
71
|
* Initialize the plugin by returning an Effect `Layer`.
|
|
55
72
|
*
|
|
@@ -107,6 +124,7 @@ export interface Plugin<
|
|
|
107
124
|
readonly contract: TContract;
|
|
108
125
|
readonly configSchema: PluginConfigFor<TVariables, TSecrets, TRequestContext>;
|
|
109
126
|
readonly servicesTag?: Context.Key<any, any>;
|
|
127
|
+
readonly warnings?: PluginWarningsFn<TVariables, TSecrets>;
|
|
110
128
|
|
|
111
129
|
initialize(
|
|
112
130
|
config: PluginInitializeInput<TVariables, TSecrets>,
|
|
@@ -156,6 +174,7 @@ export const createPlugin: CreatePluginFn = function createPlugin<
|
|
|
156
174
|
readonly contract = config.contract;
|
|
157
175
|
readonly configSchema = configSchema;
|
|
158
176
|
readonly servicesTag = config.servicesTag;
|
|
177
|
+
readonly warnings = config.warnings;
|
|
159
178
|
|
|
160
179
|
initialize(
|
|
161
180
|
pluginConfig: PluginInitializeInput<V, S>,
|
package/src/runtime/errors.ts
CHANGED
|
@@ -282,6 +282,50 @@ export interface PluginLoadFailureInfo {
|
|
|
282
282
|
dbUrlMasked?: string;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Structured, serializable warning surfaced alongside plugin failures in
|
|
287
|
+
* health endpoints and dashboards. Warnings are non-fatal: the plugin loaded
|
|
288
|
+
* and initialized successfully, but runs with reduced functionality (e.g. an
|
|
289
|
+
* optional secret was not set, so the feature depending on it is disabled).
|
|
290
|
+
*/
|
|
291
|
+
export interface PluginWarningInfo {
|
|
292
|
+
/** loader-side key (host entry key, dev plugin id) */
|
|
293
|
+
pluginKey: string;
|
|
294
|
+
message: string;
|
|
295
|
+
hint?: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The loader's default warning policy for declared-but-unset secrets: a
|
|
300
|
+
* successfully initialized plugin whose declared secret names resolved to
|
|
301
|
+
* nothing (missing or empty in the environment) gets one consolidated
|
|
302
|
+
* warning. `*_DATABASE_URL` keys are excluded — they have the shared-API-DB
|
|
303
|
+
* fallback, and a genuinely-missing DB URL fails initialization (a `failure`,
|
|
304
|
+
* not a warning). A plugin that declares its own `warnings` function opts out
|
|
305
|
+
* of this default.
|
|
306
|
+
*/
|
|
307
|
+
export const defaultMissingSecretWarnings = (
|
|
308
|
+
pluginKey: string,
|
|
309
|
+
declaredKeys: readonly string[],
|
|
310
|
+
resolvedKeys: readonly string[],
|
|
311
|
+
): PluginWarningInfo[] => {
|
|
312
|
+
const resolved = new Set(resolvedKeys);
|
|
313
|
+
const missing = declaredKeys.filter(
|
|
314
|
+
(key) => !resolved.has(key) && !key.endsWith("_DATABASE_URL"),
|
|
315
|
+
);
|
|
316
|
+
if (missing.length === 0) return [];
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
pluginKey,
|
|
320
|
+
message:
|
|
321
|
+
missing.length === 1
|
|
322
|
+
? `Declared secret ${missing[0]} is not set — features depending on it are disabled`
|
|
323
|
+
: `${missing.length} declared secrets are not set: ${missing.join(", ")} — features depending on them are disabled`,
|
|
324
|
+
hint: "Set the secret(s) in your environment (.env in development); see the plugin's config schema for what each enables",
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
};
|
|
328
|
+
|
|
285
329
|
const NETWORK_RETRYABLE_PATTERNS = ["ETIMEDOUT", "ECONNRESET", "timeout", "503", "429"] as const;
|
|
286
330
|
const NETWORK_DEAD_PATTERNS = ["ECONNREFUSED", "ENOTFOUND", "EHOSTUNREACH"] as const;
|
|
287
331
|
const RETRYABLE_ORPC_CODES = new Set([
|
|
@@ -111,13 +111,10 @@ const resolveRemoteSelfName = (pluginId: string, url: string) =>
|
|
|
111
111
|
const memo = remoteSelfNames.get(memoKey);
|
|
112
112
|
if (memo) return memo;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} catch {
|
|
119
|
-
selfName = null;
|
|
120
|
-
}
|
|
114
|
+
const manifest = yield* Effect.tryPromise(() => fetchRemoteIdentityManifest(url)).pipe(
|
|
115
|
+
Effect.orElseSucceed(() => null),
|
|
116
|
+
);
|
|
117
|
+
const selfName = manifest?.metaData?.name?.trim() || null;
|
|
121
118
|
|
|
122
119
|
const resolved = selfName ?? getNormalizedRemoteName(pluginId);
|
|
123
120
|
remoteSelfNames.set(memoKey, resolved);
|
|
@@ -11,11 +11,39 @@ import type {
|
|
|
11
11
|
PluginMetadata,
|
|
12
12
|
PluginRegistry,
|
|
13
13
|
} from "../../types";
|
|
14
|
+
import type { PluginWarningInfo } from "../errors";
|
|
14
15
|
import { PluginRuntimeError, toPluginRuntimeError } from "../errors";
|
|
15
16
|
import { validate } from "../validation";
|
|
16
17
|
import { ModuleFederationService } from "./module-federation.service";
|
|
17
18
|
import { SecretsService } from "./secrets.service";
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate a plugin's `warnings` function against the validated config.
|
|
22
|
+
* Fail-safe: a throwing warnings function becomes a warning itself rather
|
|
23
|
+
* than failing initialization. Strings are normalized to `PluginWarningInfo`
|
|
24
|
+
* with the plugin's registry key.
|
|
25
|
+
*/
|
|
26
|
+
const collectPluginWarnings = (
|
|
27
|
+
plugin: AnyPlugin,
|
|
28
|
+
config: { variables: unknown; secrets: unknown },
|
|
29
|
+
): PluginWarningInfo[] => {
|
|
30
|
+
const warningsFn = plugin.warnings;
|
|
31
|
+
if (!warningsFn) return [];
|
|
32
|
+
try {
|
|
33
|
+
const raw = warningsFn(config as never);
|
|
34
|
+
return raw.map((warning) =>
|
|
35
|
+
typeof warning === "string" ? { pluginKey: plugin.id, message: warning } : warning,
|
|
36
|
+
);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
pluginKey: plugin.id,
|
|
41
|
+
message: `Plugin warnings check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
19
47
|
export class PluginRegistryTag extends Context.Service<PluginRegistryTag, PluginRegistry>()(
|
|
20
48
|
"PluginRegistry",
|
|
21
49
|
) {}
|
|
@@ -313,6 +341,10 @@ export const PluginLoaderServiceDefault = Layer.effect(
|
|
|
313
341
|
config: { variables: _variables, secrets: hydratedConfig.secrets },
|
|
314
342
|
effectContext,
|
|
315
343
|
scope,
|
|
344
|
+
warnings: collectPluginWarnings(plugin, {
|
|
345
|
+
variables: _variables,
|
|
346
|
+
secrets: hydratedConfig.secrets,
|
|
347
|
+
}),
|
|
316
348
|
} satisfies InitializedPlugin<T>;
|
|
317
349
|
}),
|
|
318
350
|
};
|
|
@@ -19,7 +19,7 @@ function manifestUrl(remoteUrl: string): string {
|
|
|
19
19
|
|
|
20
20
|
function normalizeBaseUrl(remoteUrl: string): string {
|
|
21
21
|
const withoutHash = remoteUrl.split("#")[0] ?? remoteUrl;
|
|
22
|
-
return withoutHash.replace(/\/(mf-manifest\.json|remoteEntry
|
|
22
|
+
return withoutHash.replace(/\/(mf-manifest\.json|remoteEntry(?:\.[^/]+)?\.js)$/i, "");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function compareSharedIdentity(
|
package/src/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import type { ContractedRouter, RouterClient } from "@orpc/server";
|
|
8
8
|
import type { Context, Scope } from "effect";
|
|
9
9
|
import type { Plugin } from "./plugin";
|
|
10
|
+
import type { PluginWarningInfo } from "./runtime/errors";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Registry bindings interface - populated via module augmentation
|
|
@@ -204,6 +205,8 @@ export interface InitializedPlugin<T extends AnyPlugin = AnyPlugin> {
|
|
|
204
205
|
*/
|
|
205
206
|
readonly effectContext: Context.Context<any>;
|
|
206
207
|
readonly scope: Scope.Closeable;
|
|
208
|
+
/** Non-fatal config warnings collected during initialization (empty when none). */
|
|
209
|
+
readonly warnings: PluginWarningInfo[];
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
/**
|
package/src/version-manifest.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface VersionManifestInput {
|
|
|
67
67
|
* coordinate → a new version (and a new file).
|
|
68
68
|
*/
|
|
69
69
|
export function composeVersionManifest(input: VersionManifestInput): WorkspaceVersionManifest {
|
|
70
|
-
const { builtAt, ...hashedInputs } = input;
|
|
70
|
+
const { builtAt: _builtAt, ...hashedInputs } = input;
|
|
71
71
|
const version = createHash("sha256")
|
|
72
72
|
.update(stableStringify(hashedInputs))
|
|
73
73
|
.digest("hex")
|