@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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +37 -0
- package/dist/{AppConfigFactory-DmHOpg8O.d.ts → AppConfigFactory-Co4STjwt.d.ts} +5 -3
- package/dist/{AppConfigFactory-DWIz2hy-.js → AppConfigFactory-DxoZ4v8r.js} +40 -3
- package/dist/AppConfigFactory-DxoZ4v8r.js.map +1 -0
- package/dist/{AppContainerFactory-B5eRpvAa.js → AppContainerFactory-z9aUDFiJ.js} +275 -105
- package/dist/AppContainerFactory-z9aUDFiJ.js.map +1 -0
- package/dist/{CodemationConfig-D2ULNkec.d.ts → CodemationConfig-COs4GcOE.d.ts} +18 -10
- package/dist/{CodemationConfigNormalizer-CBLxXaRV.d.ts → CodemationConfigNormalizer-B7w1JA_2.d.ts} +2 -2
- package/dist/{CodemationConsumerConfigLoader-BBzAr6L_.js → CodemationConsumerConfigLoader-C_ISRrpI.js} +28 -7
- package/dist/CodemationConsumerConfigLoader-C_ISRrpI.js.map +1 -0
- package/dist/{CodemationConsumerConfigLoader-BLvzcfb7.d.ts → CodemationConsumerConfigLoader-OlXKw-us.d.ts} +6 -2
- package/dist/CodemationPluginListMerger-CGwOTdZ7.js +57 -0
- package/dist/CodemationPluginListMerger-CGwOTdZ7.js.map +1 -0
- package/dist/{CodemationPluginListMerger-B0-e4CJ6.d.ts → CodemationPluginListMerger-_ZIiOQxB.d.ts} +79 -117
- package/dist/{CredentialServices-BeuMtqYA.d.ts → CredentialServices-D3VTczpC.d.ts} +3 -3
- package/dist/PublicFrontendBootstrap-p7mS8aWG.d.ts +50 -0
- package/dist/{FrontendAppConfigFactory-Bj-DZNlt.d.ts → PublicFrontendBootstrapFactory-CE4oGogq.d.ts} +19 -4
- package/dist/{FrontendAppConfigJsonCodec-nOCQI0ag.js → PublicFrontendBootstrapJsonCodec-BdiVGG5R.js} +76 -2
- package/dist/PublicFrontendBootstrapJsonCodec-BdiVGG5R.js.map +1 -0
- package/dist/PublicFrontendBootstrapJsonCodec-DjkkKXcq.d.ts +35 -0
- package/dist/client.d.ts +3 -3
- package/dist/client.js +2 -2
- package/dist/consumer.d.ts +4 -4
- package/dist/consumer.js +1 -1
- package/dist/credentials.d.ts +3 -3
- package/dist/devServerSidecar.d.ts +1 -1
- package/dist/{index-CkiptHb-.d.ts → index-DbYzycTC.d.ts} +215 -71
- package/dist/index.d.ts +150 -13
- package/dist/index.js +94 -8
- package/dist/index.js.map +1 -0
- package/dist/nextServer.d.ts +30 -9
- package/dist/nextServer.js +4 -4
- package/dist/{persistenceServer-DVeWUbc3.js → persistenceServer-C4L1uMKn.js} +2 -2
- package/dist/{persistenceServer-DVeWUbc3.js.map → persistenceServer-C4L1uMKn.js.map} +1 -1
- package/dist/{persistenceServer-CaehMh3M.d.ts → persistenceServer-Cr-zCuEr.d.ts} +2 -2
- package/dist/persistenceServer.d.ts +5 -5
- package/dist/persistenceServer.js +2 -2
- package/dist/{server-Dyo8qh4k.d.ts → server-B6k53aZj.d.ts} +14 -19
- package/dist/{server-C65z_kqm.js → server-DDVXr7BN.js} +42 -25
- package/dist/server-DDVXr7BN.js.map +1 -0
- package/dist/server.d.ts +11 -11
- package/dist/server.js +7 -7
- package/package.json +9 -6
- package/playwright.scaffolded-dev.config.ts +46 -0
- package/src/application/dev/BootRuntimeSummary.types.ts +2 -0
- package/src/application/dev/DevBootstrapSummaryAssembler.ts +1 -0
- package/src/application/dev/DevBootstrapSummaryJson.types.ts +6 -0
- package/src/bootstrap/AppContainerFactory.ts +101 -6
- package/src/bootstrap/runtime/AppConfigFactory.ts +22 -2
- package/src/client.ts +4 -0
- package/src/index.ts +21 -5
- package/src/infrastructure/config/CodemationPluginRegistrar.ts +6 -0
- package/src/infrastructure/di/HandlesCommandRegistry.ts +1 -10
- package/src/infrastructure/di/HandlesQueryRegistry.ts +1 -10
- package/src/nextServer.ts +4 -0
- package/src/presentation/config/AppConfig.ts +6 -0
- package/src/presentation/config/CodemationAuthoring.types.ts +169 -0
- package/src/presentation/config/CodemationPackageManifest.ts +1 -7
- package/src/presentation/config/CodemationPlugin.ts +45 -8
- package/src/presentation/config/CodemationPluginListMerger.ts +30 -26
- package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -0
- package/src/presentation/frontend/InternalAuthBootstrap.ts +12 -0
- package/src/presentation/frontend/InternalAuthBootstrapFactory.ts +26 -0
- package/src/presentation/frontend/InternalAuthBootstrapJsonCodec.ts +49 -0
- package/src/presentation/frontend/PublicFrontendBootstrap.ts +12 -0
- package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +23 -0
- package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +48 -0
- package/src/presentation/http/ApiPaths.ts +10 -0
- package/src/presentation/http/hono/HonoHttpAnonymousRoutePolicyRegistry.ts +3 -0
- package/src/presentation/http/hono/registrars/BootstrapHonoApiRouteRegistrar.ts +32 -0
- package/src/presentation/http/routeHandlers/InternalAuthBootstrapHttpRouteHandler.ts +18 -0
- package/src/presentation/http/routeHandlers/PublicFrontendBootstrapHttpRouteHandler.ts +18 -0
- package/src/presentation/server/CodemationConsumerConfigLoader.ts +28 -6
- package/src/presentation/server/CodemationPluginDiscovery.ts +59 -28
- package/src/server.ts +6 -0
- package/dist/AppConfigFactory-DWIz2hy-.js.map +0 -1
- package/dist/AppContainerFactory-B5eRpvAa.js.map +0 -1
- package/dist/CodemationConsumerConfigLoader-BBzAr6L_.js.map +0 -1
- package/dist/CodemationPluginListMerger-DrVOw9KP.js +0 -57
- package/dist/CodemationPluginListMerger-DrVOw9KP.js.map +0 -1
- package/dist/FrontendAppConfig-D50wjj_n.d.ts +0 -27
- package/dist/FrontendAppConfigJsonCodec-1_L7H_Qo.d.ts +0 -20
- package/dist/FrontendAppConfigJsonCodec-nOCQI0ag.js.map +0 -1
- package/dist/server-C65z_kqm.js.map +0 -1
|
@@ -1,46 +1,50 @@
|
|
|
1
|
-
import type
|
|
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
|
|
7
|
-
* registered twice when the consumer config lists a plugin and discovery also finds it
|
|
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
|
|
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
|
|
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
|
|
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
|
|
230
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
41
|
-
developmentEntry: this.resolveDevelopmentPluginEntry(
|
|
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(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const exportedValue =
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
152
|
+
return discoveredPackage.pluginEntry;
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
private resolveDevelopmentPluginEntry(
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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,
|