@things-factory/auth-base 10.1.218 → 10.1.223

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-base",
3
- "version": "10.1.218",
3
+ "version": "10.1.223",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -50,5 +50,5 @@
50
50
  "passport-jwt": "^4.0.0",
51
51
  "passport-local": "^1.0.0"
52
52
  },
53
- "gitHead": "b4e60fdfe307cecebb2264267f616749a502d5fe"
53
+ "gitHead": "868f2026b8373371ad04f42b0a097964d0d0ed0f"
54
54
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * What a role lacks against the template of its name - against a real database (ADR-0075 decision 1).
2
+ * What a role lacks against its base template - against a real database (ADR-0075 decision 1, amended 2026-10-26:
3
+ * a role keeps one base template, `Role.templateId`, and is never paired by name).
3
4
  *
4
5
  * A template is a seed and nothing pushes its later grants into roles already standing. When
5
6
  * operato-plant's templates gained ai-assistant, the roles in miratek stayed without it and only
@@ -14,12 +15,16 @@ const { Role } = loadCompiled('service/role/role')
14
15
  const { Privilege } = loadCompiled('service/privilege/privilege')
15
16
  const { User } = loadCompiled('service/user/user')
16
17
  const { RoleTemplateMutation } = loadCompiled('service/role-template/role-template-mutation')
18
+ const { RoleMutation } = loadCompiled('service/role/role-mutation')
17
19
  const { RoleTemplateGapResolver } = loadCompiled('service/role-template/role-template-gap-resolver')
18
20
  const { registerRoleTemplate } = loadCompiled('service/role-template/role-template')
19
21
  const { registerRoleTask } = loadCompiled('service/role-template/role-task')
20
22
 
21
23
  const mutation = new RoleTemplateMutation()
22
- const gapOf = (role: any, templateId?: string) => new RoleTemplateGapResolver().templateGap(role, templateId)
24
+ const gapOf = (role: any) => new RoleTemplateGapResolver().templateGap(role)
25
+ const reread = (role: any) => getRepository(Role).findOneBy({ id: role.id })
26
+ /* The role screen's base template field saves through `updateRole`. */
27
+ const baseOn = (role: any, templateId: string | null) => new RoleMutation().updateRole(role.id, { templateId } as any, { ...context(), t: (key: string) => key } as any)
23
28
  const coverageOf = (role: any) => new RoleTemplateGapResolver().taskCoverage(role)
24
29
 
25
30
  let domain: any
@@ -107,13 +112,52 @@ describe('the gap between a role and its template', () => {
107
112
  expect((await gapOf(role)).missingCount).toBe(0)
108
113
  })
109
114
 
110
- it('★ is null for a role no template names - a renamed role belongs to its administrator', async () => {
115
+ it('★ a role made under a name of its own is still based on the template', async () => {
111
116
  declareTemplate(['query'])
112
117
  const role = await mutation.createRoleFromTemplate('plant.qi', 'NIGHT-INSPECTOR', context())
118
+ declareTemplate(['query', 'mutation'])
119
+
120
+ expect((await reread(role)).templateId).toBe('plant.qi')
121
+ expect(await gapOf(await reread(role))).toMatchObject({ templateId: 'plant.qi', missingCount: 1 })
122
+ })
123
+
124
+ it('★ is null for a role that is based on no template, even when a template carries its name', async () => {
125
+ declareTemplate(['query'])
126
+ const role = await getRepository(Role).save({ domain, name: 'QUALITY-INSPECTOR', privileges: [] })
113
127
 
114
128
  expect(await gapOf(role)).toBeNull()
115
129
  })
116
130
 
131
+ it('★ refuses to top up a role based on another template, and changes nothing', async () => {
132
+ declareTemplate(['query'])
133
+ registerRoleTemplate({ id: 'plant.qm', name: 'QUALITY-MANAGER', grants: [{ category: 'board', axes: ['query', 'mutation'] }] })
134
+ const role = await mutation.createRoleFromTemplate('plant.qi', undefined, context())
135
+
136
+ await expect(mutation.createRoleFromTemplate('plant.qm', 'QUALITY-INSPECTOR', context())).rejects.toThrow(/based on role template "plant.qi", not "plant.qm"/)
137
+
138
+ const stored = await getRepository(Role).findOne({ where: { id: role.id }, relations: ['privileges'] })
139
+ expect(stored.templateId).toBe('plant.qi')
140
+ expect(stored.privileges.map((p: any) => p.name)).toEqual(['query'])
141
+ })
142
+
143
+ it('★ tops up a standing role that has no base and makes the template its base', async () => {
144
+ declareTemplate(['query', 'mutation'])
145
+ const role = await getRepository(Role).save({ domain, name: 'QUALITY-INSPECTOR', privileges: [] })
146
+
147
+ await mutation.createRoleFromTemplate('plant.qi', undefined, context())
148
+
149
+ expect((await reread(role)).templateId).toBe('plant.qi')
150
+ expect((await gapOf(await reread(role))).missingCount).toBe(0)
151
+ })
152
+
153
+ it('★ is null when the base is a template this installation no longer declares', async () => {
154
+ declareTemplate(['query'])
155
+ const role = await mutation.createRoleFromTemplate('plant.qi', undefined, context())
156
+ ;(process as any)['ROLE_TEMPLATES'] = {}
157
+
158
+ expect(await gapOf(await reread(role))).toBeNull()
159
+ })
160
+
117
161
  it('does not take away what the administrator added beyond the template', async () => {
118
162
  declareTemplate(['query'])
119
163
  const role = await mutation.createRoleFromTemplate('plant.qi', undefined, context())
@@ -131,19 +175,26 @@ describe('the gap between a role and its template', () => {
131
175
  expect(gap.extraCount).toBe(1)
132
176
  })
133
177
 
134
- it('★ lays a role against a template the administrator picks, whatever the role is called', async () => {
178
+ it('★ lays a role named in the site\'s own words against the base the administrator gives it', async () => {
135
179
  declareTemplate(['query', 'mutation'])
136
180
  const role = await getRepository(Role).save({ domain, name: '품질검사', privileges: [await getRepository(Privilege).findOneBy({ category: 'board', name: 'query' })] })
137
181
 
138
182
  expect(await gapOf(role)).toBeNull()
139
- expect(await gapOf(role, 'plant.qi')).toMatchObject({ templateId: 'plant.qi', missing: [{ category: 'board', axes: ['mutation'] }], extraCount: 0 })
183
+
184
+ await baseOn(role, 'plant.qi')
185
+ expect(await gapOf(await reread(role))).toMatchObject({ templateId: 'plant.qi', missing: [{ category: 'board', axes: ['mutation'] }], extraCount: 0 })
186
+
187
+ await baseOn(role, null)
188
+ expect((await reread(role)).templateId).toBeNull()
189
+ expect(await gapOf(await reread(role))).toBeNull()
140
190
  })
141
191
 
142
- it('refuses a template id that does not exist rather than answering null', async () => {
192
+ it('refuses a base template id that does not exist rather than storing it', async () => {
143
193
  declareTemplate(['query'])
144
194
  const role = await mutation.createRoleFromTemplate('plant.qi', undefined, context())
145
195
 
146
- await expect(gapOf(role, 'plant.nobody')).rejects.toThrow(/no role template "plant.nobody"/)
196
+ await expect(baseOn(role, 'plant.nobody')).rejects.toThrow(/no role template "plant.nobody"/)
197
+ expect((await reread(role)).templateId).toBe('plant.qi')
147
198
  })
148
199
  })
149
200
 
@@ -119,7 +119,8 @@ describe('seeding', () => {
119
119
  expect(await getRepository(Role).count()).toBe(1)
120
120
  })
121
121
 
122
- it('adds the union when two applications declare the same seat', async () => {
122
+ /* A role has one base template (ADR-0075 decision 1, amended 2026-10-26) - two apps declaring one name no longer union. */
123
+ it('refuses a second template onto a role of the same name based on the first', async () => {
123
124
  registerRoleTemplate({ id: 'plant.qi', name: 'QUALITY-INSPECTOR', grants: [{ category: 'board', axes: ['query'] }] })
124
125
  registerRoleTemplate({
125
126
  id: 'twin.qi',
@@ -127,10 +128,10 @@ describe('seeding', () => {
127
128
  grants: [{ category: 'board', axes: ['mutation'] }]
128
129
  })
129
130
 
130
- await mutation.createRoleFromTemplate('plant.qi', undefined, context())
131
- const role = await mutation.createRoleFromTemplate('twin.qi', undefined, context())
131
+ const role = await mutation.createRoleFromTemplate('plant.qi', undefined, context())
132
+ await expect(mutation.createRoleFromTemplate('twin.qi', undefined, context())).rejects.toThrow(/based on role template "plant.qi", not "twin.qi"/)
132
133
 
133
- expect(await privilegesOf(role.id)).toEqual(['board:mutation', 'board:query'])
134
+ expect(await privilegesOf(role.id)).toEqual(['board:query'])
134
135
  expect(await getRepository(Role).count()).toBe(1)
135
136
  })
136
137