@jskit-ai/console-core 0.1.1

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 (32) hide show
  1. package/package.descriptor.mjs +119 -0
  2. package/package.json +18 -0
  3. package/src/server/ConsoleCoreServiceProvider.js +22 -0
  4. package/src/server/consoleBootstrapContributor.js +37 -0
  5. package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +63 -0
  6. package/src/server/consoleSettings/consoleService.js +36 -0
  7. package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
  8. package/src/server/consoleSettings/consoleSettingsRepository.js +119 -0
  9. package/src/server/consoleSettings/consoleSettingsService.js +40 -0
  10. package/src/server/consoleSettings/registerConsoleSettings.js +56 -0
  11. package/src/server/registerConsoleBootstrap.js +16 -0
  12. package/src/server/registerConsoleCore.js +17 -0
  13. package/src/server/support/consoleActionSurfaces.js +62 -0
  14. package/src/shared/operationMessages.js +16 -0
  15. package/src/shared/resources/consoleSettingsFields.js +54 -0
  16. package/src/shared/resources/consoleSettingsResource.js +119 -0
  17. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  18. package/templates/migrations/console_core_generic_initial.cjs +27 -0
  19. package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
  20. package/test/bootstrapPayloadIntegration.test.js +86 -0
  21. package/test/consoleActionSurfaces.test.js +24 -0
  22. package/test/consoleBootstrapContributor.test.js +64 -0
  23. package/test/consoleRouteRequestInputValidator.test.js +119 -0
  24. package/test/consoleRouteResources.test.js +72 -0
  25. package/test/consoleService.test.js +57 -0
  26. package/test/consoleSettingsService.test.js +86 -0
  27. package/test/exportsContract.test.js +26 -0
  28. package/test/registerConsoleCore.test.js +38 -0
  29. package/test/registerServiceRealtimeEvents.test.js +38 -0
  30. package/test/repositoryContracts.test.js +28 -0
  31. package/test/settingsFieldRegistriesSingleton.test.js +14 -0
  32. package/test-support/registerDefaultSettingsFields.js +1 -0
@@ -0,0 +1,54 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
2
+ import { resolveGlobalArrayRegistry } from "./resolveGlobalArrayRegistry.js";
3
+
4
+ const consoleSettingsFields = resolveGlobalArrayRegistry("jskit.console-core.consoleSettingsFields");
5
+
6
+ function defineField(field = {}) {
7
+ const key = normalizeText(field.key);
8
+ if (!key) {
9
+ throw new TypeError("consoleSettingsFields.defineField requires field.key.");
10
+ }
11
+ if (consoleSettingsFields.some((entry) => entry.key === key)) {
12
+ throw new Error(`consoleSettingsFields.defineField duplicate key: ${key}`);
13
+ }
14
+ if (!field.inputSchema || typeof field.inputSchema !== "object") {
15
+ throw new TypeError(`consoleSettingsFields.defineField("${key}") requires inputSchema.`);
16
+ }
17
+ if (!field.outputSchema || typeof field.outputSchema !== "object") {
18
+ throw new TypeError(`consoleSettingsFields.defineField("${key}") requires outputSchema.`);
19
+ }
20
+ const dbColumn = normalizeText(field.dbColumn);
21
+ if (!dbColumn) {
22
+ throw new TypeError(`consoleSettingsFields.defineField("${key}") requires dbColumn.`);
23
+ }
24
+ if (typeof field.normalizeInput !== "function") {
25
+ throw new TypeError(`consoleSettingsFields.defineField("${key}") requires normalizeInput.`);
26
+ }
27
+ if (typeof field.normalizeOutput !== "function") {
28
+ throw new TypeError(`consoleSettingsFields.defineField("${key}") requires normalizeOutput.`);
29
+ }
30
+ if (typeof field.resolveDefault !== "function") {
31
+ throw new TypeError(`consoleSettingsFields.defineField("${key}") requires resolveDefault.`);
32
+ }
33
+
34
+ consoleSettingsFields.push({
35
+ key,
36
+ dbColumn,
37
+ required: field.required !== false,
38
+ inputSchema: field.inputSchema,
39
+ outputSchema: field.outputSchema,
40
+ normalizeInput: field.normalizeInput,
41
+ normalizeOutput: field.normalizeOutput,
42
+ resolveDefault: field.resolveDefault
43
+ });
44
+ }
45
+
46
+ function resetConsoleSettingsFields() {
47
+ consoleSettingsFields.splice(0, consoleSettingsFields.length);
48
+ }
49
+
50
+ export {
51
+ defineField,
52
+ resetConsoleSettingsFields,
53
+ consoleSettingsFields
54
+ };
@@ -0,0 +1,119 @@
1
+ import { Type } from "typebox";
2
+ import { createOperationMessages } from "../operationMessages.js";
3
+ import {
4
+ createCursorListValidator,
5
+ normalizeObjectInput,
6
+ normalizeSettingsFieldInput,
7
+ normalizeSettingsFieldOutput
8
+ } from "@jskit-ai/kernel/shared/validators";
9
+ import { consoleSettingsFields } from "./consoleSettingsFields.js";
10
+
11
+ function buildCreateSchema() {
12
+ const properties = {};
13
+ for (const field of consoleSettingsFields) {
14
+ properties[field.key] = field.required === false ? Type.Optional(field.inputSchema) : field.inputSchema;
15
+ }
16
+ return Type.Object(properties, { additionalProperties: false });
17
+ }
18
+
19
+ function buildOutputSchema() {
20
+ const properties = {};
21
+ for (const field of consoleSettingsFields) {
22
+ properties[field.key] = field.outputSchema;
23
+ }
24
+ return Type.Object(properties, { additionalProperties: false });
25
+ }
26
+
27
+ function buildConsoleSettingsRecordSchema() {
28
+ return Type.Object(
29
+ {
30
+ settings: buildOutputSchema()
31
+ },
32
+ { additionalProperties: false }
33
+ );
34
+ }
35
+
36
+ function buildConsoleSettingsCreateSchema() {
37
+ return buildCreateSchema();
38
+ }
39
+
40
+ function buildConsoleSettingsReplaceSchema() {
41
+ return buildConsoleSettingsCreateSchema();
42
+ }
43
+
44
+ function buildConsoleSettingsPatchSchema() {
45
+ return Type.Partial(buildConsoleSettingsCreateSchema(), {
46
+ additionalProperties: false
47
+ });
48
+ }
49
+
50
+ function normalizeConsoleSettingsInput(payload = {}) {
51
+ return normalizeSettingsFieldInput(payload, consoleSettingsFields);
52
+ }
53
+
54
+ const consoleSettingsOutputValidator = Object.freeze({
55
+ get schema() {
56
+ return buildConsoleSettingsRecordSchema();
57
+ },
58
+ normalize(payload = {}) {
59
+ const source = normalizeObjectInput(payload);
60
+ const settingsSource = normalizeObjectInput(source.settings);
61
+
62
+ return {
63
+ settings: normalizeSettingsFieldOutput(settingsSource, consoleSettingsFields)
64
+ };
65
+ }
66
+ });
67
+
68
+ const CONSOLE_SETTINGS_OPERATION_MESSAGES = createOperationMessages();
69
+
70
+ const consoleSettingsResource = Object.freeze({
71
+ resource: "consoleSettings",
72
+ operations: Object.freeze({
73
+ view: Object.freeze({
74
+ method: "GET",
75
+ messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
76
+ outputValidator: consoleSettingsOutputValidator
77
+ }),
78
+ list: Object.freeze({
79
+ method: "GET",
80
+ messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
81
+ outputValidator: createCursorListValidator(consoleSettingsOutputValidator)
82
+ }),
83
+ create: Object.freeze({
84
+ method: "POST",
85
+ messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
86
+ bodyValidator: Object.freeze({
87
+ get schema() {
88
+ return buildConsoleSettingsCreateSchema();
89
+ },
90
+ normalize: normalizeConsoleSettingsInput
91
+ }),
92
+ outputValidator: consoleSettingsOutputValidator
93
+ }),
94
+ replace: Object.freeze({
95
+ method: "PUT",
96
+ messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
97
+ bodyValidator: Object.freeze({
98
+ get schema() {
99
+ return buildConsoleSettingsReplaceSchema();
100
+ },
101
+ normalize: normalizeConsoleSettingsInput
102
+ }),
103
+ outputValidator: consoleSettingsOutputValidator
104
+ }),
105
+ patch: Object.freeze({
106
+ method: "PATCH",
107
+ messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
108
+ bodyValidator: Object.freeze({
109
+ get schema() {
110
+ return buildConsoleSettingsPatchSchema();
111
+ },
112
+ normalize: normalizeConsoleSettingsInput
113
+ }),
114
+ outputValidator: consoleSettingsOutputValidator
115
+ })
116
+ })
117
+ });
118
+
119
+ export { consoleSettingsResource };
@@ -0,0 +1,6 @@
1
+ function resolveGlobalArrayRegistry(symbolKey) {
2
+ globalThis[symbolKey] = Array.isArray(globalThis[symbolKey]) ? globalThis[symbolKey] : [];
3
+ return globalThis[symbolKey];
4
+ }
5
+
6
+ export { resolveGlobalArrayRegistry };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @param {import('knex').Knex} knex
3
+ */
4
+ exports.up = async function up(knex) {
5
+ const hasConsoleSettingsTable = await knex.schema.hasTable("console_settings");
6
+ if (!hasConsoleSettingsTable) {
7
+ await knex.schema.createTable("console_settings", (table) => {
8
+ table.bigInteger("id").primary();
9
+ table.bigInteger("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
10
+ table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
11
+ table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
12
+ });
13
+
14
+ await knex("console_settings").insert({
15
+ id: 1,
16
+ created_at: knex.fn.now(),
17
+ updated_at: knex.fn.now()
18
+ });
19
+ }
20
+ };
21
+
22
+ /**
23
+ * @param {import('knex').Knex} knex
24
+ */
25
+ exports.down = async function down(knex) {
26
+ await knex.schema.dropTableIfExists("console_settings");
27
+ };
@@ -0,0 +1,11 @@
1
+ // @jskit-contract console.settings-fields.v1
2
+ // Append-only settings field registrations for console settings.
3
+
4
+ import {
5
+ defineField,
6
+ resetConsoleSettingsFields
7
+ } from "@jskit-ai/console-core/shared/resources/consoleSettingsFields";
8
+
9
+ resetConsoleSettingsFields();
10
+
11
+ void defineField;
@@ -0,0 +1,86 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createContainer } from "../../kernel/server/container/index.js";
4
+ import { resolveBootstrapPayload } from "../../kernel/server/registries/bootstrapPayloadContributorRegistry.js";
5
+ import { registerUsersBootstrap } from "../../users-core/src/server/registerUsersBootstrap.js";
6
+ import { registerConsoleBootstrap } from "../src/server/registerConsoleBootstrap.js";
7
+
8
+ function createAuthenticatedProfile(overrides = {}) {
9
+ return {
10
+ id: "12",
11
+ authProvider: "local",
12
+ authProviderUserSid: "user-12",
13
+ username: "consoleowner",
14
+ displayName: "Console Owner",
15
+ email: "owner@example.com",
16
+ ...overrides
17
+ };
18
+ }
19
+
20
+ test("bootstrap payload preserves consoleowner for authenticated users after users bootstrap runs", async () => {
21
+ const profile = createAuthenticatedProfile();
22
+ const ownerSeeds = [];
23
+ const app = createContainer();
24
+
25
+ app.instance("usersRepository", {
26
+ async findById(userId) {
27
+ return String(userId || "") === profile.id ? profile : null;
28
+ }
29
+ });
30
+ app.instance("userSettingsRepository", {
31
+ async ensureForUserId() {
32
+ return {};
33
+ }
34
+ });
35
+ app.instance("users.tenancy.profile", {
36
+ mode: "none",
37
+ workspace: {
38
+ enabled: false,
39
+ autoProvision: false,
40
+ allowSelfCreate: false,
41
+ slugPolicy: "none"
42
+ }
43
+ });
44
+ app.instance("authService", {
45
+ getOAuthProviderCatalog() {
46
+ return {
47
+ providers: [],
48
+ defaultProvider: null
49
+ };
50
+ },
51
+ writeSessionCookies() {},
52
+ clearSessionCookies() {}
53
+ });
54
+ app.instance("consoleService", {
55
+ async ensureInitialConsoleMember(userId) {
56
+ ownerSeeds.push(String(userId || ""));
57
+ return String(userId || "");
58
+ }
59
+ });
60
+
61
+ registerConsoleBootstrap(app);
62
+ registerUsersBootstrap(app);
63
+
64
+ const payload = await resolveBootstrapPayload(app, {
65
+ request: {
66
+ async executeAction({ actionId }) {
67
+ assert.equal(actionId, "auth.session.read");
68
+ return {
69
+ authenticated: true,
70
+ profile,
71
+ session: {
72
+ csrfToken: "csrf-1"
73
+ }
74
+ };
75
+ }
76
+ },
77
+ reply: {}
78
+ });
79
+
80
+ assert.deepEqual(ownerSeeds, ["12"]);
81
+ assert.equal(payload.session.authenticated, true);
82
+ assert.equal(payload.session.userId, "12");
83
+ assert.deepEqual(payload.surfaceAccess, {
84
+ consoleowner: true
85
+ });
86
+ });
@@ -0,0 +1,24 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ resolveConsoleSurfaceIdsFromAppConfig
5
+ } from "../src/server/support/consoleActionSurfaces.js";
6
+
7
+ test("resolveConsoleSurfaceIdsFromAppConfig resolves all enabled console-owner surfaces", () => {
8
+ const surfaceIds = resolveConsoleSurfaceIdsFromAppConfig({
9
+ surfaceDefinitions: {
10
+ home: { id: "home", enabled: true, requiresWorkspace: false, accessPolicyId: "public" },
11
+ console: { id: "console", enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" },
12
+ opsConsole: { id: "opsConsole", enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" },
13
+ app: { id: "app", enabled: true, requiresWorkspace: true, accessPolicyId: "workspace_member" },
14
+ disabledConsole: {
15
+ id: "disabledConsole",
16
+ enabled: false,
17
+ requiresWorkspace: false,
18
+ accessPolicyId: "console_owner"
19
+ }
20
+ }
21
+ });
22
+
23
+ assert.deepEqual(surfaceIds, ["console", "opsconsole"]);
24
+ });
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createConsoleBootstrapContributor } from "../src/server/consoleBootstrapContributor.js";
4
+
5
+ test("console bootstrap contributor seeds the initial console owner into the existing bootstrap payload", async () => {
6
+ const ownerSeeds = [];
7
+ const contributor = createConsoleBootstrapContributor({
8
+ consoleService: {
9
+ async ensureInitialConsoleMember(userId) {
10
+ ownerSeeds.push(String(userId || ""));
11
+ return String(userId || "");
12
+ }
13
+ }
14
+ });
15
+
16
+ assert.equal(contributor.order, 300);
17
+ const contribution = await contributor.contribute({
18
+ payload: {
19
+ session: {
20
+ authenticated: true,
21
+ userId: "12"
22
+ },
23
+ surfaceAccess: {
24
+ existing: true
25
+ }
26
+ }
27
+ });
28
+
29
+ assert.deepEqual(ownerSeeds, ["12"]);
30
+ assert.deepEqual(contribution, {
31
+ surfaceAccess: {
32
+ existing: true,
33
+ consoleowner: true
34
+ }
35
+ });
36
+ });
37
+
38
+ test("console bootstrap contributor exposes a false consoleowner flag for anonymous bootstrap", async () => {
39
+ const contributor = createConsoleBootstrapContributor({
40
+ consoleService: {
41
+ async ensureInitialConsoleMember() {
42
+ throw new Error("should not be called for anonymous payload");
43
+ }
44
+ }
45
+ });
46
+
47
+ const contribution = await contributor.contribute({
48
+ payload: {
49
+ session: {
50
+ authenticated: false
51
+ },
52
+ surfaceAccess: {
53
+ existing: true
54
+ }
55
+ }
56
+ });
57
+
58
+ assert.deepEqual(contribution, {
59
+ surfaceAccess: {
60
+ existing: true,
61
+ consoleowner: false
62
+ }
63
+ });
64
+ });
@@ -0,0 +1,119 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { ConsoleCoreServiceProvider } from "../src/server/ConsoleCoreServiceProvider.js";
4
+
5
+ function createReplyDouble() {
6
+ return {
7
+ statusCode: 200,
8
+ payload: null,
9
+ redirectedTo: "",
10
+ code(value) {
11
+ this.statusCode = value;
12
+ return this;
13
+ },
14
+ send(value) {
15
+ this.payload = value;
16
+ return this;
17
+ },
18
+ redirect(value) {
19
+ this.redirectedTo = String(value || "");
20
+ return this;
21
+ }
22
+ };
23
+ }
24
+
25
+ function findRoute(routes, { method, path }) {
26
+ return routes.find((route) => route.method === method && route.path === path) || null;
27
+ }
28
+
29
+ async function registerRoutes({
30
+ consoleService = {}
31
+ } = {}) {
32
+ const registeredRoutes = [];
33
+ const router = {
34
+ register(method, path, route, handler) {
35
+ registeredRoutes.push({
36
+ ...route,
37
+ method,
38
+ path,
39
+ handler
40
+ });
41
+ }
42
+ };
43
+
44
+ const bindings = new Map([
45
+ ["jskit.http.router", router],
46
+ ["actionExecutor", {}]
47
+ ]);
48
+
49
+ bindings.set("consoleService", consoleService);
50
+
51
+ const app = {
52
+ has(token) {
53
+ return bindings.has(token);
54
+ },
55
+ make(token) {
56
+ if (!bindings.has(token)) {
57
+ throw new Error(`Missing test binding for token: ${String(token)}`);
58
+ }
59
+ return bindings.get(token);
60
+ }
61
+ };
62
+
63
+ const provider = new ConsoleCoreServiceProvider();
64
+ await provider.boot(app);
65
+
66
+ return registeredRoutes;
67
+ }
68
+
69
+ function createActionRequest({ input = {}, executeAction, file = null }) {
70
+ return {
71
+ input,
72
+ executeAction,
73
+ file,
74
+ user: {
75
+ id: 42
76
+ }
77
+ };
78
+ }
79
+
80
+ test("console-core boot mounts console routes", async () => {
81
+ const routes = await registerRoutes();
82
+
83
+ assert.equal(findRoute(routes, { method: "GET", path: "/api/console/settings" })?.path, "/api/console/settings");
84
+ assert.equal(findRoute(routes, { method: "PATCH", path: "/api/console/settings" })?.path, "/api/console/settings");
85
+ });
86
+
87
+ test("console settings route handlers use request.input payloads", async () => {
88
+ const routes = await registerRoutes();
89
+ const calls = [];
90
+ const executeAction = async (payload) => {
91
+ calls.push(payload);
92
+ return {
93
+ settings: {}
94
+ };
95
+ };
96
+
97
+ await findRoute(routes, { method: "GET", path: "/api/console/settings" }).handler(
98
+ createActionRequest({ executeAction }),
99
+ createReplyDouble()
100
+ );
101
+
102
+ await findRoute(routes, { method: "PATCH", path: "/api/console/settings" }).handler(
103
+ createActionRequest({
104
+ input: {
105
+ body: {}
106
+ },
107
+ executeAction
108
+ }),
109
+ createReplyDouble()
110
+ );
111
+
112
+ assert.equal(calls[0].actionId, "console.settings.read");
113
+ assert.deepEqual(calls[1], {
114
+ actionId: "console.settings.update",
115
+ input: {
116
+ payload: {}
117
+ }
118
+ });
119
+ });
@@ -0,0 +1,72 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import path from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { deriveResourceRequiredMetadata } from "@jskit-ai/kernel/_testable";
7
+ import "../test-support/registerDefaultSettingsFields.js";
8
+ import { consoleSettingsResource } from "../src/shared/resources/consoleSettingsResource.js";
9
+
10
+ function assertResourceShape(resource, label) {
11
+ assert.ok(resource, `${label} resource must exist.`);
12
+ assert.equal(typeof resource, "object", `${label} resource must be an object.`);
13
+ assert.equal(typeof resource.resource, "string", `${label}.resource must be a string.`);
14
+
15
+ for (const operationName of ["view", "list", "create", "replace", "patch"]) {
16
+ const operation = resource.operations?.[operationName];
17
+ assert.equal(typeof operation, "object", `${label}.operations.${operationName} must exist.`);
18
+ assert.equal(typeof operation.method, "string", `${label}.operations.${operationName}.method must exist.`);
19
+ const resolvedMessages =
20
+ operation?.messages && typeof operation.messages === "object"
21
+ ? operation.messages
22
+ : resource?.messages || resource?.operationMessages;
23
+ assert.equal(
24
+ typeof resolvedMessages,
25
+ "object",
26
+ `${label}.operations.${operationName} must resolve messages from operation.messages or resource.messages.`
27
+ );
28
+ assert.equal(
29
+ typeof operation.outputValidator?.schema,
30
+ "object",
31
+ `${label}.operations.${operationName} payload schema is required.`
32
+ );
33
+ }
34
+
35
+ assert.equal(typeof resource.operations.create.bodyValidator?.schema, "object", `${label}.operations.create.bodyValidator.schema is required.`);
36
+ assert.equal(typeof resource.operations.replace.bodyValidator?.schema, "object", `${label}.operations.replace.bodyValidator.schema is required.`);
37
+ assert.equal(typeof resource.operations.patch.bodyValidator?.schema, "object", `${label}.operations.patch.bodyValidator.schema is required.`);
38
+
39
+ const requiredMetadata = deriveResourceRequiredMetadata(resource);
40
+ assert.ok(Array.isArray(requiredMetadata.create), `${label}.derivedRequired.create must be an array.`);
41
+ assert.ok(Array.isArray(requiredMetadata.replace), `${label}.derivedRequired.replace must be an array.`);
42
+ assert.ok(Array.isArray(requiredMetadata.patch), `${label}.derivedRequired.patch must be an array.`);
43
+ }
44
+
45
+ test("console settings resources expose canonical validators", () => {
46
+ assertResourceShape(consoleSettingsResource, "consoleSettings");
47
+ });
48
+
49
+ test("console settings operations expose canonical validators", () => {
50
+ for (const operationName of ["view", "list", "create", "replace", "patch"]) {
51
+ const operation = consoleSettingsResource.operations?.[operationName];
52
+ assert.equal(typeof operation?.method, "string", `${operationName}.method must exist.`);
53
+ assert.equal(typeof operation?.outputValidator?.schema, "object", `${operationName}.outputValidator.schema must exist.`);
54
+ if (operation?.bodyValidator) {
55
+ assert.equal(typeof operation.bodyValidator.schema, "object", `${operationName}.bodyValidator.schema must exist.`);
56
+ }
57
+ }
58
+ });
59
+
60
+ test("console-core no longer uses a legacy workspace schema helper path", () => {
61
+ const testFilePath = fileURLToPath(import.meta.url);
62
+ const packageRoot = path.resolve(path.dirname(testFilePath), "..");
63
+ const legacyWorkspaceRoutesFile = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
64
+ assert.equal(existsSync(legacyWorkspaceRoutesFile), false, "workspaceRoutes.js must not exist.");
65
+ });
66
+
67
+ test("console-core route validators do not live under a legacy shared/schema directory", () => {
68
+ const testFilePath = fileURLToPath(import.meta.url);
69
+ const packageRoot = path.resolve(path.dirname(testFilePath), "..");
70
+ const legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
71
+ assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
72
+ });
@@ -0,0 +1,57 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createService } from "../src/server/consoleSettings/consoleService.js";
4
+
5
+ function createFixture(initialOwnerUserId = null) {
6
+ const state = {
7
+ ownerUserId: initialOwnerUserId
8
+ };
9
+
10
+ const service = createService({
11
+ consoleSettingsRepository: {
12
+ async ensureOwnerUserId(userId) {
13
+ const normalizedUserId = String(userId || "");
14
+ if (!state.ownerUserId) {
15
+ state.ownerUserId = normalizedUserId;
16
+ }
17
+ return state.ownerUserId;
18
+ }
19
+ }
20
+ });
21
+
22
+ return { service, state };
23
+ }
24
+
25
+ test("consoleService seeds the first authenticated user as console owner", async () => {
26
+ const { service, state } = createFixture();
27
+
28
+ const firstOwner = await service.ensureInitialConsoleMember("7");
29
+ const secondAttempt = await service.ensureInitialConsoleMember("9");
30
+
31
+ assert.equal(firstOwner, "7");
32
+ assert.equal(secondAttempt, "7");
33
+ assert.equal(state.ownerUserId, "7");
34
+ });
35
+
36
+ test("consoleService.requireConsoleOwner denies authenticated non-owners", async () => {
37
+ const { service } = createFixture("7");
38
+
39
+ await assert.rejects(
40
+ () =>
41
+ service.requireConsoleOwner({
42
+ actor: {
43
+ id: "9"
44
+ }
45
+ }),
46
+ (error) => error?.status === 403
47
+ );
48
+ });
49
+
50
+ test("consoleService.requireConsoleOwner requires authentication", async () => {
51
+ const { service } = createFixture("7");
52
+
53
+ await assert.rejects(
54
+ () => service.requireConsoleOwner({}),
55
+ (error) => error?.status === 401
56
+ );
57
+ });