@codemation/host 0.0.14 → 0.0.16

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 (77) hide show
  1. package/dist/{AppConfigFactory-DmHOpg8O.d.ts → AppConfigFactory-D_ReOKfV.d.ts} +5 -3
  2. package/dist/{AppConfigFactory-DWIz2hy-.js → AppConfigFactory-JjNqYnGg.js} +41 -3
  3. package/dist/AppConfigFactory-JjNqYnGg.js.map +1 -0
  4. package/dist/{AppContainerFactory-B5eRpvAa.js → AppContainerFactory-_dnF0VOf.js} +272 -105
  5. package/dist/AppContainerFactory-_dnF0VOf.js.map +1 -0
  6. package/dist/{CodemationConfig-D2ULNkec.d.ts → CodemationConfig-u80ZoIrc.d.ts} +18 -10
  7. package/dist/{CodemationConfigNormalizer-CBLxXaRV.d.ts → CodemationConfigNormalizer-DCt0mPPz.d.ts} +2 -2
  8. package/dist/{CodemationConsumerConfigLoader-BLvzcfb7.d.ts → CodemationConsumerConfigLoader-C8iscLBJ.d.ts} +4 -2
  9. package/dist/{CodemationConsumerConfigLoader-BBzAr6L_.js → CodemationConsumerConfigLoader-LGrCspIx.js} +18 -4
  10. package/dist/CodemationConsumerConfigLoader-LGrCspIx.js.map +1 -0
  11. package/dist/CodemationPluginListMerger-CGwOTdZ7.js +57 -0
  12. package/dist/CodemationPluginListMerger-CGwOTdZ7.js.map +1 -0
  13. package/dist/{CodemationPluginListMerger-B0-e4CJ6.d.ts → CodemationPluginListMerger-ZY3R_kTo.d.ts} +79 -117
  14. package/dist/{CredentialServices-BeuMtqYA.d.ts → CredentialServices-Uo1jxSYB.d.ts} +2 -2
  15. package/dist/PublicFrontendBootstrap-p7mS8aWG.d.ts +50 -0
  16. package/dist/{FrontendAppConfigFactory-Bj-DZNlt.d.ts → PublicFrontendBootstrapFactory-BA65IXL4.d.ts} +19 -4
  17. package/dist/{FrontendAppConfigJsonCodec-nOCQI0ag.js → PublicFrontendBootstrapJsonCodec-BdiVGG5R.js} +76 -2
  18. package/dist/PublicFrontendBootstrapJsonCodec-BdiVGG5R.js.map +1 -0
  19. package/dist/PublicFrontendBootstrapJsonCodec-DjkkKXcq.d.ts +35 -0
  20. package/dist/client.d.ts +3 -3
  21. package/dist/client.js +2 -2
  22. package/dist/consumer.d.ts +3 -3
  23. package/dist/consumer.js +1 -1
  24. package/dist/credentials.d.ts +2 -2
  25. package/dist/index.d.ts +44 -12
  26. package/dist/index.js +8 -8
  27. package/dist/nextServer.d.ts +30 -9
  28. package/dist/nextServer.js +4 -4
  29. package/dist/{persistenceServer-CaehMh3M.d.ts → persistenceServer-BKbOs-TQ.d.ts} +2 -2
  30. package/dist/{persistenceServer-DVeWUbc3.js → persistenceServer-J2teHIIg.js} +2 -2
  31. package/dist/{persistenceServer-DVeWUbc3.js.map → persistenceServer-J2teHIIg.js.map} +1 -1
  32. package/dist/persistenceServer.d.ts +4 -4
  33. package/dist/persistenceServer.js +2 -2
  34. package/dist/{server-C65z_kqm.js → server-CVC7QBwT.js} +42 -25
  35. package/dist/server-CVC7QBwT.js.map +1 -0
  36. package/dist/{server-Dyo8qh4k.d.ts → server-CbcnGZHm.d.ts} +14 -19
  37. package/dist/server.d.ts +10 -10
  38. package/dist/server.js +7 -7
  39. package/package.json +5 -5
  40. package/src/application/dev/BootRuntimeSummary.types.ts +2 -0
  41. package/src/application/dev/DevBootstrapSummaryAssembler.ts +1 -0
  42. package/src/application/dev/DevBootstrapSummaryJson.types.ts +6 -0
  43. package/src/bootstrap/AppContainerFactory.ts +101 -6
  44. package/src/bootstrap/runtime/AppConfigFactory.ts +22 -2
  45. package/src/client.ts +4 -0
  46. package/src/index.ts +13 -5
  47. package/src/infrastructure/config/CodemationPluginRegistrar.ts +6 -0
  48. package/src/infrastructure/di/HandlesCommandRegistry.ts +1 -10
  49. package/src/infrastructure/di/HandlesQueryRegistry.ts +1 -10
  50. package/src/nextServer.ts +4 -0
  51. package/src/presentation/config/AppConfig.ts +6 -0
  52. package/src/presentation/config/CodemationPackageManifest.ts +1 -7
  53. package/src/presentation/config/CodemationPlugin.ts +45 -9
  54. package/src/presentation/config/CodemationPluginListMerger.ts +30 -26
  55. package/src/presentation/frontend/InternalAuthBootstrap.ts +12 -0
  56. package/src/presentation/frontend/InternalAuthBootstrapFactory.ts +26 -0
  57. package/src/presentation/frontend/InternalAuthBootstrapJsonCodec.ts +49 -0
  58. package/src/presentation/frontend/PublicFrontendBootstrap.ts +12 -0
  59. package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +23 -0
  60. package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +48 -0
  61. package/src/presentation/http/ApiPaths.ts +10 -0
  62. package/src/presentation/http/hono/HonoHttpAnonymousRoutePolicyRegistry.ts +3 -0
  63. package/src/presentation/http/hono/registrars/BootstrapHonoApiRouteRegistrar.ts +32 -0
  64. package/src/presentation/http/routeHandlers/InternalAuthBootstrapHttpRouteHandler.ts +18 -0
  65. package/src/presentation/http/routeHandlers/PublicFrontendBootstrapHttpRouteHandler.ts +18 -0
  66. package/src/presentation/server/CodemationConsumerConfigLoader.ts +16 -3
  67. package/src/presentation/server/CodemationPluginDiscovery.ts +59 -28
  68. package/src/server.ts +6 -0
  69. package/dist/AppConfigFactory-DWIz2hy-.js.map +0 -1
  70. package/dist/AppContainerFactory-B5eRpvAa.js.map +0 -1
  71. package/dist/CodemationConsumerConfigLoader-BBzAr6L_.js.map +0 -1
  72. package/dist/CodemationPluginListMerger-DrVOw9KP.js +0 -57
  73. package/dist/CodemationPluginListMerger-DrVOw9KP.js.map +0 -1
  74. package/dist/FrontendAppConfig-D50wjj_n.d.ts +0 -27
  75. package/dist/FrontendAppConfigJsonCodec-1_L7H_Qo.d.ts +0 -20
  76. package/dist/FrontendAppConfigJsonCodec-nOCQI0ag.js.map +0 -1
  77. package/dist/server-C65z_kqm.js.map +0 -1
@@ -1,7 +1,8 @@
1
- import type { Container } from "@codemation/core";
1
+ import type { AnyCredentialType, Container } from "@codemation/core";
2
2
  import type { LoggerFactory } from "../../application/logging/Logger";
3
3
  import type { AppConfig } from "./AppConfig";
4
4
  import type { CodemationRegistrationContextBase } from "./CodemationAppContext";
5
+ import type { CodemationConfig } from "./CodemationConfig";
5
6
 
6
7
  export interface CodemationPluginContext extends CodemationRegistrationContextBase {
7
8
  readonly container: Container;
@@ -9,12 +10,47 @@ export interface CodemationPluginContext extends CodemationRegistrationContextBa
9
10
  readonly loggerFactory: LoggerFactory;
10
11
  }
11
12
 
12
- export interface CodemationPlugin {
13
- /**
14
- * Optional npm package name for this plugin (e.g. `"@codemation/core-nodes-gmail"`).
15
- * When set, the host merges configured and discovered plugin lists by this id so the same package is not
16
- * registered twice when separate module graphs load duplicate class instances (different `constructor` values).
17
- */
18
- readonly pluginPackageId?: string;
19
- register(context: CodemationPluginContext): void | Promise<void>;
13
+ export interface CodemationPluginConfig {
14
+ readonly credentialTypes?: ReadonlyArray<AnyCredentialType>;
15
+ readonly register?: (context: CodemationPluginContext) => void | Promise<void>;
16
+ readonly sandbox?: CodemationConfig;
17
+ }
18
+
19
+ export type CodemationPlugin = CodemationPluginConfig;
20
+
21
+ const definePlugin = <TConfig extends CodemationPluginConfig>(config: TConfig): TConfig => config;
22
+
23
+ export { definePlugin };
24
+
25
+ export class CodemationPluginPackageMetadata {
26
+ private static readonly packageNameSymbol = Symbol.for("@codemation/plugin-package-name");
27
+
28
+ attachPackageName(plugin: CodemationPlugin, packageName: string): CodemationPlugin {
29
+ if (packageName.trim().length === 0) {
30
+ return plugin;
31
+ }
32
+ const mutablePlugin = plugin as CodemationPlugin & {
33
+ [CodemationPluginPackageMetadata.packageNameSymbol]?: string;
34
+ };
35
+ const existingPackageName = mutablePlugin[CodemationPluginPackageMetadata.packageNameSymbol];
36
+ if (existingPackageName === packageName) {
37
+ return plugin;
38
+ }
39
+ Object.defineProperty(mutablePlugin, CodemationPluginPackageMetadata.packageNameSymbol, {
40
+ configurable: true,
41
+ enumerable: false,
42
+ value: packageName,
43
+ writable: true,
44
+ });
45
+ return plugin;
46
+ }
47
+
48
+ readPackageName(plugin: CodemationPlugin): string | undefined {
49
+ const packageName = (
50
+ plugin as CodemationPlugin & {
51
+ [CodemationPluginPackageMetadata.packageNameSymbol]?: unknown;
52
+ }
53
+ )[CodemationPluginPackageMetadata.packageNameSymbol];
54
+ return typeof packageName === "string" && packageName.trim().length > 0 ? packageName : undefined;
55
+ }
20
56
  }
@@ -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
  }
@@ -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";
@@ -173,10 +172,15 @@ export class CodemationConsumerConfigLoader {
173
172
  return await this.importModuleWithNativeRuntime(modulePath);
174
173
  }
175
174
  const tsconfigPath = await this.resolveTsconfigPath(modulePath);
175
+ if (this.shouldResetImporterBeforeImport()) {
176
+ await this.resetImporter(tsconfigPath);
177
+ }
176
178
  const importSpecifier = await this.createImportSpecifier(modulePath);
177
179
  for (let attempt = 0; attempt < 3; attempt += 1) {
178
180
  try {
179
- const importedModule = await this.getOrCreateImporter(tsconfigPath).import(importSpecifier, import.meta.url);
181
+ const importedModule = await (
182
+ await this.getOrCreateImporter(tsconfigPath)
183
+ ).import(importSpecifier, import.meta.url);
180
184
  return importedModule as Record<string, unknown>;
181
185
  } catch (error) {
182
186
  if (!this.isStoppedTransformServiceError(error) || attempt === 2) {
@@ -202,12 +206,13 @@ export class CodemationConsumerConfigLoader {
202
206
  return discoveredPath ?? false;
203
207
  }
204
208
 
205
- private getOrCreateImporter(tsconfigPath: string | false): NamespacedUnregister {
209
+ private async getOrCreateImporter(tsconfigPath: string | false): Promise<NamespacedUnregister> {
206
210
  const cacheKey = tsconfigPath || "default";
207
211
  const existingImporter = CodemationConsumerConfigLoader.importerRegistrationsByTsconfig.get(cacheKey);
208
212
  if (existingImporter) {
209
213
  return existingImporter;
210
214
  }
215
+ const { register } = await import(/* webpackIgnore: true */ this.resolveTsxImporterModuleSpecifier());
211
216
  const nextImporter = register({
212
217
  namespace: this.toNamespace(cacheKey),
213
218
  tsconfig: tsconfigPath,
@@ -230,6 +235,10 @@ export class CodemationConsumerConfigLoader {
230
235
  return `codemation_consumer_${cacheKey.replace(/[^a-zA-Z0-9_-]+/g, "_")}`;
231
236
  }
232
237
 
238
+ private resolveTsxImporterModuleSpecifier(): string {
239
+ return ["tsx", "esm", "api"].join("/");
240
+ }
241
+
233
242
  private async findNearestTsconfig(modulePath: string): Promise<string | null> {
234
243
  let currentDirectory = path.dirname(modulePath);
235
244
  while (true) {
@@ -256,6 +265,10 @@ export class CodemationConsumerConfigLoader {
256
265
  return process.env.CODEMATION_TS_RUNTIME === "ts-node";
257
266
  }
258
267
 
268
+ private shouldResetImporterBeforeImport(): boolean {
269
+ return (process.env.CODEMATION_DEV_SERVER_TOKEN?.trim().length ?? 0) > 0;
270
+ }
271
+
259
272
  private isStoppedTransformServiceError(error: unknown): boolean {
260
273
  return error instanceof Error && error.message.includes("The service is no longer running");
261
274
  }
@@ -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,