@jskit-ai/users-core 0.1.4

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 (148) hide show
  1. package/package.descriptor.mjs +464 -0
  2. package/package.json +35 -0
  3. package/src/server/UsersCoreServiceProvider.js +74 -0
  4. package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
  5. package/src/server/accountNotifications/accountNotificationsService.js +41 -0
  6. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
  7. package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
  8. package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
  9. package/src/server/accountPreferences/accountPreferencesService.js +41 -0
  10. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
  11. package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
  12. package/src/server/accountProfile/accountProfileActions.js +137 -0
  13. package/src/server/accountProfile/accountProfileService.js +124 -0
  14. package/src/server/accountProfile/avatarService.js +141 -0
  15. package/src/server/accountProfile/avatarStorageService.js +132 -0
  16. package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
  17. package/src/server/accountProfile/registerAccountProfile.js +62 -0
  18. package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
  19. package/src/server/accountSecurity/accountSecurityActions.js +144 -0
  20. package/src/server/accountSecurity/accountSecurityService.js +103 -0
  21. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
  22. package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
  23. package/src/server/common/README.md +21 -0
  24. package/src/server/common/contributors/README.md +11 -0
  25. package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
  26. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
  27. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
  28. package/src/server/common/diTokens.js +21 -0
  29. package/src/server/common/formatters/README.md +11 -0
  30. package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
  31. package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
  32. package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
  33. package/src/server/common/formatters/workspaceFormatter.js +46 -0
  34. package/src/server/common/registerCommonRepositories.js +45 -0
  35. package/src/server/common/registerSharedApi.js +9 -0
  36. package/src/server/common/repositories/README.md +24 -0
  37. package/src/server/common/repositories/repositoryUtils.js +50 -0
  38. package/src/server/common/repositories/userProfilesRepository.js +251 -0
  39. package/src/server/common/repositories/userSettingsRepository.js +179 -0
  40. package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
  41. package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
  42. package/src/server/common/repositories/workspacesRepository.js +183 -0
  43. package/src/server/common/routes/README.md +11 -0
  44. package/src/server/common/services/README.md +12 -0
  45. package/src/server/common/services/accountContextService.js +31 -0
  46. package/src/server/common/services/authProfileSyncService.js +128 -0
  47. package/src/server/common/services/workspaceContextService.js +270 -0
  48. package/src/server/common/support/deepFreeze.js +17 -0
  49. package/src/server/common/support/realtimeServiceEvents.js +94 -0
  50. package/src/server/common/support/resolveActionUser.js +11 -0
  51. package/src/server/common/support/workspaceRoutePaths.js +17 -0
  52. package/src/server/common/validators/README.md +11 -0
  53. package/src/server/common/validators/authenticatedUserValidator.js +42 -0
  54. package/src/server/common/validators/routeParamsValidator.js +62 -0
  55. package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
  56. package/src/server/consoleSettings/consoleService.js +36 -0
  57. package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
  58. package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
  59. package/src/server/consoleSettings/consoleSettingsService.js +40 -0
  60. package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
  61. package/src/server/registerWorkspaceBootstrap.js +36 -0
  62. package/src/server/registerWorkspaceCore.js +95 -0
  63. package/src/server/support/resolveWorkspace.js +16 -0
  64. package/src/server/support/workspaceActionSurfaces.js +135 -0
  65. package/src/server/support/workspaceInvitationsPolicy.js +45 -0
  66. package/src/server/support/workspaceRouteInput.js +22 -0
  67. package/src/server/workspaceBootstrapContributor.js +401 -0
  68. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
  69. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
  70. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
  71. package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
  72. package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
  73. package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
  74. package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
  75. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
  76. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
  77. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
  78. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
  79. package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
  80. package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
  81. package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
  82. package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
  83. package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
  84. package/src/shared/events/usersEvents.js +19 -0
  85. package/src/shared/index.js +91 -0
  86. package/src/shared/operationMessages.js +16 -0
  87. package/src/shared/resources/consoleSettingsFields.js +55 -0
  88. package/src/shared/resources/consoleSettingsResource.js +139 -0
  89. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  90. package/src/shared/resources/userProfileResource.js +148 -0
  91. package/src/shared/resources/userSettingsFields.js +71 -0
  92. package/src/shared/resources/userSettingsResource.js +416 -0
  93. package/src/shared/resources/workspaceMembersResource.js +352 -0
  94. package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
  95. package/src/shared/resources/workspaceResource.js +149 -0
  96. package/src/shared/resources/workspaceSettingsFields.js +60 -0
  97. package/src/shared/resources/workspaceSettingsResource.js +178 -0
  98. package/src/shared/roles.js +136 -0
  99. package/src/shared/settings.js +31 -0
  100. package/src/shared/support/usersApiPaths.js +34 -0
  101. package/src/shared/support/usersVisibility.js +45 -0
  102. package/src/shared/support/workspacePathModel.js +145 -0
  103. package/src/shared/tenancyMode.js +35 -0
  104. package/src/shared/tenancyProfile.js +73 -0
  105. package/templates/config/workspaceRoles.js +30 -0
  106. package/templates/migrations/users_core_console_owner.cjs +39 -0
  107. package/templates/migrations/users_core_initial.cjs +118 -0
  108. package/templates/migrations/users_core_profile_username.cjs +98 -0
  109. package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
  110. package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
  111. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
  112. package/test/authProfileSyncService.test.js +119 -0
  113. package/test/avatarService.test.js +114 -0
  114. package/test/avatarStorageService.test.js +61 -0
  115. package/test/consoleService.test.js +57 -0
  116. package/test/consoleSettingsService.test.js +86 -0
  117. package/test/exportsContract.test.js +38 -0
  118. package/test/registerAvatarMultipartSupport.test.js +64 -0
  119. package/test/registerServiceRealtimeEvents.test.js +160 -0
  120. package/test/registerWorkspaceDirectory.test.js +26 -0
  121. package/test/registerWorkspaceSettings.test.js +44 -0
  122. package/test/resourcesCanonical.test.js +90 -0
  123. package/test/roles.test.js +74 -0
  124. package/test/settingsFieldRegistriesSingleton.test.js +24 -0
  125. package/test/tenancyProfile.test.js +67 -0
  126. package/test/userSettingsResource.test.js +31 -0
  127. package/test/usersApiPaths.test.js +31 -0
  128. package/test/usersRouteRequestInputValidator.test.js +556 -0
  129. package/test/usersRouteResources.test.js +113 -0
  130. package/test/usersRouteValidators.test.js +49 -0
  131. package/test/usersVisibility.test.js +22 -0
  132. package/test/workspaceActionContextContributor.test.js +251 -0
  133. package/test/workspaceActionSurfaces.test.js +105 -0
  134. package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
  135. package/test/workspaceBootstrapContributor.test.js +466 -0
  136. package/test/workspaceInvitationsPolicy.test.js +71 -0
  137. package/test/workspaceInvitesRepository.test.js +111 -0
  138. package/test/workspaceMembersService.test.js +400 -0
  139. package/test/workspacePathModel.test.js +93 -0
  140. package/test/workspacePendingInvitationsResource.test.js +38 -0
  141. package/test/workspacePendingInvitationsService.test.js +151 -0
  142. package/test/workspaceRouteVisibilityResolver.test.js +83 -0
  143. package/test/workspaceService.test.js +480 -0
  144. package/test/workspaceSettingsActions.test.js +42 -0
  145. package/test/workspaceSettingsRepository.test.js +156 -0
  146. package/test/workspaceSettingsResource.test.js +156 -0
  147. package/test/workspaceSettingsService.test.js +120 -0
  148. package/test-support/registerDefaultSettingsFields.js +3 -0
@@ -0,0 +1,90 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import path from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import "../test-support/registerDefaultSettingsFields.js";
7
+ import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
8
+ import { workspaceResource } from "../src/shared/resources/workspaceResource.js";
9
+ import { workspaceSettingsResource } from "../src/shared/resources/workspaceSettingsResource.js";
10
+ import { userProfileResource } from "../src/shared/resources/userProfileResource.js";
11
+ import { userSettingsResource } from "../src/shared/resources/userSettingsResource.js";
12
+ import { consoleSettingsResource } from "../src/shared/resources/consoleSettingsResource.js";
13
+
14
+ function assertResourceOperationMessages(resource, operationName, label) {
15
+ const operation = resource?.operations?.[operationName];
16
+ assert.equal(typeof operation, "object", `${label}.operations.${operationName} must exist.`);
17
+
18
+ const operationMessages = operation?.messages;
19
+ const resourceMessages = resource?.messages || resource?.operationMessages;
20
+ const resolvedMessages =
21
+ operationMessages && typeof operationMessages === "object"
22
+ ? operationMessages
23
+ : resourceMessages;
24
+
25
+ assert.equal(
26
+ typeof resolvedMessages,
27
+ "object",
28
+ `${label}.operations.${operationName} must resolve operation messages from operation.messages or resource.messages.`
29
+ );
30
+ }
31
+
32
+ test("users-core resources expose messages for all operations", () => {
33
+ const resources = {
34
+ workspace: workspaceResource,
35
+ workspaceSettings: workspaceSettingsResource,
36
+ userProfile: userProfileResource,
37
+ userSettings: userSettingsResource,
38
+ consoleSettings: consoleSettingsResource
39
+ };
40
+
41
+ for (const [label, resource] of Object.entries(resources)) {
42
+ for (const operationName of ["view", "list", "create", "replace", "patch"]) {
43
+ assertResourceOperationMessages(resource, operationName, label);
44
+ }
45
+ }
46
+ });
47
+
48
+ test("users-core specialized resource operations expose messages and validators", () => {
49
+ const workspaceMembersOperationSpecs = [
50
+ { label: "workspaceMembers.rolesList", operation: workspaceMembersResource.operations.rolesList },
51
+ { label: "workspaceMembers.membersList", operation: workspaceMembersResource.operations.membersList },
52
+ { label: "workspaceMembers.updateMemberRole", operation: workspaceMembersResource.operations.updateMemberRole },
53
+ { label: "workspaceMembers.removeMember", operation: workspaceMembersResource.operations.removeMember },
54
+ { label: "workspaceMembers.invitesList", operation: workspaceMembersResource.operations.invitesList },
55
+ { label: "workspaceMembers.createInvite", operation: workspaceMembersResource.operations.createInvite },
56
+ { label: "workspaceMembers.revokeInvite", operation: workspaceMembersResource.operations.revokeInvite },
57
+ { label: "workspaceMembers.redeemInvite", operation: workspaceMembersResource.operations.redeemInvite }
58
+ ];
59
+ const operationSpecs = [
60
+ ...workspaceMembersOperationSpecs,
61
+ { label: "userProfile.avatarUpload", operation: userProfileResource.operations.avatarUpload },
62
+ { label: "userProfile.avatarDelete", operation: userProfileResource.operations.avatarDelete },
63
+ { label: "userSettings.passwordChange", operation: userSettingsResource.operations.passwordChange },
64
+ { label: "userSettings.passwordMethodToggle", operation: userSettingsResource.operations.passwordMethodToggle },
65
+ { label: "userSettings.oauthLinkStart", operation: userSettingsResource.operations.oauthLinkStart },
66
+ { label: "userSettings.oauthUnlink", operation: userSettingsResource.operations.oauthUnlink },
67
+ { label: "userSettings.logoutOtherSessions", operation: userSettingsResource.operations.logoutOtherSessions }
68
+ ];
69
+
70
+ for (const { label, operation } of operationSpecs) {
71
+ assert.equal(typeof operation?.messages, "object", `${label}.messages must be an object.`);
72
+ assert.equal(typeof operation?.outputValidator?.schema, "object", `${label}.outputValidator.schema must exist.`);
73
+ if (operation?.bodyValidator) {
74
+ assert.equal(typeof operation.bodyValidator.schema, "object", `${label}.bodyValidator.schema must exist.`);
75
+ }
76
+ if (operation?.paramsValidator) {
77
+ assert.equal(typeof operation.paramsValidator.schema, "object", `${label}.paramsValidator.schema must exist.`);
78
+ }
79
+ if (operation?.queryValidator) {
80
+ assert.equal(typeof operation.queryValidator.schema, "object", `${label}.queryValidator.schema must exist.`);
81
+ }
82
+ }
83
+ });
84
+
85
+ test("users-core no longer contains legacy shared/schema directory", () => {
86
+ const testFilePath = fileURLToPath(import.meta.url);
87
+ const packageRoot = path.resolve(path.dirname(testFilePath), "..");
88
+ const legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
89
+ assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
90
+ });
@@ -0,0 +1,74 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ createWorkspaceRoleCatalog,
5
+ cloneWorkspaceRoleCatalog,
6
+ resolveRolePermissions,
7
+ hasPermission
8
+ } from "../src/shared/roles.js";
9
+
10
+ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.workspaceRoles", () => {
11
+ const emptyCatalog = createWorkspaceRoleCatalog();
12
+ assert.deepEqual(emptyCatalog.roles, []);
13
+ assert.deepEqual(emptyCatalog.assignableRoleIds, []);
14
+ assert.equal(emptyCatalog.defaultInviteRole, "");
15
+ assert.equal(emptyCatalog.collaborationEnabled, false);
16
+
17
+ const appConfig = {
18
+ workspaceRoles: {
19
+ defaultInviteRole: "editor",
20
+ roles: {
21
+ owner: {
22
+ assignable: false,
23
+ permissions: ["workspace.settings.update"]
24
+ },
25
+ editor: {
26
+ assignable: true,
27
+ permissions: ["crud_contacts.*"]
28
+ }
29
+ }
30
+ }
31
+ };
32
+ const roleCatalog = createWorkspaceRoleCatalog(appConfig);
33
+ const editorRole = roleCatalog.roles.find((role) => role.id === "editor");
34
+
35
+ assert.equal(roleCatalog.defaultInviteRole, "editor");
36
+ assert.equal(roleCatalog.assignableRoleIds.includes("editor"), true);
37
+ assert.deepEqual(resolveRolePermissions("owner", appConfig), ["workspace.settings.update"]);
38
+ assert.equal(hasPermission(editorRole?.permissions, "crud_contacts.update"), true);
39
+ });
40
+
41
+ test("cloneWorkspaceRoleCatalog normalizes role ids and returns detached arrays", () => {
42
+ const source = {
43
+ collaborationEnabled: true,
44
+ defaultInviteRole: "member",
45
+ roles: [
46
+ {
47
+ id: " MEMBER ",
48
+ assignable: true,
49
+ permissions: ["workspace.members.view"]
50
+ }
51
+ ],
52
+ assignableRoleIds: ["member"]
53
+ };
54
+
55
+ const cloned = cloneWorkspaceRoleCatalog(source);
56
+ assert.deepEqual(cloned, {
57
+ collaborationEnabled: true,
58
+ defaultInviteRole: "member",
59
+ roles: [
60
+ {
61
+ id: "member",
62
+ assignable: true,
63
+ permissions: ["workspace.members.view"]
64
+ }
65
+ ],
66
+ assignableRoleIds: ["member"]
67
+ });
68
+
69
+ cloned.roles[0].permissions.push("workspace.members.manage");
70
+ cloned.assignableRoleIds.push("admin");
71
+
72
+ assert.deepEqual(source.roles[0].permissions, ["workspace.members.view"]);
73
+ assert.deepEqual(source.assignableRoleIds, ["member"]);
74
+ });
@@ -0,0 +1,24 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ async function importWithIdentity(url, identity) {
5
+ return import(`${url.href}?identity=${identity}`);
6
+ }
7
+
8
+ test("settings field registries stay shared across module identities", async () => {
9
+ const workspaceModuleUrl = new URL("../src/shared/resources/workspaceSettingsFields.js", import.meta.url);
10
+ const userModuleUrl = new URL("../src/shared/resources/userSettingsFields.js", import.meta.url);
11
+ const consoleModuleUrl = new URL("../src/shared/resources/consoleSettingsFields.js", import.meta.url);
12
+
13
+ const workspaceA = await importWithIdentity(workspaceModuleUrl, "workspace-a");
14
+ const workspaceB = await importWithIdentity(workspaceModuleUrl, "workspace-b");
15
+ assert.equal(workspaceA.workspaceSettingsFields, workspaceB.workspaceSettingsFields);
16
+
17
+ const userA = await importWithIdentity(userModuleUrl, "user-a");
18
+ const userB = await importWithIdentity(userModuleUrl, "user-b");
19
+ assert.equal(userA.userSettingsFields, userB.userSettingsFields);
20
+
21
+ const consoleA = await importWithIdentity(consoleModuleUrl, "console-a");
22
+ const consoleB = await importWithIdentity(consoleModuleUrl, "console-b");
23
+ assert.equal(consoleA.consoleSettingsFields, consoleB.consoleSettingsFields);
24
+ });
@@ -0,0 +1,67 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ TENANCY_MODE_NONE,
5
+ TENANCY_MODE_PERSONAL,
6
+ TENANCY_MODE_WORKSPACE,
7
+ WORKSPACE_SLUG_POLICY_NONE,
8
+ WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
9
+ WORKSPACE_SLUG_POLICY_USER_SELECTED,
10
+ resolveTenancyProfile,
11
+ isWorkspaceTenancyMode
12
+ } from "../src/shared/tenancyProfile.js";
13
+
14
+ test("resolveTenancyProfile returns mode-specific workspace policy matrix", () => {
15
+ const noneProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_NONE });
16
+ assert.deepEqual(noneProfile, {
17
+ mode: TENANCY_MODE_NONE,
18
+ workspace: {
19
+ enabled: false,
20
+ autoProvision: false,
21
+ allowSelfCreate: false,
22
+ slugPolicy: WORKSPACE_SLUG_POLICY_NONE
23
+ }
24
+ });
25
+
26
+ const personalProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_PERSONAL });
27
+ assert.deepEqual(personalProfile, {
28
+ mode: TENANCY_MODE_PERSONAL,
29
+ workspace: {
30
+ enabled: true,
31
+ autoProvision: true,
32
+ allowSelfCreate: false,
33
+ slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
34
+ }
35
+ });
36
+
37
+ const workspaceProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACE });
38
+ assert.deepEqual(workspaceProfile, {
39
+ mode: TENANCY_MODE_WORKSPACE,
40
+ workspace: {
41
+ enabled: true,
42
+ autoProvision: false,
43
+ allowSelfCreate: false,
44
+ slugPolicy: WORKSPACE_SLUG_POLICY_USER_SELECTED
45
+ }
46
+ });
47
+ });
48
+
49
+ test("isWorkspaceTenancyMode is true only for workspace mode", () => {
50
+ assert.equal(isWorkspaceTenancyMode(TENANCY_MODE_WORKSPACE), true);
51
+ assert.equal(isWorkspaceTenancyMode(TENANCY_MODE_PERSONAL), false);
52
+ assert.equal(isWorkspaceTenancyMode(TENANCY_MODE_NONE), false);
53
+ });
54
+
55
+ test("resolveTenancyProfile allows explicit workspace self-create policy override", () => {
56
+ const workspaceProfile = resolveTenancyProfile({
57
+ tenancyMode: TENANCY_MODE_WORKSPACE,
58
+ tenancyPolicy: {
59
+ workspace: {
60
+ allowSelfCreate: true
61
+ }
62
+ }
63
+ });
64
+
65
+ assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACE);
66
+ assert.equal(workspaceProfile.workspace.allowSelfCreate, true);
67
+ });
@@ -0,0 +1,31 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
4
+ import "../test-support/registerDefaultSettingsFields.js";
5
+ import { userSettingsResource } from "../src/shared/resources/userSettingsResource.js";
6
+
7
+ function parseBody(operation, payload = {}) {
8
+ return validateOperationSection({
9
+ operation,
10
+ section: "bodyValidator",
11
+ value: payload
12
+ });
13
+ }
14
+
15
+ test("user settings preferences update keeps required string validation after normalization", () => {
16
+ const parsed = parseBody(userSettingsResource.operations.preferencesUpdate, {
17
+ theme: " "
18
+ });
19
+
20
+ assert.equal(parsed.ok, false);
21
+ assert.equal(typeof parsed.fieldErrors.theme, "string");
22
+ });
23
+
24
+ test("user settings notifications update rejects non-boolean values", () => {
25
+ const parsed = parseBody(userSettingsResource.operations.notificationsUpdate, {
26
+ productUpdates: "yes"
27
+ });
28
+
29
+ assert.equal(parsed.ok, false);
30
+ assert.equal(typeof parsed.fieldErrors.productUpdates, "string");
31
+ });
@@ -0,0 +1,31 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ normalizeSurfaceWorkspaceRequirement,
5
+ resolveApiBasePath
6
+ } from "../src/shared/support/usersApiPaths.js";
7
+
8
+ test("normalizeSurfaceWorkspaceRequirement only accepts explicit true", () => {
9
+ assert.equal(normalizeSurfaceWorkspaceRequirement(true), true);
10
+ assert.equal(normalizeSurfaceWorkspaceRequirement(false), false);
11
+ assert.equal(normalizeSurfaceWorkspaceRequirement("true"), false);
12
+ assert.equal(normalizeSurfaceWorkspaceRequirement(1), false);
13
+ });
14
+
15
+ test("resolveApiBasePath resolves workspace and non-workspace API base paths", () => {
16
+ assert.equal(
17
+ resolveApiBasePath({
18
+ surfaceRequiresWorkspace: true,
19
+ relativePath: "/customers"
20
+ }),
21
+ "/api/w/:workspaceSlug/workspace/customers"
22
+ );
23
+
24
+ assert.equal(
25
+ resolveApiBasePath({
26
+ surfaceRequiresWorkspace: false,
27
+ relativePath: "/customers"
28
+ }),
29
+ "/api/customers"
30
+ );
31
+ });