@jskit-ai/users-core 0.1.172 → 0.1.173

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 (58) hide show
  1. package/package.json +41 -253
  2. package/patterns/user-administration-server/PATTERN.md +64 -0
  3. package/patterns/user-administration-server/example/packages/users/package.json +42 -0
  4. package/patterns/user-administration-server/example/packages/users/src/server/UsersFeature.js +13 -0
  5. package/patterns/user-administration-server/example/packages/users/src/shared/index.js +1 -0
  6. package/patterns/user-administration-server/example/packages/users-workspace/package.json +43 -0
  7. package/patterns/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +22 -0
  8. package/patterns/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +56 -0
  9. package/src/server/UsersExtensionsProvider.js +14 -0
  10. package/src/server/UsersFeature.js +119 -0
  11. package/src/server/UsersIdentityProvider.js +57 -0
  12. package/src/server/accountNotifications/accountNotificationsActions.js +17 -5
  13. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +4 -6
  14. package/src/server/accountPreferences/accountPreferencesActions.js +17 -5
  15. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +4 -6
  16. package/src/server/accountProfile/accountProfileActions.js +28 -14
  17. package/src/server/accountProfile/bootAccountProfileRoutes.js +11 -10
  18. package/src/server/accountSecurity/accountSecurityActions.js +27 -17
  19. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +4 -7
  20. package/src/server/common/services/authProfileSyncService.js +11 -4
  21. package/src/server/common/support/realtimeServiceEvents.js +8 -11
  22. package/src/server/usersBootstrapContributor.js +4 -28
  23. package/src/server/usersExtensions.js +65 -0
  24. package/test/authProfileSyncService.test.js +2 -2
  25. package/test/exportsContract.test.js +6 -1
  26. package/test/featureRuntime.test.js +78 -0
  27. package/test/usersBootstrapContributor.test.js +1 -43
  28. package/test/usersPackageScaffoldContract.test.js +58 -204
  29. package/test/usersRouteRequestInputValidator.test.js +19 -43
  30. package/src/server/UsersCoreServiceProvider.js +0 -56
  31. package/src/server/accountNotifications/registerAccountNotifications.js +0 -37
  32. package/src/server/accountPreferences/registerAccountPreferences.js +0 -37
  33. package/src/server/accountProfile/registerAccountProfile.js +0 -56
  34. package/src/server/accountSecurity/registerAccountSecurity.js +0 -29
  35. package/src/server/common/registerCommonRepositories.js +0 -23
  36. package/src/server/common/registerSharedApi.js +0 -9
  37. package/src/server/profileSyncLifecycleContributorRegistry.js +0 -56
  38. package/src/server/registerUsersBootstrap.js +0 -20
  39. package/src/server/registerUsersCore.js +0 -22
  40. package/templates/packages/users/package.json +0 -82
  41. package/templates/packages/users/src/server/UsersProvider.js +0 -90
  42. package/templates/packages/users/src/server/actions.js +0 -68
  43. package/templates/packages/users/src/server/registerRoutes.js +0 -101
  44. package/templates/packages/users/src/server/repository.js +0 -51
  45. package/templates/packages/users/src/server/service.js +0 -32
  46. package/templates/packages/users-workspace/package.json +0 -83
  47. package/templates/packages/users-workspace/src/server/UsersProvider.js +0 -91
  48. package/templates/packages/users-workspace/src/server/actions.js +0 -77
  49. package/templates/packages/users-workspace/src/server/registerRoutes.js +0 -111
  50. package/test/providerLifecycle.test.js +0 -63
  51. package/test/registerCommonRepositories.test.js +0 -57
  52. package/test/registerServiceRealtimeEvents.test.js +0 -57
  53. package/test/registerUsersCore.test.js +0 -53
  54. /package/{templates/migrations → migrations}/users_core_generic_initial.cjs +0 -0
  55. /package/{templates/migrations → migrations}/users_core_profile_updated_at.cjs +0 -0
  56. /package/{templates/migrations → migrations}/users_core_profile_username.cjs +0 -0
  57. /package/{templates → patterns/user-administration-server/example}/packages/users/src/shared/userResource.js +0 -0
  58. /package/{templates/packages/users → patterns/user-administration-server/example/packages/users-workspace}/src/shared/index.js +0 -0
@@ -64,7 +64,11 @@ function normalizeLifecycleContributors(entries = []) {
64
64
  );
65
65
  }
66
66
 
67
- function createService({ userProfilesRepository, lifecycleContributors = [], userSettingsRepository = null } = {}) {
67
+ function createService({
68
+ userProfilesRepository,
69
+ resolveLifecycleContributors = () => [],
70
+ userSettingsRepository = null
71
+ } = {}) {
68
72
  if (!userProfilesRepository || typeof userProfilesRepository.findByIdentity !== "function") {
69
73
  throw new Error("authProfileSyncService requires userProfilesRepository.findByIdentity().");
70
74
  }
@@ -78,7 +82,9 @@ function createService({ userProfilesRepository, lifecycleContributors = [], use
78
82
  throw new Error("authProfileSyncService requires userSettingsRepository.ensureForUserId().");
79
83
  }
80
84
 
81
- const normalizedLifecycleContributors = normalizeLifecycleContributors(lifecycleContributors);
85
+ if (typeof resolveLifecycleContributors !== "function") {
86
+ throw new Error("authProfileSyncService requires resolveLifecycleContributors().");
87
+ }
82
88
 
83
89
  async function findByIdentity(identityLike, options = {}) {
84
90
  const normalized = buildNormalizedIdentityKey(identityLike);
@@ -107,6 +113,7 @@ function createService({ userProfilesRepository, lifecycleContributors = [], use
107
113
 
108
114
  async function syncIdentityProfile(profileLike, options = {}) {
109
115
  const normalized = buildNormalizedIdentityProfile(profileLike);
116
+ const lifecycleContributors = normalizeLifecycleContributors(resolveLifecycleContributors());
110
117
 
111
118
  const runSync = async (trx = null) => {
112
119
  const operationOptions = trx ? { ...options, trx } : options;
@@ -115,7 +122,7 @@ function createService({ userProfilesRepository, lifecycleContributors = [], use
115
122
  if (!profileNeedsUpdate(existing, normalized)) {
116
123
  const synchronizedProfile = requireSynchronizedProfile(existing);
117
124
  await userSettingsRepository.ensureForUserId(synchronizedProfile.id, operationOptions);
118
- for (const contributor of normalizedLifecycleContributors) {
125
+ for (const contributor of lifecycleContributors) {
119
126
  await contributor.afterIdentityProfileSynced({
120
127
  profile: synchronizedProfile,
121
128
  created,
@@ -129,7 +136,7 @@ function createService({ userProfilesRepository, lifecycleContributors = [], use
129
136
  const synchronizedProfile = requireSynchronizedProfile(upserted);
130
137
  await userSettingsRepository.ensureForUserId(synchronizedProfile.id, operationOptions);
131
138
  created = !existing;
132
- for (const contributor of normalizedLifecycleContributors) {
139
+ for (const contributor of lifecycleContributors) {
133
140
  await contributor.afterIdentityProfileSynced({
134
141
  profile: synchronizedProfile,
135
142
  created,
@@ -1,13 +1,11 @@
1
- import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
2
- import { deepFreeze } from "./deepFreeze.js";
1
+ import { createEntityChangedActionEvent } from "@jskit-ai/kernel/server/actions";
3
2
 
4
- function resolveActorScopedEntityId({ options } = {}) {
5
- return normalizeRecordId(options?.context?.actor?.id, { fallback: "" });
3
+ function resolveActorScopedEntityId({ context } = {}) {
4
+ return context?.actor?.id;
6
5
  }
7
6
 
8
- const ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS = deepFreeze([
9
- {
10
- type: "entity.changed",
7
+ const ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS = Object.freeze([
8
+ createEntityChangedActionEvent({
11
9
  source: "account",
12
10
  entity: "settings",
13
11
  operation: "updated",
@@ -16,9 +14,8 @@ const ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS = deepFreeze([
16
14
  event: "account.settings.changed",
17
15
  audience: "actor_user"
18
16
  }
19
- },
20
- {
21
- type: "entity.changed",
17
+ }),
18
+ createEntityChangedActionEvent({
22
19
  source: "users",
23
20
  entity: "bootstrap",
24
21
  operation: "updated",
@@ -27,7 +24,7 @@ const ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS = deepFreeze([
27
24
  event: "users.bootstrap.changed",
28
25
  audience: "actor_user"
29
26
  }
30
- }
27
+ })
31
28
  ]);
32
29
 
33
30
  export { ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS };
@@ -1,7 +1,7 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime";
2
2
  import { requireServiceMethod } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
3
3
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
- import { normalizeBoolean, normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
5
5
  import { accountAvatarFormatter } from "./common/formatters/accountAvatarFormatter.js";
6
6
  import { USER_SETTINGS_BOOTSTRAP_KEYS } from "../shared/resources/userSettingsResource.js";
7
7
 
@@ -30,34 +30,13 @@ function getOAuthProviderCatalogPayload(authService) {
30
30
  };
31
31
  }
32
32
 
33
- function resolveBooleanConfigValue(value, fallback) {
34
- if (value === undefined || value === null || value === "") {
35
- return fallback;
36
- }
37
-
38
- return normalizeBoolean(value);
39
- }
40
-
41
- function resolveAppState(appConfig = {}) {
42
- const features = {
43
- assistantEnabled: resolveBooleanConfigValue(appConfig.assistantEnabled, false),
44
- assistantRequiredPermission: normalizeText(appConfig.assistantRequiredPermission),
45
- socialEnabled: resolveBooleanConfigValue(appConfig.socialEnabled, false),
46
- socialFederationEnabled: resolveBooleanConfigValue(appConfig.socialFederationEnabled, false)
47
- };
48
-
49
- return {
50
- features
51
- };
52
- }
53
-
54
- function createAnonymousBootstrapPayload({ appState, surfaceAccess = {} }) {
33
+ function createAnonymousBootstrapPayload({ surfaceAccess = {} }) {
55
34
  return {
56
35
  session: {
57
36
  authenticated: false
58
37
  },
59
38
  profile: null,
60
- app: appState,
39
+ app: {},
61
40
  surfaceAccess: normalizeObject(surfaceAccess),
62
41
  userSettings: null,
63
42
  requestMeta: {
@@ -80,11 +59,9 @@ function mapUserSettingsBootstrap(settings = {}) {
80
59
  function createUsersBootstrapContributor({
81
60
  userProfilesRepository,
82
61
  userSettingsRepository,
83
- appConfig = {},
84
62
  authService
85
63
  } = {}) {
86
64
  const contributorId = "users.bootstrap";
87
- const appState = resolveAppState(appConfig);
88
65
 
89
66
  requireServiceMethod(userProfilesRepository, "findById", contributorId, {
90
67
  serviceLabel: "internal.repository.user-profiles"
@@ -114,7 +91,6 @@ function createUsersBootstrapContributor({
114
91
  const normalizedUser = authResult?.authenticated === true ? authResult?.actor || null : null;
115
92
  const inheritedSurfaceAccess = normalizeObject(existingPayload?.surfaceAccess);
116
93
  let payload = createAnonymousBootstrapPayload({
117
- appState,
118
94
  surfaceAccess: inheritedSurfaceAccess
119
95
  });
120
96
 
@@ -132,7 +108,7 @@ function createUsersBootstrapContributor({
132
108
  email: latestProfile.email,
133
109
  avatar: accountAvatarFormatter(latestProfile, userSettings)
134
110
  },
135
- app: appState,
111
+ app: {},
136
112
  surfaceAccess: inheritedSurfaceAccess,
137
113
  userSettings: mapUserSettingsBootstrap(userSettings),
138
114
  requestMeta: {
@@ -0,0 +1,65 @@
1
+ function normalizeContributor(value) {
2
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3
+ throw new TypeError("Profile synchronization lifecycle contributor must be an object.");
4
+ }
5
+ const contributorId = String(value.contributorId || "").trim();
6
+ if (!contributorId) {
7
+ throw new TypeError("Profile synchronization lifecycle contributor requires contributorId.");
8
+ }
9
+ if (typeof value.afterIdentityProfileSynced !== "function") {
10
+ throw new TypeError(`Profile synchronization lifecycle contributor "${contributorId}" requires afterIdentityProfileSynced().`);
11
+ }
12
+ return Object.freeze({
13
+ ...value,
14
+ contributorId,
15
+ order: Number.isFinite(value.order) ? Number(value.order) : 0,
16
+ afterIdentityProfileSynced: value.afterIdentityProfileSynced
17
+ });
18
+ }
19
+
20
+ function createUsersExtensions() {
21
+ const contributors = [];
22
+ const contributorIds = new Set();
23
+ let sealed = false;
24
+
25
+ function registerProfileSyncLifecycleContributor(value) {
26
+ if (sealed) {
27
+ throw new Error("User extension registration is closed after profile synchronization starts.");
28
+ }
29
+ const contributor = normalizeContributor(value);
30
+ if (contributorIds.has(contributor.contributorId)) {
31
+ throw new Error(`Profile synchronization lifecycle contributor "${contributor.contributorId}" is duplicated.`);
32
+ }
33
+ contributorIds.add(contributor.contributorId);
34
+ contributors.push(contributor);
35
+ return api;
36
+ }
37
+
38
+ function profileSyncLifecycleContributors() {
39
+ sealed = true;
40
+ return Object.freeze(
41
+ contributors
42
+ .map((contributor, index) => ({ contributor, index }))
43
+ .sort((left, right) => left.contributor.order - right.contributor.order || left.index - right.index)
44
+ .map(({ contributor }) => contributor)
45
+ );
46
+ }
47
+
48
+ function diagnostics() {
49
+ return Object.freeze({
50
+ sealed,
51
+ profileSyncLifecycleContributorIds: Object.freeze(
52
+ contributors.map((entry) => entry.contributorId).sort()
53
+ )
54
+ });
55
+ }
56
+
57
+ const api = Object.freeze({
58
+ registerProfileSyncLifecycleContributor,
59
+ profileSyncLifecycleContributors,
60
+ diagnostics
61
+ });
62
+ return api;
63
+ }
64
+
65
+ export { createUsersExtensions };
@@ -33,7 +33,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
33
33
  return { userId: Number(userId) };
34
34
  }
35
35
  },
36
- lifecycleContributors: [
36
+ resolveLifecycleContributors: () => [
37
37
  {
38
38
  contributorId: "test.lifecycle",
39
39
  async afterIdentityProfileSynced({ created, options = {} } = {}) {
@@ -91,7 +91,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
91
91
  return { userId: "7" };
92
92
  }
93
93
  },
94
- lifecycleContributors: [
94
+ resolveLifecycleContributors: () => [
95
95
  {
96
96
  contributorId: "test.lifecycle",
97
97
  async afterIdentityProfileSynced({ created } = {}) {
@@ -12,7 +12,12 @@ test("users-core exports are explicit and aligned with production/template usage
12
12
  const result = evaluatePackageExportsContract({
13
13
  repoRoot: REPO_ROOT,
14
14
  packageDir: PACKAGE_DIR,
15
- packageId: "@jskit-ai/users-core"
15
+ packageId: "@jskit-ai/users-core",
16
+ requiredExports: [
17
+ "./server/UsersFeature",
18
+ "./server/UsersIdentityProvider",
19
+ "./server/usersExtensions"
20
+ ]
16
21
  });
17
22
 
18
23
  assert.deepEqual(
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
4
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
5
+ import { createBootstrapRuntime } from "@jskit-ai/kernel/server/runtime";
6
+ import { createAuthExtensions } from "@jskit-ai/auth-core/server/authExtensions";
7
+ import { UsersExtensionsProvider } from "../src/server/UsersExtensionsProvider.js";
8
+ import { UsersFeature } from "../src/server/UsersFeature.js";
9
+ import { UsersIdentityProvider } from "../src/server/UsersIdentityProvider.js";
10
+
11
+ function createJsonRestApiFixture() {
12
+ const resources = {};
13
+ return {
14
+ resources,
15
+ async addResource(scopeName) {
16
+ resources[scopeName] = Object.freeze({
17
+ query: async () => ({ data: [] }),
18
+ post: async () => null,
19
+ patch: async () => null
20
+ });
21
+ }
22
+ };
23
+ }
24
+
25
+ test("UsersFeature assembles account behavior through explicit capabilities", async () => {
26
+ const routes = [];
27
+ const bootstrap = createBootstrapRuntime();
28
+ const authExtensions = createAuthExtensions();
29
+ const jsonRestApi = createJsonRestApiFixture();
30
+ let users = null;
31
+ let actionCatalogue = null;
32
+ const probe = defineProvider({
33
+ id: "test.users.probe",
34
+ requires: {
35
+ actions: "runtime.actions",
36
+ usersCapability: "users.core"
37
+ },
38
+ setup({ actions, usersCapability }) {
39
+ actionCatalogue = actions;
40
+ users = usersCapability;
41
+ return {};
42
+ }
43
+ });
44
+ const runtime = createCapabilityRuntime({
45
+ inputs: {
46
+ "auth.extensions": authExtensions,
47
+ "auth.service": {},
48
+ "runtime.bootstrap": bootstrap,
49
+ "runtime.database": { knex() {} },
50
+ "runtime.http": {
51
+ router: {
52
+ register(method, path) {
53
+ routes.push(`${method} ${path}`);
54
+ }
55
+ }
56
+ },
57
+ "runtime.json-rest-api": jsonRestApi,
58
+ "runtime.storage": {
59
+ async getItemRaw() { return null; },
60
+ async setItemRaw() {},
61
+ async removeItem() {}
62
+ },
63
+ "runtime.uploads": { readSingleMultipartFile() {} }
64
+ },
65
+ providers: [createActionProvider(), UsersExtensionsProvider, UsersIdentityProvider, UsersFeature, probe]
66
+ });
67
+
68
+ await runtime.start();
69
+
70
+ assert.deepEqual(Object.keys(jsonRestApi.resources).sort(), ["userProfiles", "userSettings"]);
71
+ assert.equal(typeof users.repositories.userProfiles.findByIdentity, "function");
72
+ assert.equal(typeof users.services.accountProfile.updateProfile, "function");
73
+ assert.equal(routes.includes("GET /api/settings"), true);
74
+ assert.equal(routes.includes("POST /api/settings/security/change-password"), true);
75
+ assert.equal(actionCatalogue.listDefinitions().length, 11);
76
+ assert.deepEqual(bootstrap.diagnostics().contributorIds, ["users.bootstrap"]);
77
+ assert.equal(authExtensions.resolveProfileProjector().projectorId, "users.profile");
78
+ });
@@ -93,7 +93,7 @@ test("users bootstrap contributor exposes the generic authenticated bootstrap pa
93
93
  assert.deepEqual(payload.surfaceAccess, {
94
94
  consoleowner: true
95
95
  });
96
- assert.equal(payload.app.features.assistantEnabled, false);
96
+ assert.deepEqual(payload.app, {});
97
97
  assert.deepEqual(payload.session.oauthProviders, [
98
98
  {
99
99
  id: "google",
@@ -117,9 +117,6 @@ test("users bootstrap contributor emits anonymous bootstrap payload without work
117
117
  return createUserSettings();
118
118
  }
119
119
  },
120
- appConfig: {
121
- tenancyMode: "none"
122
- },
123
120
  authService: {
124
121
  getOAuthProviderCatalog() {
125
122
  return {
@@ -156,42 +153,3 @@ test("users bootstrap contributor emits anonymous bootstrap payload without work
156
153
  });
157
154
  assert.equal(payload.userSettings, null);
158
155
  });
159
-
160
- test("users bootstrap contributor uses shared boolean normalization for app feature flags", async () => {
161
- const contributor = createUsersBootstrapContributor({
162
- userProfilesRepository: {
163
- async findById() {
164
- return null;
165
- }
166
- },
167
- userSettingsRepository: {
168
- async ensureForUserId() {
169
- return createUserSettings();
170
- }
171
- },
172
- appConfig: {
173
- assistantEnabled: "yes",
174
- socialEnabled: 0,
175
- socialFederationEnabled: "no"
176
- }
177
- });
178
-
179
- const payload = await contributor.contribute({
180
- request: {
181
- async executeAction() {
182
- return {
183
- authenticated: false
184
- };
185
- }
186
- },
187
- payload: {},
188
- reply: {}
189
- });
190
-
191
- assert.deepEqual(payload.app.features, {
192
- assistantEnabled: true,
193
- assistantRequiredPermission: "",
194
- socialEnabled: false,
195
- socialFederationEnabled: false
196
- });
197
- });