@codemation/host 0.0.15 → 0.0.18

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 (85) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +37 -0
  3. package/dist/{AppConfigFactory-DmHOpg8O.d.ts → AppConfigFactory-Co4STjwt.d.ts} +5 -3
  4. package/dist/{AppConfigFactory-DWIz2hy-.js → AppConfigFactory-DxoZ4v8r.js} +40 -3
  5. package/dist/AppConfigFactory-DxoZ4v8r.js.map +1 -0
  6. package/dist/{AppContainerFactory-B5eRpvAa.js → AppContainerFactory-z9aUDFiJ.js} +275 -105
  7. package/dist/AppContainerFactory-z9aUDFiJ.js.map +1 -0
  8. package/dist/{CodemationConfig-D2ULNkec.d.ts → CodemationConfig-COs4GcOE.d.ts} +18 -10
  9. package/dist/{CodemationConfigNormalizer-CBLxXaRV.d.ts → CodemationConfigNormalizer-B7w1JA_2.d.ts} +2 -2
  10. package/dist/{CodemationConsumerConfigLoader-BBzAr6L_.js → CodemationConsumerConfigLoader-C_ISRrpI.js} +28 -7
  11. package/dist/CodemationConsumerConfigLoader-C_ISRrpI.js.map +1 -0
  12. package/dist/{CodemationConsumerConfigLoader-BLvzcfb7.d.ts → CodemationConsumerConfigLoader-OlXKw-us.d.ts} +6 -2
  13. package/dist/CodemationPluginListMerger-CGwOTdZ7.js +57 -0
  14. package/dist/CodemationPluginListMerger-CGwOTdZ7.js.map +1 -0
  15. package/dist/{CodemationPluginListMerger-B0-e4CJ6.d.ts → CodemationPluginListMerger-_ZIiOQxB.d.ts} +79 -117
  16. package/dist/{CredentialServices-BeuMtqYA.d.ts → CredentialServices-D3VTczpC.d.ts} +3 -3
  17. package/dist/PublicFrontendBootstrap-p7mS8aWG.d.ts +50 -0
  18. package/dist/{FrontendAppConfigFactory-Bj-DZNlt.d.ts → PublicFrontendBootstrapFactory-CE4oGogq.d.ts} +19 -4
  19. package/dist/{FrontendAppConfigJsonCodec-nOCQI0ag.js → PublicFrontendBootstrapJsonCodec-BdiVGG5R.js} +76 -2
  20. package/dist/PublicFrontendBootstrapJsonCodec-BdiVGG5R.js.map +1 -0
  21. package/dist/PublicFrontendBootstrapJsonCodec-DjkkKXcq.d.ts +35 -0
  22. package/dist/client.d.ts +3 -3
  23. package/dist/client.js +2 -2
  24. package/dist/consumer.d.ts +4 -4
  25. package/dist/consumer.js +1 -1
  26. package/dist/credentials.d.ts +3 -3
  27. package/dist/devServerSidecar.d.ts +1 -1
  28. package/dist/{index-CkiptHb-.d.ts → index-DbYzycTC.d.ts} +215 -71
  29. package/dist/index.d.ts +150 -13
  30. package/dist/index.js +94 -8
  31. package/dist/index.js.map +1 -0
  32. package/dist/nextServer.d.ts +30 -9
  33. package/dist/nextServer.js +4 -4
  34. package/dist/{persistenceServer-DVeWUbc3.js → persistenceServer-C4L1uMKn.js} +2 -2
  35. package/dist/{persistenceServer-DVeWUbc3.js.map → persistenceServer-C4L1uMKn.js.map} +1 -1
  36. package/dist/{persistenceServer-CaehMh3M.d.ts → persistenceServer-Cr-zCuEr.d.ts} +2 -2
  37. package/dist/persistenceServer.d.ts +5 -5
  38. package/dist/persistenceServer.js +2 -2
  39. package/dist/{server-Dyo8qh4k.d.ts → server-B6k53aZj.d.ts} +14 -19
  40. package/dist/{server-C65z_kqm.js → server-DDVXr7BN.js} +42 -25
  41. package/dist/server-DDVXr7BN.js.map +1 -0
  42. package/dist/server.d.ts +11 -11
  43. package/dist/server.js +7 -7
  44. package/package.json +9 -6
  45. package/playwright.scaffolded-dev.config.ts +46 -0
  46. package/src/application/dev/BootRuntimeSummary.types.ts +2 -0
  47. package/src/application/dev/DevBootstrapSummaryAssembler.ts +1 -0
  48. package/src/application/dev/DevBootstrapSummaryJson.types.ts +6 -0
  49. package/src/bootstrap/AppContainerFactory.ts +101 -6
  50. package/src/bootstrap/runtime/AppConfigFactory.ts +22 -2
  51. package/src/client.ts +4 -0
  52. package/src/index.ts +21 -5
  53. package/src/infrastructure/config/CodemationPluginRegistrar.ts +6 -0
  54. package/src/infrastructure/di/HandlesCommandRegistry.ts +1 -10
  55. package/src/infrastructure/di/HandlesQueryRegistry.ts +1 -10
  56. package/src/nextServer.ts +4 -0
  57. package/src/presentation/config/AppConfig.ts +6 -0
  58. package/src/presentation/config/CodemationAuthoring.types.ts +169 -0
  59. package/src/presentation/config/CodemationPackageManifest.ts +1 -7
  60. package/src/presentation/config/CodemationPlugin.ts +45 -8
  61. package/src/presentation/config/CodemationPluginListMerger.ts +30 -26
  62. package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -0
  63. package/src/presentation/frontend/InternalAuthBootstrap.ts +12 -0
  64. package/src/presentation/frontend/InternalAuthBootstrapFactory.ts +26 -0
  65. package/src/presentation/frontend/InternalAuthBootstrapJsonCodec.ts +49 -0
  66. package/src/presentation/frontend/PublicFrontendBootstrap.ts +12 -0
  67. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +23 -0
  68. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +48 -0
  69. package/src/presentation/http/ApiPaths.ts +10 -0
  70. package/src/presentation/http/hono/HonoHttpAnonymousRoutePolicyRegistry.ts +3 -0
  71. package/src/presentation/http/hono/registrars/BootstrapHonoApiRouteRegistrar.ts +32 -0
  72. package/src/presentation/http/routeHandlers/InternalAuthBootstrapHttpRouteHandler.ts +18 -0
  73. package/src/presentation/http/routeHandlers/PublicFrontendBootstrapHttpRouteHandler.ts +18 -0
  74. package/src/presentation/server/CodemationConsumerConfigLoader.ts +28 -6
  75. package/src/presentation/server/CodemationPluginDiscovery.ts +59 -28
  76. package/src/server.ts +6 -0
  77. package/dist/AppConfigFactory-DWIz2hy-.js.map +0 -1
  78. package/dist/AppContainerFactory-B5eRpvAa.js.map +0 -1
  79. package/dist/CodemationConsumerConfigLoader-BBzAr6L_.js.map +0 -1
  80. package/dist/CodemationPluginListMerger-DrVOw9KP.js +0 -57
  81. package/dist/CodemationPluginListMerger-DrVOw9KP.js.map +0 -1
  82. package/dist/FrontendAppConfig-D50wjj_n.d.ts +0 -27
  83. package/dist/FrontendAppConfigJsonCodec-1_L7H_Qo.d.ts +0 -20
  84. package/dist/FrontendAppConfigJsonCodec-nOCQI0ag.js.map +0 -1
  85. package/dist/server-C65z_kqm.js.map +0 -1
@@ -1,46 +1,50 @@
1
- import type { CodemationPlugin } from "./CodemationPlugin";
1
+ import { CodemationPluginPackageMetadata, type CodemationPlugin } from "./CodemationPlugin";
2
2
 
3
3
  /**
4
4
  * Merges explicitly configured plugins with auto-discovered plugins.
5
5
  * Configured plugins are applied first; discovered plugins fill in gaps.
6
- * Plugins that declare `pluginPackageId` are deduped by that string so the same npm package is not
7
- * registered twice when the consumer config lists a plugin and discovery also finds it, or when two
8
- * module evaluations produce different `constructor` identities for the same logical plugin.
6
+ * Plugins discovered from package.json manifests are deduped by npm package name so the same package is not
7
+ * registered twice when the consumer config lists a discovered plugin explicitly and auto-discovery also finds it.
9
8
  */
10
9
  export class CodemationPluginListMerger {
10
+ constructor(private readonly packageMetadata: CodemationPluginPackageMetadata) {}
11
+
11
12
  merge(
12
13
  configuredPlugins: ReadonlyArray<CodemationPlugin>,
13
14
  discoveredPlugins: ReadonlyArray<CodemationPlugin>,
14
15
  ): ReadonlyArray<CodemationPlugin> {
15
16
  const pluginsByPackageId = new Map<string, CodemationPlugin>();
16
- const pluginsByConstructor = new Map<unknown, CodemationPlugin>();
17
+ const pluginsByReference = new Set<CodemationPlugin>();
17
18
  const result: CodemationPlugin[] = [];
18
19
 
19
- const tryAdd = (plugin: CodemationPlugin): void => {
20
- const packageId = plugin.pluginPackageId;
21
- if (typeof packageId === "string" && packageId.trim().length > 0) {
22
- const key = packageId.trim();
23
- if (pluginsByPackageId.has(key)) {
24
- return;
25
- }
26
- pluginsByPackageId.set(key, plugin);
27
- result.push(plugin);
28
- return;
29
- }
30
- const constructorKey = Object.getPrototypeOf(plugin)?.constructor ?? plugin;
31
- if (pluginsByConstructor.has(constructorKey)) {
32
- return;
33
- }
34
- pluginsByConstructor.set(constructorKey, plugin);
35
- result.push(plugin);
36
- };
37
-
38
20
  for (const plugin of configuredPlugins) {
39
- tryAdd(plugin);
21
+ this.tryAdd(plugin, pluginsByPackageId, pluginsByReference, result);
40
22
  }
41
23
  for (const plugin of discoveredPlugins) {
42
- tryAdd(plugin);
24
+ this.tryAdd(plugin, pluginsByPackageId, pluginsByReference, result);
43
25
  }
44
26
  return result;
45
27
  }
28
+
29
+ private tryAdd(
30
+ plugin: CodemationPlugin,
31
+ pluginsByPackageId: Map<string, CodemationPlugin>,
32
+ pluginsByReference: Set<CodemationPlugin>,
33
+ result: CodemationPlugin[],
34
+ ): void {
35
+ const packageId = this.packageMetadata.readPackageName(plugin);
36
+ if (packageId) {
37
+ if (pluginsByPackageId.has(packageId)) {
38
+ return;
39
+ }
40
+ pluginsByPackageId.set(packageId, plugin);
41
+ result.push(plugin);
42
+ return;
43
+ }
44
+ if (pluginsByReference.has(plugin)) {
45
+ return;
46
+ }
47
+ pluginsByReference.add(plugin);
48
+ result.push(plugin);
49
+ }
46
50
  }
@@ -39,6 +39,13 @@ export class CodemationFrontendAuthSnapshotFactory {
39
39
  }
40
40
 
41
41
  private resolveUiAuthEnabled(authConfig: CodemationAuthConfig | undefined, env: NodeJS.ProcessEnv): boolean {
42
+ const configuredOverride = env.CODEMATION_UI_AUTH_ENABLED?.trim().toLowerCase();
43
+ if (configuredOverride === "true") {
44
+ return true;
45
+ }
46
+ if (configuredOverride === "false") {
47
+ return false;
48
+ }
42
49
  return !(env.NODE_ENV !== "production" && authConfig?.allowUnauthenticatedInDevelopment === true);
43
50
  }
44
51
 
@@ -0,0 +1,12 @@
1
+ import type { CodemationAuthConfig } from "../config/CodemationAuthConfig";
2
+ import type { CodemationFrontendAuthProviderSnapshot } from "./CodemationFrontendAuthSnapshot";
3
+
4
+ /**
5
+ * Node-side runtime bootstrap for NextAuth provider wiring.
6
+ */
7
+ export type InternalAuthBootstrap = Readonly<{
8
+ authConfig: CodemationAuthConfig | undefined;
9
+ credentialsEnabled: boolean;
10
+ oauthProviders: ReadonlyArray<CodemationFrontendAuthProviderSnapshot>;
11
+ uiAuthEnabled: boolean;
12
+ }>;
@@ -0,0 +1,26 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+
3
+ import { ApplicationTokens } from "../../applicationTokens";
4
+ import type { AppConfig } from "../config/AppConfig";
5
+ import { CodemationFrontendAuthSnapshotFactory } from "./CodemationFrontendAuthSnapshotFactory";
6
+ import type { InternalAuthBootstrap } from "./InternalAuthBootstrap";
7
+
8
+ @injectable()
9
+ export class InternalAuthBootstrapFactory {
10
+ constructor(
11
+ @inject(ApplicationTokens.AppConfig)
12
+ private readonly appConfig: AppConfig,
13
+ @inject(CodemationFrontendAuthSnapshotFactory)
14
+ private readonly authSnapshotFactory: CodemationFrontendAuthSnapshotFactory,
15
+ ) {}
16
+
17
+ create(): InternalAuthBootstrap {
18
+ const authSnapshot = this.authSnapshotFactory.createFromAppConfig(this.appConfig);
19
+ return {
20
+ authConfig: this.appConfig.auth,
21
+ credentialsEnabled: authSnapshot.credentialsEnabled,
22
+ oauthProviders: authSnapshot.oauthProviders,
23
+ uiAuthEnabled: authSnapshot.uiAuthEnabled,
24
+ };
25
+ }
26
+ }
@@ -0,0 +1,49 @@
1
+ import type { CodemationAuthConfig } from "../config/CodemationAuthConfig";
2
+ import type { CodemationFrontendAuthProviderSnapshot } from "./CodemationFrontendAuthSnapshot";
3
+ import type { InternalAuthBootstrap } from "./InternalAuthBootstrap";
4
+
5
+ export class InternalAuthBootstrapJsonCodec {
6
+ serialize(bootstrap: InternalAuthBootstrap): string {
7
+ return JSON.stringify(bootstrap);
8
+ }
9
+
10
+ deserialize(serialized: string | undefined): InternalAuthBootstrap | null {
11
+ if (!serialized || serialized.trim().length === 0) {
12
+ return null;
13
+ }
14
+ try {
15
+ const parsed = JSON.parse(serialized) as Partial<InternalAuthBootstrap> | null;
16
+ if (!parsed || typeof parsed !== "object") {
17
+ return null;
18
+ }
19
+ return {
20
+ authConfig: this.resolveAuthConfig(parsed.authConfig),
21
+ credentialsEnabled: parsed.credentialsEnabled === true,
22
+ oauthProviders: this.resolveOauthProviders(parsed.oauthProviders),
23
+ uiAuthEnabled: parsed.uiAuthEnabled !== false,
24
+ };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ private resolveAuthConfig(value: unknown): CodemationAuthConfig | undefined {
31
+ return value && typeof value === "object" ? (value as CodemationAuthConfig) : undefined;
32
+ }
33
+
34
+ private resolveOauthProviders(value: unknown): ReadonlyArray<CodemationFrontendAuthProviderSnapshot> {
35
+ if (!Array.isArray(value)) {
36
+ return [];
37
+ }
38
+ return value.flatMap((entry) => {
39
+ if (!entry || typeof entry !== "object") {
40
+ return [];
41
+ }
42
+ const provider = entry as Partial<CodemationFrontendAuthProviderSnapshot>;
43
+ if (typeof provider.id !== "string" || typeof provider.name !== "string") {
44
+ return [];
45
+ }
46
+ return [{ id: provider.id, name: provider.name }];
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,12 @@
1
+ import type { CodemationFrontendAuthProviderSnapshot } from "./CodemationFrontendAuthSnapshot";
2
+
3
+ /**
4
+ * Frontend-safe runtime bootstrap consumed by the Next.js shell.
5
+ */
6
+ export type PublicFrontendBootstrap = Readonly<{
7
+ credentialsEnabled: boolean;
8
+ logoUrl: string | null;
9
+ oauthProviders: ReadonlyArray<CodemationFrontendAuthProviderSnapshot>;
10
+ productName: string;
11
+ uiAuthEnabled: boolean;
12
+ }>;
@@ -0,0 +1,23 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+
3
+ import { FrontendAppConfigFactory } from "./FrontendAppConfigFactory";
4
+ import type { PublicFrontendBootstrap } from "./PublicFrontendBootstrap";
5
+
6
+ @injectable()
7
+ export class PublicFrontendBootstrapFactory {
8
+ constructor(
9
+ @inject(FrontendAppConfigFactory)
10
+ private readonly frontendAppConfigFactory: FrontendAppConfigFactory,
11
+ ) {}
12
+
13
+ create(): PublicFrontendBootstrap {
14
+ const frontendAppConfig = this.frontendAppConfigFactory.create();
15
+ return {
16
+ credentialsEnabled: frontendAppConfig.auth.credentialsEnabled,
17
+ logoUrl: frontendAppConfig.logoUrl,
18
+ oauthProviders: frontendAppConfig.auth.oauthProviders,
19
+ productName: frontendAppConfig.productName,
20
+ uiAuthEnabled: frontendAppConfig.auth.uiAuthEnabled,
21
+ };
22
+ }
23
+ }
@@ -0,0 +1,48 @@
1
+ import type { PublicFrontendBootstrap } from "./PublicFrontendBootstrap";
2
+ import type { CodemationFrontendAuthProviderSnapshot } from "./CodemationFrontendAuthSnapshot";
3
+
4
+ export class PublicFrontendBootstrapJsonCodec {
5
+ serialize(bootstrap: PublicFrontendBootstrap): string {
6
+ return JSON.stringify(bootstrap);
7
+ }
8
+
9
+ deserialize(serialized: string | undefined): PublicFrontendBootstrap | null {
10
+ if (!serialized || serialized.trim().length === 0) {
11
+ return null;
12
+ }
13
+ try {
14
+ const parsed = JSON.parse(serialized) as Partial<PublicFrontendBootstrap> | null;
15
+ if (!parsed || typeof parsed !== "object") {
16
+ return null;
17
+ }
18
+ return {
19
+ credentialsEnabled: parsed.credentialsEnabled === true,
20
+ logoUrl: typeof parsed.logoUrl === "string" && parsed.logoUrl.trim().length > 0 ? parsed.logoUrl : null,
21
+ oauthProviders: this.resolveOauthProviders(parsed.oauthProviders),
22
+ productName:
23
+ typeof parsed.productName === "string" && parsed.productName.trim().length > 0
24
+ ? parsed.productName
25
+ : "Codemation",
26
+ uiAuthEnabled: parsed.uiAuthEnabled !== false,
27
+ };
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ private resolveOauthProviders(value: unknown): ReadonlyArray<CodemationFrontendAuthProviderSnapshot> {
34
+ if (!Array.isArray(value)) {
35
+ return [];
36
+ }
37
+ return value.flatMap((entry) => {
38
+ if (!entry || typeof entry !== "object") {
39
+ return [];
40
+ }
41
+ const provider = entry as Partial<CodemationFrontendAuthProviderSnapshot>;
42
+ if (typeof provider.id !== "string" || typeof provider.name !== "string") {
43
+ return [];
44
+ }
45
+ return [{ id: provider.id, name: provider.name }];
46
+ });
47
+ }
48
+ }
@@ -15,6 +15,8 @@ export class ApiPaths {
15
15
 
16
16
  private static readonly whitelabelBasePath = `${this.apiBasePath}/whitelabel`;
17
17
 
18
+ private static readonly bootstrapBasePath = `${this.apiBasePath}/bootstrap`;
19
+
18
20
  static workflows(): string {
19
21
  return this.workflowsBasePath;
20
22
  }
@@ -162,4 +164,12 @@ export class ApiPaths {
162
164
  static whitelabelLogo(): string {
163
165
  return `${this.whitelabelBasePath}/logo`;
164
166
  }
167
+
168
+ static frontendBootstrap(): string {
169
+ return `${this.bootstrapBasePath}/frontend`;
170
+ }
171
+
172
+ static internalAuthBootstrap(): string {
173
+ return `${this.bootstrapBasePath}/auth/internal`;
174
+ }
165
175
  }
@@ -16,6 +16,9 @@ export class HonoHttpAnonymousRoutePolicy {
16
16
  if (pathname === "/api/dev/runtime" || pathname === "/api/dev/bootstrap-summary") {
17
17
  return true;
18
18
  }
19
+ if (pathname === ApiPaths.frontendBootstrap() || pathname === ApiPaths.internalAuthBootstrap()) {
20
+ return true;
21
+ }
19
22
  if (pathname === ApiPaths.userInviteVerify() || pathname === ApiPaths.userInviteAccept()) {
20
23
  return true;
21
24
  }
@@ -0,0 +1,32 @@
1
+ import { inject, injectable, registry } from "@codemation/core";
2
+ import { Hono } from "hono";
3
+
4
+ import { ApplicationTokens } from "../../../../applicationTokens";
5
+ import { ApiPaths } from "../../ApiPaths";
6
+ import { InternalAuthBootstrapHttpRouteHandler } from "../../routeHandlers/InternalAuthBootstrapHttpRouteHandler";
7
+ import { PublicFrontendBootstrapHttpRouteHandler } from "../../routeHandlers/PublicFrontendBootstrapHttpRouteHandler";
8
+ import type { HonoApiRouteRegistrar } from "../HonoApiRouteRegistrar";
9
+
10
+ @injectable()
11
+ @registry([{ token: ApplicationTokens.HonoApiRouteRegistrar, useClass: BootstrapHonoApiRouteRegistrar }])
12
+ export class BootstrapHonoApiRouteRegistrar implements HonoApiRouteRegistrar {
13
+ constructor(
14
+ @inject(PublicFrontendBootstrapHttpRouteHandler)
15
+ private readonly publicFrontendBootstrapHttpRouteHandler: PublicFrontendBootstrapHttpRouteHandler,
16
+ @inject(InternalAuthBootstrapHttpRouteHandler)
17
+ private readonly internalAuthBootstrapHttpRouteHandler: InternalAuthBootstrapHttpRouteHandler,
18
+ ) {}
19
+
20
+ register(app: Hono): void {
21
+ app.get(this.resolveRelativePath(ApiPaths.frontendBootstrap()), () =>
22
+ this.publicFrontendBootstrapHttpRouteHandler.getBootstrap(),
23
+ );
24
+ app.get(this.resolveRelativePath(ApiPaths.internalAuthBootstrap()), () =>
25
+ this.internalAuthBootstrapHttpRouteHandler.getBootstrap(),
26
+ );
27
+ }
28
+
29
+ private resolveRelativePath(pathname: string): string {
30
+ return pathname.replace(/^\/api/, "");
31
+ }
32
+ }
@@ -0,0 +1,18 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+
3
+ import { InternalAuthBootstrapFactory } from "../../frontend/InternalAuthBootstrapFactory";
4
+
5
+ @injectable()
6
+ export class InternalAuthBootstrapHttpRouteHandler {
7
+ constructor(
8
+ @inject(InternalAuthBootstrapFactory)
9
+ private readonly bootstrapFactory: InternalAuthBootstrapFactory,
10
+ ) {}
11
+
12
+ getBootstrap(): Response {
13
+ return new Response(JSON.stringify(this.bootstrapFactory.create()), {
14
+ status: 200,
15
+ headers: { "content-type": "application/json" },
16
+ });
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+
3
+ import { PublicFrontendBootstrapFactory } from "../../frontend/PublicFrontendBootstrapFactory";
4
+
5
+ @injectable()
6
+ export class PublicFrontendBootstrapHttpRouteHandler {
7
+ constructor(
8
+ @inject(PublicFrontendBootstrapFactory)
9
+ private readonly bootstrapFactory: PublicFrontendBootstrapFactory,
10
+ ) {}
11
+
12
+ getBootstrap(): Response {
13
+ return new Response(JSON.stringify(this.bootstrapFactory.create()), {
14
+ status: 200,
15
+ headers: { "content-type": "application/json" },
16
+ });
17
+ }
18
+ }
@@ -3,7 +3,6 @@ import { access, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import type { NamespacedUnregister } from "tsx/esm/api";
6
- import { register } from "tsx/esm/api";
7
6
  import type { CodemationConfig } from "../config/CodemationConfig";
8
7
  import { CodemationConfigNormalizer } from "../config/CodemationConfigNormalizer";
9
8
  import type { NormalizedCodemationConfig } from "../config/CodemationConfigNormalizer";
@@ -23,6 +22,7 @@ export type CodemationConsumerConfigResolution = Readonly<{
23
22
 
24
23
  export class CodemationConsumerConfigLoader {
25
24
  private static readonly importerRegistrationsByTsconfig = new Map<string, NamespacedUnregister>();
25
+ private static readonly importerNamespaceVersionByTsconfig = new Map<string, number>();
26
26
  private readonly configExportsResolver = new CodemationConsumerConfigExportsResolver();
27
27
  private readonly configNormalizer = new CodemationConfigNormalizer();
28
28
  private readonly workflowModulePathFinder = new WorkflowModulePathFinder();
@@ -173,10 +173,15 @@ export class CodemationConsumerConfigLoader {
173
173
  return await this.importModuleWithNativeRuntime(modulePath);
174
174
  }
175
175
  const tsconfigPath = await this.resolveTsconfigPath(modulePath);
176
+ if (this.shouldResetImporterBeforeImport()) {
177
+ await this.resetImporter(tsconfigPath);
178
+ }
176
179
  const importSpecifier = await this.createImportSpecifier(modulePath);
177
180
  for (let attempt = 0; attempt < 3; attempt += 1) {
178
181
  try {
179
- const importedModule = await this.getOrCreateImporter(tsconfigPath).import(importSpecifier, import.meta.url);
182
+ const importedModule = await (
183
+ await this.getOrCreateImporter(tsconfigPath)
184
+ ).import(importSpecifier, import.meta.url);
180
185
  return importedModule as Record<string, unknown>;
181
186
  } catch (error) {
182
187
  if (!this.isStoppedTransformServiceError(error) || attempt === 2) {
@@ -202,14 +207,17 @@ export class CodemationConsumerConfigLoader {
202
207
  return discoveredPath ?? false;
203
208
  }
204
209
 
205
- private getOrCreateImporter(tsconfigPath: string | false): NamespacedUnregister {
210
+ private async getOrCreateImporter(tsconfigPath: string | false): Promise<NamespacedUnregister> {
206
211
  const cacheKey = tsconfigPath || "default";
207
212
  const existingImporter = CodemationConsumerConfigLoader.importerRegistrationsByTsconfig.get(cacheKey);
208
213
  if (existingImporter) {
209
214
  return existingImporter;
210
215
  }
216
+ const { register } = await import(/* webpackIgnore: true */ this.resolveTsxImporterModuleSpecifier());
217
+ const namespaceVersion = this.nextNamespaceVersion(cacheKey);
211
218
  const nextImporter = register({
212
- namespace: this.toNamespace(cacheKey),
219
+ // A fresh namespace prevents cache hits from prior scoped imports after a dev rebuild.
220
+ namespace: this.toNamespace(cacheKey, namespaceVersion),
213
221
  tsconfig: tsconfigPath,
214
222
  });
215
223
  CodemationConsumerConfigLoader.importerRegistrationsByTsconfig.set(cacheKey, nextImporter);
@@ -226,8 +234,18 @@ export class CodemationConsumerConfigLoader {
226
234
  await existingImporter.unregister().catch(() => null);
227
235
  }
228
236
 
229
- private toNamespace(cacheKey: string): string {
230
- return `codemation_consumer_${cacheKey.replace(/[^a-zA-Z0-9_-]+/g, "_")}`;
237
+ private nextNamespaceVersion(cacheKey: string): number {
238
+ const nextVersion = (CodemationConsumerConfigLoader.importerNamespaceVersionByTsconfig.get(cacheKey) ?? 0) + 1;
239
+ CodemationConsumerConfigLoader.importerNamespaceVersionByTsconfig.set(cacheKey, nextVersion);
240
+ return nextVersion;
241
+ }
242
+
243
+ private toNamespace(cacheKey: string, namespaceVersion: number): string {
244
+ return `codemation_consumer_${cacheKey.replace(/[^a-zA-Z0-9_-]+/g, "_")}_${namespaceVersion}`;
245
+ }
246
+
247
+ private resolveTsxImporterModuleSpecifier(): string {
248
+ return ["tsx", "esm", "api"].join("/");
231
249
  }
232
250
 
233
251
  private async findNearestTsconfig(modulePath: string): Promise<string | null> {
@@ -256,6 +274,10 @@ export class CodemationConsumerConfigLoader {
256
274
  return process.env.CODEMATION_TS_RUNTIME === "ts-node";
257
275
  }
258
276
 
277
+ private shouldResetImporterBeforeImport(): boolean {
278
+ return (process.env.CODEMATION_DEV_SERVER_TOKEN?.trim().length ?? 0) > 0;
279
+ }
280
+
259
281
  private isStoppedTransformServiceError(error: unknown): boolean {
260
282
  return error instanceof Error && error.message.includes("The service is no longer running");
261
283
  }
@@ -1,5 +1,5 @@
1
1
  import type { CodemationPackageManifest } from "../config/CodemationPackageManifest";
2
- import type { CodemationPlugin } from "../config/CodemationPlugin";
2
+ import { CodemationPluginPackageMetadata, type CodemationPlugin } from "../config/CodemationPlugin";
3
3
  import { readFile, readdir } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
@@ -7,7 +7,7 @@ import { pathToFileURL } from "node:url";
7
7
  export type CodemationDiscoveredPluginPackage = Readonly<{
8
8
  packageName: string;
9
9
  packageRoot: string;
10
- manifest: NonNullable<CodemationPackageManifest["plugin"]>;
10
+ pluginEntry: string;
11
11
  developmentEntry?: string;
12
12
  }>;
13
13
 
@@ -24,6 +24,8 @@ type PackageJsonShape = Readonly<{
24
24
  }>;
25
25
 
26
26
  export class CodemationPluginDiscovery {
27
+ private readonly pluginPackageMetadata = new CodemationPluginPackageMetadata();
28
+
27
29
  async discover(consumerRoot: string): Promise<ReadonlyArray<CodemationDiscoveredPluginPackage>> {
28
30
  const nodeModulesRoot = path.resolve(consumerRoot, "node_modules");
29
31
  const packageRoots = await this.collectPackageRoots(nodeModulesRoot);
@@ -31,14 +33,14 @@ export class CodemationPluginDiscovery {
31
33
  for (const packageRoot of packageRoots) {
32
34
  const packageJson = await this.readPackageJson(path.resolve(packageRoot, "package.json"));
33
35
  const pluginManifest = packageJson.codemation?.plugin;
34
- if (!packageJson.name || !pluginManifest || pluginManifest.kind !== "plugin") {
36
+ if (!packageJson.name || typeof pluginManifest !== "string" || pluginManifest.trim().length === 0) {
35
37
  continue;
36
38
  }
37
39
  discoveredPackages.push({
38
40
  packageName: packageJson.name,
39
41
  packageRoot,
40
- manifest: pluginManifest,
41
- developmentEntry: this.resolveDevelopmentPluginEntry(packageJson),
42
+ pluginEntry: pluginManifest,
43
+ developmentEntry: await this.resolveDevelopmentPluginEntry(packageRoot),
42
44
  });
43
45
  }
44
46
  return discoveredPackages.sort((left, right) => left.packageName.localeCompare(right.packageName));
@@ -98,37 +100,47 @@ export class CodemationPluginDiscovery {
98
100
 
99
101
  private async loadPlugin(discoveredPackage: CodemationDiscoveredPluginPackage): Promise<CodemationPlugin> {
100
102
  const pluginModulePath = path.resolve(discoveredPackage.packageRoot, this.resolvePluginEntry(discoveredPackage));
101
- const importedModule = (await import(pathToFileURL(pluginModulePath).href)) as Record<string, unknown>;
102
- const pluginExportName = discoveredPackage.manifest.exportName;
103
- const explicitExport = pluginExportName ? importedModule[pluginExportName] : undefined;
104
- const exportedValue = explicitExport ?? importedModule.default ?? importedModule.codemationPlugin;
103
+ const importedModule = (await import(
104
+ /* webpackIgnore: true */ this.resolvePluginModuleSpecifier(pluginModulePath)
105
+ )) as Record<string, unknown>;
106
+ const exportedValue = importedModule.default;
105
107
  const plugin = this.resolvePluginValue(exportedValue);
106
108
  if (!plugin) {
107
- throw new Error(`Plugin package "${discoveredPackage.packageName}" did not export a Codemation plugin instance.`);
109
+ throw new Error(`Plugin package "${discoveredPackage.packageName}" did not default-export a Codemation plugin.`);
108
110
  }
109
- return plugin;
111
+ return this.pluginPackageMetadata.attachPackageName(plugin, discoveredPackage.packageName);
110
112
  }
111
113
 
112
114
  private resolvePluginValue(value: unknown): CodemationPlugin | null {
113
- if (this.isPlugin(value)) {
115
+ if (this.isPluginConfig(value)) {
114
116
  return value;
115
117
  }
116
- if (this.isPluginConstructor(value)) {
117
- return new value();
118
- }
119
118
  return null;
120
119
  }
121
120
 
122
- private isPlugin(value: unknown): value is CodemationPlugin {
121
+ private isPluginConfig(value: unknown): value is CodemationPlugin {
122
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
123
+ return false;
124
+ }
125
+ const pluginValue = value as {
126
+ credentialTypes?: unknown;
127
+ register?: unknown;
128
+ sandbox?: unknown;
129
+ };
130
+ if (pluginValue.register !== undefined && typeof pluginValue.register !== "function") {
131
+ return false;
132
+ }
133
+ if (pluginValue.credentialTypes !== undefined && !Array.isArray(pluginValue.credentialTypes)) {
134
+ return false;
135
+ }
123
136
  return (
124
- Boolean(value) && typeof value === "object" && typeof (value as { register?: unknown }).register === "function"
137
+ pluginValue.register !== undefined ||
138
+ pluginValue.credentialTypes !== undefined ||
139
+ pluginValue.sandbox !== undefined ||
140
+ Object.keys(pluginValue).length === 0
125
141
  );
126
142
  }
127
143
 
128
- private isPluginConstructor(value: unknown): value is new () => CodemationPlugin {
129
- return typeof value === "function" && this.isPlugin(value.prototype);
130
- }
131
-
132
144
  private resolvePluginEntry(discoveredPackage: CodemationDiscoveredPluginPackage): string {
133
145
  if (
134
146
  process.env.CODEMATION_PREFER_PLUGIN_SOURCE_ENTRY === "true" &&
@@ -137,15 +149,34 @@ export class CodemationPluginDiscovery {
137
149
  ) {
138
150
  return discoveredPackage.developmentEntry;
139
151
  }
140
- return discoveredPackage.manifest.entry;
152
+ return discoveredPackage.pluginEntry;
141
153
  }
142
154
 
143
- private resolveDevelopmentPluginEntry(packageJson: PackageJsonShape): string | undefined {
144
- const exportRecord = packageJson.exports?.["./codemation-plugin"];
145
- if (!exportRecord || typeof exportRecord !== "object") {
146
- return undefined;
155
+ private async resolveDevelopmentPluginEntry(packageRoot: string): Promise<string | undefined> {
156
+ const candidates = [
157
+ path.resolve(packageRoot, "codemation.plugin.ts"),
158
+ path.resolve(packageRoot, "codemation.plugin.js"),
159
+ path.resolve(packageRoot, "src", "codemation.plugin.ts"),
160
+ path.resolve(packageRoot, "src", "codemation.plugin.js"),
161
+ ];
162
+ for (const candidate of candidates) {
163
+ if (await this.exists(candidate)) {
164
+ return path.relative(packageRoot, candidate);
165
+ }
147
166
  }
148
- const importPath = (exportRecord as { import?: unknown }).import;
149
- return typeof importPath === "string" && importPath.trim().length > 0 ? importPath : undefined;
167
+ return undefined;
168
+ }
169
+
170
+ private async exists(filePath: string): Promise<boolean> {
171
+ try {
172
+ await readFile(filePath, "utf8");
173
+ return true;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ private resolvePluginModuleSpecifier(pluginModulePath: string): string {
180
+ return pathToFileURL(pluginModulePath).href;
150
181
  }
151
182
  }
package/src/server.ts CHANGED
@@ -12,7 +12,13 @@ export { CodemationFrontendAuthSnapshotFactory } from "./presentation/frontend/C
12
12
  export { CodemationFrontendAuthSnapshotJsonCodec } from "./presentation/frontend/CodemationFrontendAuthSnapshotJsonCodec";
13
13
  export { FrontendAppConfigFactory } from "./presentation/frontend/FrontendAppConfigFactory";
14
14
  export { FrontendAppConfigJsonCodec } from "./presentation/frontend/FrontendAppConfigJsonCodec";
15
+ export { InternalAuthBootstrapFactory } from "./presentation/frontend/InternalAuthBootstrapFactory";
16
+ export { InternalAuthBootstrapJsonCodec } from "./presentation/frontend/InternalAuthBootstrapJsonCodec";
17
+ export { PublicFrontendBootstrapFactory } from "./presentation/frontend/PublicFrontendBootstrapFactory";
18
+ export { PublicFrontendBootstrapJsonCodec } from "./presentation/frontend/PublicFrontendBootstrapJsonCodec";
15
19
  export type { FrontendAppConfig } from "./presentation/frontend/FrontendAppConfig";
20
+ export type { InternalAuthBootstrap } from "./presentation/frontend/InternalAuthBootstrap";
21
+ export type { PublicFrontendBootstrap } from "./presentation/frontend/PublicFrontendBootstrap";
16
22
  export type {
17
23
  CodemationDiscoveredPluginPackage,
18
24
  CodemationResolvedPluginPackage,