@jskit-ai/users-core 0.1.55 → 0.1.57

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 (47) hide show
  1. package/package.descriptor.mjs +175 -6
  2. package/package.json +6 -6
  3. package/src/server/accountNotifications/accountNotificationsService.js +3 -3
  4. package/src/server/accountNotifications/registerAccountNotifications.js +2 -2
  5. package/src/server/accountPreferences/accountPreferencesService.js +3 -3
  6. package/src/server/accountPreferences/registerAccountPreferences.js +2 -2
  7. package/src/server/accountProfile/accountProfileService.js +5 -5
  8. package/src/server/accountProfile/avatarService.js +6 -6
  9. package/src/server/accountProfile/registerAccountProfile.js +3 -3
  10. package/src/server/accountSecurity/accountSecurityService.js +3 -3
  11. package/src/server/accountSecurity/registerAccountSecurity.js +2 -2
  12. package/src/server/common/registerCommonRepositories.js +5 -5
  13. package/src/server/common/repositories/{usersRepository.js → userProfilesRepository.js} +117 -95
  14. package/src/server/common/repositories/userSettingsRepository.js +12 -11
  15. package/src/server/common/resources/userProfilesResource.js +203 -0
  16. package/src/server/common/services/accountContextService.js +4 -4
  17. package/src/server/common/services/authProfileSyncService.js +11 -11
  18. package/src/server/common/support/identity.js +17 -0
  19. package/src/server/registerUsersBootstrap.js +2 -2
  20. package/src/server/registerUsersCore.js +2 -2
  21. package/src/server/usersBootstrapContributor.js +5 -5
  22. package/src/shared/resources/userProfileResource.js +1 -1
  23. package/src/shared/resources/userSettingsFields.js +10 -4
  24. package/src/shared/resources/userSettingsResource.js +1 -1
  25. package/templates/packages/main/src/shared/resources/userSettingsFields.js +10 -10
  26. package/templates/packages/users/package.descriptor.mjs +65 -0
  27. package/templates/packages/users/package.json +10 -0
  28. package/templates/packages/users/src/server/UsersProvider.js +91 -0
  29. package/templates/packages/users/src/server/actionIds.js +6 -0
  30. package/templates/packages/users/src/server/actions.js +73 -0
  31. package/templates/packages/users/src/server/listConfig.js +16 -0
  32. package/templates/packages/users/src/server/registerRoutes.js +87 -0
  33. package/templates/packages/users/src/server/repository.js +41 -0
  34. package/templates/packages/users/src/server/service.js +28 -0
  35. package/templates/packages/users/src/shared/index.js +1 -0
  36. package/templates/packages/users/src/shared/userResource.js +74 -0
  37. package/templates/packages/users-workspace/package.descriptor.mjs +66 -0
  38. package/templates/packages/users-workspace/src/server/UsersProvider.js +92 -0
  39. package/templates/packages/users-workspace/src/server/actions.js +74 -0
  40. package/templates/packages/users-workspace/src/server/registerRoutes.js +93 -0
  41. package/test/authProfileSyncService.test.js +3 -3
  42. package/test/avatarService.test.js +2 -2
  43. package/test/registerCommonRepositories.test.js +37 -0
  44. package/test/repositoryContracts.test.js +48 -5
  45. package/test/usersBootstrapContributor.test.js +2 -2
  46. package/test/usersPackageScaffoldContract.test.js +98 -0
  47. package/test/usersRouteResources.test.js +1 -1
@@ -0,0 +1,66 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@local/users",
4
+ version: "0.1.0",
5
+ kind: "runtime",
6
+ description: "App-local CRUD package (users).",
7
+ dependsOn: [
8
+ "@jskit-ai/auth-core",
9
+ "@jskit-ai/crud-core",
10
+ "@jskit-ai/database-runtime",
11
+ "@jskit-ai/http-runtime",
12
+ "@jskit-ai/users-core",
13
+ "@jskit-ai/workspaces-core"
14
+ ],
15
+ capabilities: {
16
+ provides: [
17
+ "crud.users"
18
+ ],
19
+ requires: [
20
+ "runtime.actions",
21
+ "runtime.database",
22
+ "auth.policy"
23
+ ]
24
+ },
25
+ runtime: {
26
+ server: {
27
+ providers: [
28
+ {
29
+ entrypoint: "src/server/UsersProvider.js",
30
+ export: "UsersProvider"
31
+ }
32
+ ]
33
+ }
34
+ },
35
+ metadata: {
36
+ apiSummary: {
37
+ surfaces: [
38
+ {
39
+ subpath: "./server/actionIds",
40
+ summary: "App-local CRUD public action identifiers."
41
+ },
42
+ {
43
+ subpath: "./shared",
44
+ summary: "App-local CRUD shared resource."
45
+ }
46
+ ],
47
+ containerTokens: {
48
+ server: [
49
+ "repository.users",
50
+ "crud.users"
51
+ ]
52
+ }
53
+ }
54
+ },
55
+ mutations: {
56
+ dependencies: {
57
+ runtime: {},
58
+ dev: {}
59
+ },
60
+ packageJson: {
61
+ scripts: {}
62
+ },
63
+ procfile: {},
64
+ files: []
65
+ }
66
+ });
@@ -0,0 +1,92 @@
1
+ import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
2
+ import { resolveCrudSurfacePolicyFromAppConfig } from "@jskit-ai/crud-core/server/crudModuleConfig";
3
+ import {
4
+ createCrudLookupResolver,
5
+ createCrudLookup
6
+ } from "@jskit-ai/crud-core/server/lookups";
7
+ import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
8
+ import { createRepository } from "./repository.js";
9
+ import {
10
+ createService,
11
+ serviceEvents
12
+ } from "./service.js";
13
+ import { createActions } from "./actions.js";
14
+ import { registerRoutes } from "./registerRoutes.js";
15
+
16
+ const CRUD_MODULE_CONFIG = Object.freeze({
17
+ namespace: "users",
18
+ surface: "admin",
19
+ ownershipFilter: "public",
20
+ relativePath: "/users"
21
+ });
22
+
23
+ function resolveCrudPolicyFromApp(app) {
24
+ return resolveCrudSurfacePolicyFromAppConfig(CRUD_MODULE_CONFIG, resolveAppConfig(app), {
25
+ context: "UsersProvider"
26
+ });
27
+ }
28
+
29
+ class UsersProvider {
30
+ static id = "crud.users";
31
+
32
+ static dependsOn = ["runtime.actions", "runtime.database", "auth.policy.fastify", "local.main"];
33
+
34
+ register(app) {
35
+ if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
36
+ throw new Error("UsersProvider requires application singleton()/service()/actions().");
37
+ }
38
+
39
+ const crudPolicy = resolveCrudPolicyFromApp(app);
40
+
41
+ app.singleton("repository.users", (scope) => {
42
+ const knex = scope.make("jskit.database.knex");
43
+ return createRepository(knex, {
44
+ resolveLookup: createCrudLookupResolver(scope)
45
+ });
46
+ });
47
+
48
+ app.singleton("lookup.users", (scope) => {
49
+ return createCrudLookup(scope.make("repository.users"), {
50
+ ownershipFilter: crudPolicy.ownershipFilter
51
+ });
52
+ });
53
+
54
+ app.service(
55
+ "crud.users",
56
+ (scope) => {
57
+ return createService({
58
+ usersRepository: scope.make("repository.users")
59
+ });
60
+ },
61
+ {
62
+ events: serviceEvents
63
+ }
64
+ );
65
+
66
+ app.actions(
67
+ withActionDefaults(
68
+ createActions({
69
+ surface: crudPolicy.surfaceId
70
+ }),
71
+ {
72
+ domain: "crud",
73
+ dependencies: {
74
+ usersService: "crud.users"
75
+ }
76
+ }
77
+ )
78
+ );
79
+ }
80
+
81
+ boot(app) {
82
+ const crudPolicy = resolveCrudPolicyFromApp(app);
83
+ registerRoutes(app, {
84
+ routeOwnershipFilter: crudPolicy.ownershipFilter,
85
+ routeSurface: crudPolicy.surfaceId,
86
+ routeSurfaceRequiresWorkspace: crudPolicy.surfaceDefinition.requiresWorkspace === true,
87
+ routeRelativePath: crudPolicy.relativePath
88
+ });
89
+ }
90
+ }
91
+
92
+ export { UsersProvider };
@@ -0,0 +1,74 @@
1
+ import { recordIdParamsValidator } from "@jskit-ai/kernel/shared/validators";
2
+ import {
3
+ createCrudCursorPaginationQueryValidator,
4
+ listSearchQueryValidator
5
+ } from "@jskit-ai/crud-core/server/listQueryValidators";
6
+ import { workspaceSlugParamsValidator } from "@jskit-ai/workspaces-core/server/validators/routeParamsValidator";
7
+ import { resource } from "../shared/userResource.js";
8
+ import { actionIds } from "./actionIds.js";
9
+ import { LIST_CONFIG } from "./listConfig.js";
10
+
11
+ const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
12
+ const authenticatedPermission = Object.freeze({
13
+ require: "authenticated"
14
+ });
15
+
16
+ function requireActionSurface(surface = "") {
17
+ const normalizedSurface = String(surface || "").trim().toLowerCase();
18
+ if (!normalizedSurface) {
19
+ throw new TypeError("createActions requires a non-empty surface.");
20
+ }
21
+
22
+ return normalizedSurface;
23
+ }
24
+
25
+ function createActions({ surface = "" } = {}) {
26
+ const actionSurface = requireActionSurface(surface);
27
+
28
+ return Object.freeze([
29
+ {
30
+ id: actionIds.list,
31
+ version: 1,
32
+ kind: "query",
33
+ channels: ["api", "automation", "internal"],
34
+ surfaces: [actionSurface],
35
+ permission: authenticatedPermission,
36
+ inputValidator: [workspaceSlugParamsValidator, listCursorPaginationQueryValidator, listSearchQueryValidator],
37
+ outputValidator: resource.operations.list.outputValidator,
38
+ idempotency: "none",
39
+ audit: {
40
+ actionName: actionIds.list
41
+ },
42
+ observability: {},
43
+ async execute(input, context, deps) {
44
+ return deps.usersService.listRecords(input, {
45
+ context,
46
+ visibilityContext: context?.visibilityContext
47
+ });
48
+ }
49
+ },
50
+ {
51
+ id: actionIds.view,
52
+ version: 1,
53
+ kind: "query",
54
+ channels: ["api", "automation", "internal"],
55
+ surfaces: [actionSurface],
56
+ permission: authenticatedPermission,
57
+ inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
58
+ outputValidator: resource.operations.view.outputValidator,
59
+ idempotency: "none",
60
+ audit: {
61
+ actionName: actionIds.view
62
+ },
63
+ observability: {},
64
+ async execute(input, context, deps) {
65
+ return deps.usersService.getRecord(input.recordId, {
66
+ context,
67
+ visibilityContext: context?.visibilityContext
68
+ });
69
+ }
70
+ }
71
+ ]);
72
+ }
73
+
74
+ export { createActions };
@@ -0,0 +1,93 @@
1
+ import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
+ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
+ import {
4
+ createCrudCursorPaginationQueryValidator,
5
+ listSearchQueryValidator
6
+ } from "@jskit-ai/crud-core/server/listQueryValidators";
7
+ import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
8
+ import { checkRouteVisibility } from "@jskit-ai/kernel/shared/support/visibility";
9
+ import { recordIdParamsValidator } from "@jskit-ai/kernel/shared/validators";
10
+ import { routeParamsValidator } from "@jskit-ai/workspaces-core/server/validators/routeParamsValidator";
11
+ import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/workspaces-core/server/support/workspaceRouteInput";
12
+ import { actionIds } from "./actionIds.js";
13
+ import { LIST_CONFIG } from "./listConfig.js";
14
+ import { resource } from "../shared/userResource.js";
15
+
16
+ const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
17
+
18
+ function registerRoutes(
19
+ app,
20
+ {
21
+ routeOwnershipFilter = "public",
22
+ routeSurface = "",
23
+ routeSurfaceRequiresWorkspace = false,
24
+ routeRelativePath = ""
25
+ } = {}
26
+ ) {
27
+ const router = app.make("jskit.http.router");
28
+ const normalizedRouteSurface = normalizeSurfaceId(routeSurface);
29
+ const routeBase = resolveScopedApiBasePath({
30
+ routeBase: routeSurfaceRequiresWorkspace === true ? "/w/:workspaceSlug" : "/",
31
+ relativePath: routeRelativePath,
32
+ strictParams: false
33
+ });
34
+
35
+ router.register(
36
+ "GET",
37
+ routeBase,
38
+ {
39
+ auth: "required",
40
+ surface: normalizedRouteSurface,
41
+ visibility: checkRouteVisibility(routeOwnershipFilter),
42
+ meta: {
43
+ tags: ["crud"],
44
+ summary: "List users."
45
+ },
46
+ paramsValidator: routeParamsValidator,
47
+ queryValidator: [listCursorPaginationQueryValidator, listSearchQueryValidator],
48
+ responseValidators: withStandardErrorResponses({
49
+ 200: resource.operations.list.outputValidator
50
+ })
51
+ },
52
+ async function (request, reply) {
53
+ const response = await request.executeAction({
54
+ actionId: actionIds.list,
55
+ input: {
56
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
57
+ ...(request.input.query || {})
58
+ }
59
+ });
60
+ reply.code(200).send(response);
61
+ }
62
+ );
63
+
64
+ router.register(
65
+ "GET",
66
+ `${routeBase}/:recordId`,
67
+ {
68
+ auth: "required",
69
+ surface: normalizedRouteSurface,
70
+ visibility: checkRouteVisibility(routeOwnershipFilter),
71
+ meta: {
72
+ tags: ["crud"],
73
+ summary: "View a user."
74
+ },
75
+ paramsValidator: [routeParamsValidator, recordIdParamsValidator],
76
+ responseValidators: withStandardErrorResponses({
77
+ 200: resource.operations.view.outputValidator
78
+ })
79
+ },
80
+ async function (request, reply) {
81
+ const response = await request.executeAction({
82
+ actionId: actionIds.view,
83
+ input: {
84
+ ...buildWorkspaceInputFromRouteParams(request.input.params),
85
+ recordId: request.input.params.recordId
86
+ }
87
+ });
88
+ reply.code(200).send(response);
89
+ }
90
+ );
91
+ }
92
+
93
+ export { registerRoutes };
@@ -7,7 +7,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
7
7
  const transaction = { trxId: "tx-1" };
8
8
 
9
9
  const service = createService({
10
- usersRepository: {
10
+ userProfilesRepository: {
11
11
  async findByIdentity(_identity, options = {}) {
12
12
  calls.push({ step: "find", trx: options.trx || null });
13
13
  return null;
@@ -68,7 +68,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
68
68
  let provisionCalls = 0;
69
69
 
70
70
  const service = createService({
71
- usersRepository: {
71
+ userProfilesRepository: {
72
72
  async findByIdentity() {
73
73
  return {
74
74
  id: "7",
@@ -118,7 +118,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
118
118
  test("authProfileSyncService.findByIdentity normalizes provider identity input", async () => {
119
119
  let capturedIdentity = null;
120
120
  const service = createService({
121
- usersRepository: {
121
+ userProfilesRepository: {
122
122
  async findByIdentity(identity) {
123
123
  capturedIdentity = identity;
124
124
  return null;
@@ -59,7 +59,7 @@ test("avatarService uploadForUser stores bytes and updates profile avatar fields
59
59
  };
60
60
 
61
61
  const avatarService = createService({
62
- usersRepository: repository,
62
+ userProfilesRepository: repository,
63
63
  avatarStorageService
64
64
  });
65
65
 
@@ -99,7 +99,7 @@ test("avatarService clearForUser removes stored avatar and clears profile fields
99
99
  };
100
100
 
101
101
  const avatarService = createService({
102
- usersRepository: repository,
102
+ userProfilesRepository: repository,
103
103
  avatarStorageService
104
104
  });
105
105
 
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { registerCommonRepositories } from "../src/server/common/registerCommonRepositories.js";
4
+
5
+ test("registerCommonRepositories exposes the shared users-core repositories", async () => {
6
+ const bindings = new Map();
7
+ const app = {
8
+ singleton(token, factory) {
9
+ bindings.set(token, factory);
10
+ return this;
11
+ }
12
+ };
13
+
14
+ registerCommonRepositories(app);
15
+
16
+ assert.equal(typeof bindings.get("internal.repository.user-settings"), "function");
17
+ assert.equal(typeof bindings.get("internal.repository.user-profiles"), "function");
18
+
19
+ const scope = {
20
+ make(token) {
21
+ assert.equal(token, "jskit.database.knex");
22
+ return Object.assign(() => {
23
+ throw new Error("query execution not expected");
24
+ }, {
25
+ async transaction(work) {
26
+ return work({ trxId: "trx-1" });
27
+ }
28
+ });
29
+ }
30
+ };
31
+
32
+ const settingsRepository = bindings.get("internal.repository.user-settings")(scope);
33
+ const profileRepository = bindings.get("internal.repository.user-profiles")(scope);
34
+
35
+ assert.equal(typeof settingsRepository.findByUserId, "function");
36
+ assert.equal(typeof profileRepository.findById, "function");
37
+ });
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { createRepository as createUsersRepository } from "../src/server/common/repositories/usersRepository.js";
3
+ import { createRepository as createUserProfilesRepository } from "../src/server/common/repositories/userProfilesRepository.js";
4
4
  import { createRepository as createUserSettingsRepository } from "../src/server/common/repositories/userSettingsRepository.js";
5
5
 
6
6
  function createKnexStub() {
@@ -17,10 +17,7 @@ function createKnexStub() {
17
17
 
18
18
  test("users-core repositories expose withTransaction", async () => {
19
19
  const knex = createKnexStub();
20
- const repositories = [
21
- createUsersRepository(knex),
22
- createUserSettingsRepository(knex)
23
- ];
20
+ const repositories = [createUserProfilesRepository(knex), createUserSettingsRepository(knex)];
24
21
 
25
22
  for (const repository of repositories) {
26
23
  assert.equal(typeof repository.withTransaction, "function");
@@ -28,3 +25,49 @@ test("users-core repositories expose withTransaction", async () => {
28
25
  assert.deepEqual(result, { id: "trx-1" });
29
26
  }
30
27
  });
28
+
29
+ function createFindByEmailKnexStub(expectedRow) {
30
+ const calls = [];
31
+ const knex = Object.assign((tableName) => {
32
+ assert.equal(tableName, "users");
33
+ return {
34
+ where(criteria) {
35
+ calls.push(criteria);
36
+ return {
37
+ async first() {
38
+ return expectedRow;
39
+ }
40
+ };
41
+ }
42
+ };
43
+ }, {
44
+ async transaction(work) {
45
+ return work({ trxId: "trx-1" });
46
+ }
47
+ });
48
+
49
+ return { knex, calls };
50
+ }
51
+
52
+ test("userProfilesRepository.findByEmail normalizes email lookup", async () => {
53
+ const { knex, calls } = createFindByEmailKnexStub({
54
+ id: 7,
55
+ auth_provider: "supabase",
56
+ auth_provider_user_sid: "supabase-user-7",
57
+ email: "ada@example.com",
58
+ username: "ada",
59
+ display_name: "Ada Example",
60
+ avatar_storage_key: null,
61
+ avatar_version: null,
62
+ avatar_updated_at: null,
63
+ created_at: "2026-04-20T00:00:00.000Z"
64
+ });
65
+ const repository = createUserProfilesRepository(knex);
66
+
67
+ const profile = await repository.findByEmail(" ADA@EXAMPLE.COM ");
68
+
69
+ assert.deepEqual(calls, [{ email: "ada@example.com" }]);
70
+ assert.equal(profile?.id, "7");
71
+ assert.equal(profile?.email, "ada@example.com");
72
+ assert.equal(profile?.displayName, "Ada Example");
73
+ });
@@ -33,7 +33,7 @@ test("users bootstrap contributor exposes the generic authenticated bootstrap pa
33
33
  const profile = createAuthenticatedProfile({ id: "12" });
34
34
  const writtenSessions = [];
35
35
  const contributor = createUsersBootstrapContributor({
36
- usersRepository: {
36
+ userProfilesRepository: {
37
37
  async findById() {
38
38
  return profile;
39
39
  }
@@ -103,7 +103,7 @@ test("users bootstrap contributor exposes the generic authenticated bootstrap pa
103
103
 
104
104
  test("users bootstrap contributor emits anonymous bootstrap payload without workspace fields", async () => {
105
105
  const contributor = createUsersBootstrapContributor({
106
- usersRepository: {
106
+ userProfilesRepository: {
107
107
  async findById() {
108
108
  return null;
109
109
  }
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import test from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import descriptor from "../package.descriptor.mjs";
7
+
8
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
9
+ const PACKAGE_ROOT = path.resolve(TEST_DIRECTORY, "..");
10
+
11
+ function readFileMutationById(id) {
12
+ return descriptor.mutations.files.find((entry) => entry.id === id) || null;
13
+ }
14
+
15
+ test("users-core installs the app-local users package scaffold", () => {
16
+ assert.equal(descriptor.mutations.dependencies.runtime["@local/users"], "file:packages/users");
17
+ assert.equal(descriptor.mutations.dependencies.runtime["@jskit-ai/crud-core"], "0.1.54");
18
+
19
+ const expectedFileIds = [
20
+ "users-core-users-package-json",
21
+ "users-core-users-package-descriptor-base",
22
+ "users-core-users-package-descriptor-workspace",
23
+ "users-core-users-provider-base",
24
+ "users-core-users-provider-workspace",
25
+ "users-core-users-action-ids",
26
+ "users-core-users-actions-base",
27
+ "users-core-users-actions-workspace",
28
+ "users-core-users-list-config",
29
+ "users-core-users-routes-base",
30
+ "users-core-users-routes-workspace",
31
+ "users-core-users-repository",
32
+ "users-core-users-service",
33
+ "users-core-users-shared-index",
34
+ "users-core-users-resource"
35
+ ];
36
+
37
+ for (const fileId of expectedFileIds) {
38
+ const mutation = readFileMutationById(fileId);
39
+ assert.ok(mutation, `Missing users-core scaffold file mutation ${fileId}.`);
40
+ assert.equal(mutation.ownership, "app", `${fileId} must remain app-owned.`);
41
+ assert.equal(mutation.preserveOnRemove, true, `${fileId} must remain preserved on remove.`);
42
+ assert.ok(mutation.to.startsWith("packages/users/"), `${fileId} must target packages/users.`);
43
+ assert.ok(mutation.from, `${fileId} must define a template source.`);
44
+ assert.ok(mutation.reason, `${fileId} must document why it exists.`);
45
+ }
46
+ });
47
+
48
+ test("users-core base users package templates stay aligned with non-workspace apps", async () => {
49
+ const packageDescriptorSource = await readFile(
50
+ path.join(PACKAGE_ROOT, "templates/packages/users/package.descriptor.mjs"),
51
+ "utf8"
52
+ );
53
+ const providerSource = await readFile(
54
+ path.join(PACKAGE_ROOT, "templates/packages/users/src/server/UsersProvider.js"),
55
+ "utf8"
56
+ );
57
+ const actionsSource = await readFile(
58
+ path.join(PACKAGE_ROOT, "templates/packages/users/src/server/actions.js"),
59
+ "utf8"
60
+ );
61
+ const routesSource = await readFile(
62
+ path.join(PACKAGE_ROOT, "templates/packages/users/src/server/registerRoutes.js"),
63
+ "utf8"
64
+ );
65
+
66
+ assert.doesNotMatch(packageDescriptorSource, /@jskit-ai\/workspaces-core/);
67
+ assert.match(providerSource, /surface: "home"/);
68
+ assert.doesNotMatch(providerSource, /routeSurfaceRequiresWorkspace/);
69
+ assert.doesNotMatch(actionsSource, /workspaceSlugParamsValidator/);
70
+ assert.doesNotMatch(routesSource, /workspaceRouteInput/);
71
+ assert.match(routesSource, /routeBase: "\/"/);
72
+ });
73
+
74
+ test("users-core workspace users package templates stay aligned with workspace apps", async () => {
75
+ const packageDescriptorSource = await readFile(
76
+ path.join(PACKAGE_ROOT, "templates/packages/users-workspace/package.descriptor.mjs"),
77
+ "utf8"
78
+ );
79
+ const providerSource = await readFile(
80
+ path.join(PACKAGE_ROOT, "templates/packages/users-workspace/src/server/UsersProvider.js"),
81
+ "utf8"
82
+ );
83
+ const actionsSource = await readFile(
84
+ path.join(PACKAGE_ROOT, "templates/packages/users-workspace/src/server/actions.js"),
85
+ "utf8"
86
+ );
87
+ const routesSource = await readFile(
88
+ path.join(PACKAGE_ROOT, "templates/packages/users-workspace/src/server/registerRoutes.js"),
89
+ "utf8"
90
+ );
91
+
92
+ assert.match(packageDescriptorSource, /@jskit-ai\/workspaces-core/);
93
+ assert.match(providerSource, /surface: "admin"/);
94
+ assert.match(providerSource, /routeSurfaceRequiresWorkspace/);
95
+ assert.match(actionsSource, /workspaceSlugParamsValidator/);
96
+ assert.match(routesSource, /buildWorkspaceInputFromRouteParams/);
97
+ assert.match(routesSource, /routeBase: routeSurfaceRequiresWorkspace === true \? "\/w\/:workspaceSlug" : "\/"/);
98
+ });
@@ -11,7 +11,7 @@ import { userSettingsResource } from "../src/shared/resources/userSettingsResour
11
11
  function assertResourceShape(resource, label) {
12
12
  assert.ok(resource, `${label} resource must exist.`);
13
13
  assert.equal(typeof resource, "object", `${label} resource must be an object.`);
14
- assert.equal(typeof resource.resource, "string", `${label}.resource must be a string.`);
14
+ assert.equal(typeof resource.namespace, "string", `.namespace must be a string.`);
15
15
 
16
16
  for (const operationName of ["view", "list", "create", "replace", "patch"]) {
17
17
  const operation = resource.operations?.[operationName];