@rubytech/create-maxy-code 0.1.352 → 0.1.354

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 (62) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +96 -14
  3. package/payload/platform/lib/admin-access-password/dist/index.d.ts +20 -4
  4. package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/admin-access-password/dist/index.js +43 -4
  6. package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
  7. package/payload/platform/lib/admin-access-password/src/index.ts +67 -4
  8. package/payload/platform/lib/admins-write/__tests__/index.test.ts +182 -0
  9. package/payload/platform/lib/admins-write/dist/index.d.ts +65 -11
  10. package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
  11. package/payload/platform/lib/admins-write/dist/index.js +93 -31
  12. package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
  13. package/payload/platform/lib/admins-write/src/index.ts +133 -36
  14. package/payload/platform/package-lock.json +76 -1
  15. package/payload/platform/plugins/admin/PLUGIN.md +4 -0
  16. package/payload/platform/plugins/admin/mcp/dist/index.js +15 -3
  17. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
  19. package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +2 -2
  20. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +41 -0
  21. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +236 -47
  22. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +6 -0
  23. package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts +27 -9
  24. package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts.map +1 -1
  25. package/payload/platform/plugins/connector/mcp/dist/lib/call.js +102 -29
  26. package/payload/platform/plugins/connector/mcp/dist/lib/call.js.map +1 -1
  27. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts +17 -6
  28. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts.map +1 -1
  29. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js +28 -10
  30. package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js.map +1 -1
  31. package/payload/platform/plugins/connector/mcp/package.json +13 -3
  32. package/payload/platform/plugins/docs/references/troubleshooting.md +15 -0
  33. package/payload/server/{chunk-QAKVFSEJ.js → chunk-7GNXBVGY.js} +38 -11
  34. package/payload/server/maxy-edge.js +1 -1
  35. package/payload/server/public/assets/{AdminLoginScreens-lt72mYrz.js → AdminLoginScreens-6WgO-Z3m.js} +1 -1
  36. package/payload/server/public/assets/{AdminShell-B-GeoG3j.js → AdminShell-GGnPXCOe.js} +1 -1
  37. package/payload/server/public/assets/{Checkbox-DoGMXVpF.js → Checkbox-D3XFVUMT.js} +1 -1
  38. package/payload/server/public/assets/OperatorConversations-CFWXdnjB.css +1 -0
  39. package/payload/server/public/assets/{OperatorConversations-CfG1EYyP.js → OperatorConversations-DeZ2wDEK.js} +1 -1
  40. package/payload/server/public/assets/{admin-DzLxv-nz.js → admin-B2yNhVvH.js} +1 -1
  41. package/payload/server/public/assets/{browser-Cyi6v8BS.js → browser-ByaQ0ItQ.js} +1 -1
  42. package/payload/server/public/assets/calendar-BjqCT8cW.js +1 -0
  43. package/payload/server/public/assets/chat-kl-Ips-o.js +1 -0
  44. package/payload/server/public/assets/{data-CrJZjlQ1.js → data-p9p-181v.js} +1 -1
  45. package/payload/server/public/assets/{graph-CutaO0w1.js → graph-BuunC8eP.js} +1 -1
  46. package/payload/server/public/assets/{graph-labels-CH0sWvcA.js → graph-labels-BF3Ak8q0.js} +1 -1
  47. package/payload/server/public/assets/{operator-MASl7IqX.js → operator-D7c8mN4J.js} +1 -1
  48. package/payload/server/public/assets/page-C6uMZdT1.js +32 -0
  49. package/payload/server/public/assets/{public-77Ubicd7.js → public-B9_UhjEa.js} +1 -1
  50. package/payload/server/public/browser.html +4 -4
  51. package/payload/server/public/calendar.html +4 -4
  52. package/payload/server/public/chat.html +5 -5
  53. package/payload/server/public/data.html +4 -4
  54. package/payload/server/public/graph.html +6 -6
  55. package/payload/server/public/index.html +6 -6
  56. package/payload/server/public/operator.html +7 -7
  57. package/payload/server/public/public.html +5 -5
  58. package/payload/server/server.js +1127 -1042
  59. package/payload/server/public/assets/OperatorConversations-C8G6Gfgu.css +0 -1
  60. package/payload/server/public/assets/calendar-CA3_Dm8j.js +0 -1
  61. package/payload/server/public/assets/chat-B9aNK1FG.js +0 -1
  62. package/payload/server/public/assets/page-CZy33qUH.js +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.352",
3
+ "version": "0.1.354",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
4
+ import { dirname, join } from 'node:path'
5
+ import type { AuditContext } from '../src/index'
5
6
  import {
6
7
  hashAccessPassword,
7
8
  setAccessPassword,
@@ -13,6 +14,17 @@ import {
13
14
  migrateLegacyRemotePassword,
14
15
  } from '../src/index'
15
16
 
17
+ // Throwaway audit context for the behaviour tests that do not assert on the
18
+ // audit line. The log lands beside the users.json fixture and is ignored.
19
+ function aud(usersFile: string, over: Partial<AuditContext> = {}): AuditContext {
20
+ return { actor: 'test', logFile: join(dirname(usersFile), 'users-audit.log'), ...over }
21
+ }
22
+
23
+ function readAudit(logFile: string): string[] {
24
+ if (!existsSync(logFile)) return []
25
+ return readFileSync(logFile, 'utf-8').trim().split('\n').filter(Boolean)
26
+ }
27
+
16
28
  function tmpUsers(content: unknown): string {
17
29
  const dir = mkdtempSync(join(tmpdir(), 'aap-'))
18
30
  const f = join(dir, 'users.json')
@@ -26,7 +38,7 @@ describe('admin-access-password', () => {
26
38
  { userId: 'a', pin: 'pinA', phone: '+1' },
27
39
  { userId: 'b', pin: 'pinB' },
28
40
  ])
29
- await setAccessPassword('a', 'Sw0rdfish!', f)
41
+ await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
30
42
  const users = JSON.parse(readFileSync(f, 'utf-8'))
31
43
  expect(users[0].accessHash).toMatch(/^[0-9a-f]+:[0-9a-f]+$/)
32
44
  expect(users[0].pin).toBe('pinA')
@@ -36,15 +48,15 @@ describe('admin-access-password', () => {
36
48
 
37
49
  it('verifyAccessPassword returns the matching userId, null on miss', async () => {
38
50
  const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
39
- await setAccessPassword('b', 'Sw0rdfish!', f)
51
+ await setAccessPassword('b', 'Sw0rdfish!', f, aud(f))
40
52
  expect(await verifyAccessPassword('Sw0rdfish!', f)).toBe('b')
41
53
  expect(await verifyAccessPassword('wrong', f)).toBeNull()
42
54
  })
43
55
 
44
56
  it("clearAccessPassword removes only that record's hash", async () => {
45
57
  const f = tmpUsers([{ userId: 'a', pin: 'x' }])
46
- await setAccessPassword('a', 'Sw0rdfish!', f)
47
- clearAccessPassword('a', f)
58
+ await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
59
+ clearAccessPassword('a', f, aud(f))
48
60
  const users = JSON.parse(readFileSync(f, 'utf-8'))
49
61
  expect(users[0].accessHash).toBeUndefined()
50
62
  expect(users[0].pin).toBe('x')
@@ -53,7 +65,7 @@ describe('admin-access-password', () => {
53
65
  it('anyAccessPasswordConfigured reflects presence', async () => {
54
66
  const f = tmpUsers([{ userId: 'a', pin: 'x' }])
55
67
  expect(anyAccessPasswordConfigured(f)).toBe(false)
56
- await setAccessPassword('a', 'Sw0rdfish!', f)
68
+ await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
57
69
  expect(anyAccessPasswordConfigured(f)).toBe(true)
58
70
  })
59
71
 
@@ -64,21 +76,21 @@ describe('admin-access-password', () => {
64
76
 
65
77
  it('accessPasswordCollides: true when a DIFFERENT admin already has that password', async () => {
66
78
  const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
67
- await setAccessPassword('a', 'Sw0rdfish!', f)
79
+ await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
68
80
  // b is choosing the same password a already uses
69
81
  expect(await accessPasswordCollides('Sw0rdfish!', f, 'b')).toBe(true)
70
82
  })
71
83
 
72
84
  it('accessPasswordCollides: false when only the excluded admin holds that password', async () => {
73
85
  const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
74
- await setAccessPassword('a', 'Sw0rdfish!', f)
86
+ await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
75
87
  // a rotating to the same value it already has is not a cross-admin collision
76
88
  expect(await accessPasswordCollides('Sw0rdfish!', f, 'a')).toBe(false)
77
89
  })
78
90
 
79
91
  it('accessPasswordCollides: false for a password no other admin uses', async () => {
80
92
  const f = tmpUsers([{ userId: 'a', pin: 'x' }, { userId: 'b', pin: 'y' }])
81
- await setAccessPassword('a', 'Sw0rdfish!', f)
93
+ await setAccessPassword('a', 'Sw0rdfish!', f, aud(f))
82
94
  expect(await accessPasswordCollides('Different1!', f, 'b')).toBe(false)
83
95
  })
84
96
 
@@ -113,7 +125,7 @@ describe('migrateLegacyRemotePassword', () => {
113
125
  it('moves the legacy hash into the owner record and deletes the file', async () => {
114
126
  const legacyHash = await hashAccessPassword('OwnerPass1!')
115
127
  const { usersFile, legacyFile } = setup([{ userId: 'owner', pin: 'x' }], legacyHash)
116
- const r = migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')
128
+ const r = migrateLegacyRemotePassword(usersFile, legacyFile, 'owner', aud(usersFile))
117
129
  expect(r).toBe('migrated')
118
130
  expect(existsSync(legacyFile)).toBe(false)
119
131
  const users = JSON.parse(readFileSync(usersFile, 'utf-8'))
@@ -123,20 +135,90 @@ describe('migrateLegacyRemotePassword', () => {
123
135
  it('is idempotent — second run is noop', async () => {
124
136
  const legacyHash = await hashAccessPassword('OwnerPass1!')
125
137
  const { usersFile, legacyFile } = setup([{ userId: 'owner', pin: 'x' }], legacyHash)
126
- migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')
127
- expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')).toBe('noop-no-legacy')
138
+ migrateLegacyRemotePassword(usersFile, legacyFile, 'owner', aud(usersFile))
139
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner', aud(usersFile))).toBe('noop-no-legacy')
128
140
  })
129
141
 
130
142
  it('no owner record → leaves the legacy file in place', async () => {
131
143
  const legacyHash = await hashAccessPassword('OwnerPass1!')
132
144
  const { usersFile, legacyFile } = setup([{ userId: 'someoneelse', pin: 'x' }], legacyHash)
133
- expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')).toBe('noop-no-owner')
145
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner', aud(usersFile))).toBe('noop-no-owner')
134
146
  expect(existsSync(legacyFile)).toBe(true)
135
147
  })
136
148
 
137
149
  it('owner already has accessHash → deletes stale legacy file, noop', async () => {
138
150
  const { usersFile, legacyFile } = setup([{ userId: 'owner', pin: 'x', accessHash: 'aa:bb' }], 'cc:dd')
139
- expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner')).toBe('noop-owner-has-hash')
151
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner', aud(usersFile))).toBe('noop-owner-has-hash')
140
152
  expect(existsSync(legacyFile)).toBe(false)
141
153
  })
142
154
  })
155
+
156
+ describe('users-audit emission', () => {
157
+ it('setAccessPassword emits one set-access line with actor and unchanged row-set', async () => {
158
+ const f = tmpUsers([{ userId: 'aaaaaaaa-1', pin: 'x' }, { userId: 'bbbbbbbb-2', pin: 'y' }])
159
+ const logFile = join(dirname(f), 'users-audit.log')
160
+ await setAccessPassword('aaaaaaaa-1', 'Sw0rdfish!', f, { actor: 'cccccccc-9', logFile })
161
+ const lines = readAudit(logFile)
162
+ expect(lines).toHaveLength(1)
163
+ expect(lines[0]).toContain('[users-audit]')
164
+ expect(lines[0]).toContain('action=set-access')
165
+ expect(lines[0]).toContain('field=accessHash')
166
+ expect(lines[0]).toContain('actor=cccccccc')
167
+ expect(lines[0]).toContain('rowsBefore=aaaaaaaa,bbbbbbbb')
168
+ expect(lines[0]).toContain('rowsAfter=aaaaaaaa,bbbbbbbb')
169
+ expect(lines[0]).toMatch(/ts=\d{4}-\d\d-\d\dT/)
170
+ expect(lines[0]).not.toContain('session=')
171
+ })
172
+
173
+ it('a non-uuid sentinel actor is written whole (not truncated to 8 chars)', async () => {
174
+ const f = tmpUsers([{ userId: 'aaaaaaaa-1', pin: 'x' }])
175
+ const logFile = join(dirname(f), 'users-audit.log')
176
+ await setAccessPassword('aaaaaaaa-1', 'Sw0rdfish!', f, { actor: 'onboarding', logFile })
177
+ expect(readAudit(logFile)[0]).toContain('actor=onboarding')
178
+ })
179
+
180
+ it('session= appears only when supplied', async () => {
181
+ const f = tmpUsers([{ userId: 'aaaaaaaa-1', pin: 'x' }])
182
+ const logFile = join(dirname(f), 'users-audit.log')
183
+ await setAccessPassword('aaaaaaaa-1', 'Sw0rdfish!', f, { actor: 'cccccccc-9', session: 'dddddddd-7', logFile })
184
+ expect(readAudit(logFile)[0]).toContain('session=dddddddd')
185
+ })
186
+
187
+ it('clearAccessPassword on a record with a hash emits clear-access', async () => {
188
+ const f = tmpUsers([{ userId: 'aaaaaaaa-1', pin: 'x' }])
189
+ const logFile = join(dirname(f), 'users-audit.log')
190
+ await setAccessPassword('aaaaaaaa-1', 'Sw0rdfish!', f, { actor: 'a', logFile })
191
+ clearAccessPassword('aaaaaaaa-1', f, { actor: 'cccccccc-9', logFile })
192
+ const lines = readAudit(logFile)
193
+ expect(lines).toHaveLength(2)
194
+ expect(lines[1]).toContain('action=clear-access')
195
+ expect(lines[1]).toContain('field=accessHash')
196
+ expect(lines[1]).toContain('actor=cccccccc')
197
+ })
198
+
199
+ it('clearAccessPassword on a record with no hash is a noop and emits nothing', () => {
200
+ const f = tmpUsers([{ userId: 'aaaaaaaa-1', pin: 'x' }])
201
+ const logFile = join(dirname(f), 'users-audit.log')
202
+ clearAccessPassword('aaaaaaaa-1', f, { actor: 'cccccccc-9', logFile })
203
+ expect(readAudit(logFile)).toHaveLength(0)
204
+ })
205
+
206
+ it('migrate emits one migrate line; the noop variants emit nothing', async () => {
207
+ const legacyHash = await hashAccessPassword('OwnerPass1!')
208
+ const dir = mkdtempSync(join(tmpdir(), 'aapmaudit-'))
209
+ const usersFile = join(dir, 'users.json')
210
+ const legacyFile = join(dir, '.remote-password')
211
+ const logFile = join(dir, 'users-audit.log')
212
+ writeFileSync(usersFile, JSON.stringify([{ userId: 'owner-aaa', pin: 'x' }], null, 2))
213
+ writeFileSync(legacyFile, legacyHash)
214
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner-aaa', { actor: 'boot', logFile })).toBe('migrated')
215
+ let lines = readAudit(logFile)
216
+ expect(lines).toHaveLength(1)
217
+ expect(lines[0]).toContain('action=migrate')
218
+ expect(lines[0]).toContain('field=accessHash')
219
+ expect(lines[0]).toContain('actor=boot')
220
+ // second run is a noop → no new line
221
+ expect(migrateLegacyRemotePassword(usersFile, legacyFile, 'owner-aaa', { actor: 'boot', logFile })).toBe('noop-no-legacy')
222
+ expect(readAudit(logFile)).toHaveLength(1)
223
+ })
224
+ })
@@ -5,12 +5,28 @@ export interface AccessUserEntry {
5
5
  accessHash?: string;
6
6
  [k: string]: unknown;
7
7
  }
8
+ /**
9
+ * Identity + destination for one credential-store audit line. The writers are
10
+ * pure (they only know the file paths); the caller is the only party that knows
11
+ * WHO is acting and WHERE the audit log lives, so it is threaded in. `actor` is
12
+ * an admin userId or a non-user sentinel (`onboarding` | `install` | `boot` |
13
+ * `unknown` when an MCP caller has no resolvable USER_ID); `session` is the
14
+ * spawning agent session (SESSION_NODE_ID) when one drove the write. `logFile`
15
+ * is the absolute path to the append-only users-audit log, distinct from
16
+ * server.log. Mirrored in platform/lib/admins-write.
17
+ */
18
+ export interface AuditContext {
19
+ actor: string;
20
+ session?: string;
21
+ logFile: string;
22
+ }
8
23
  export declare function hashAccessPassword(password: string): Promise<string>;
9
24
  declare function matchesHash(password: string, stored: string): Promise<boolean>;
10
25
  /** Write `accessHash` onto the userId's record. Throws if the record is absent. */
11
- export declare function setAccessPassword(userId: string, password: string, usersFile: string): Promise<void>;
12
- /** Remove `accessHash` from the userId's record. No-op if absent. */
13
- export declare function clearAccessPassword(userId: string, usersFile: string): void;
26
+ export declare function setAccessPassword(userId: string, password: string, usersFile: string, audit: AuditContext): Promise<void>;
27
+ /** Remove `accessHash` from the userId's record. No-op (no write, no audit
28
+ * line) when the record is absent or already carries no accessHash. */
29
+ export declare function clearAccessPassword(userId: string, usersFile: string, audit: AuditContext): void;
14
30
  /** Return the userId whose accessHash matches, or null. */
15
31
  export declare function verifyAccessPassword(password: string, usersFile: string): Promise<string | null>;
16
32
  /**
@@ -41,7 +57,7 @@ export type MigrateResult = 'migrated' | 'noop-no-legacy' | 'noop-no-owner' | 'n
41
57
  * exists yet, leave the legacy file untouched (verify still reads it as a
42
58
  * candidate during this window — see readLegacyRemoteHash).
43
59
  */
44
- export declare function migrateLegacyRemotePassword(usersFile: string, legacyFile: string, ownerUserId: string): MigrateResult;
60
+ export declare function migrateLegacyRemotePassword(usersFile: string, legacyFile: string, ownerUserId: string, audit: AuditContext): MigrateResult;
45
61
  /** Read a still-present legacy single-password hash, or null. Used by verify
46
62
  * as one additional candidate before the boot migration runs. */
47
63
  export declare function readLegacyRemoteHash(legacyFile: string): string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAWD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E;AAED,iBAAe,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS7E;AAiBD,mFAAmF;AACnF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM1G;AAED,qEAAqE;AACrE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAQ3E;AAED,2DAA2D;AAC3D,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAStG;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,2DAA2D;AAC3D,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAItE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAGhE;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,gBAAgB,GAAG,eAAe,GAAG,qBAAqB,CAAA;AAEnG;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,aAAa,CAgBf;AAED;kEACkE;AAClE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAItE;AAED,OAAO,EAAE,WAAW,IAAI,mBAAmB,EAAE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AA+CD,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E;AAED,iBAAe,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS7E;AAiBD,mFAAmF;AACnF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/H;AAED;wEACwE;AACxE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAYhG;AAED,2DAA2D;AAC3D,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAStG;AAED;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED,2DAA2D;AAC3D,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAItE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAGhE;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,gBAAgB,GAAG,eAAe,GAAG,qBAAqB,CAAA;AAEnG;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,YAAY,GAClB,aAAa,CAkBf;AAED;kEACkE;AAClE,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAItE;AAED,OAAO,EAAE,WAAW,IAAI,mBAAmB,EAAE,CAAA"}
@@ -15,10 +15,39 @@ exports.verifyHashCandidate = matchesHash;
15
15
  // admin MCP plugin (dist) — mirrors platform/lib/admins-write.
16
16
  const node_crypto_1 = require("node:crypto");
17
17
  const node_fs_1 = require("node:fs");
18
+ const node_path_1 = require("node:path");
18
19
  const SCRYPT_N = 16384;
19
20
  const SCRYPT_R = 8;
20
21
  const SCRYPT_P = 1;
21
22
  const SCRYPT_KEYLEN = 64;
23
+ /** First 8 chars — the short-id convention used across the auth logs. */
24
+ function id8(value) {
25
+ return value.slice(0, 8);
26
+ }
27
+ function rowIds(users) {
28
+ return users.map(u => id8(u.userId)).join(',');
29
+ }
30
+ /**
31
+ * Append one canonical `[users-audit]` line. Append failures never propagate —
32
+ * an audit-write fault must not break the credential write it records; the
33
+ * fault is surfaced once on stderr instead.
34
+ */
35
+ function appendUsersAuditLine(audit, fields) {
36
+ const sess = audit.session ? ` session=${id8(audit.session)}` : '';
37
+ // userIds are uuids (shortened to id8); sentinels (boot|install|onboarding|
38
+ // unknown) carry no dash and are written whole so attribution stays readable.
39
+ const actor = audit.actor.includes('-') ? id8(audit.actor) : audit.actor;
40
+ const line = `[users-audit] action=${fields.action} actor=${actor}${sess} ` +
41
+ `field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ` +
42
+ `ts=${new Date().toISOString()}\n`;
43
+ try {
44
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(audit.logFile), { recursive: true, mode: 0o700 });
45
+ (0, node_fs_1.appendFileSync)(audit.logFile, line, { mode: 0o600 });
46
+ }
47
+ catch (err) {
48
+ console.error(`[users-audit] op=write-failed logFile=${audit.logFile} error=${err instanceof Error ? err.message : String(err)}`);
49
+ }
50
+ }
22
51
  function scryptAsync(password, salt) {
23
52
  return new Promise((resolve, reject) => {
24
53
  (0, node_crypto_1.scrypt)(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, key) => {
@@ -68,24 +97,32 @@ function writeUsersAtomic(usersFile, users) {
68
97
  (0, node_fs_1.renameSync)(tmp, usersFile);
69
98
  }
70
99
  /** Write `accessHash` onto the userId's record. Throws if the record is absent. */
71
- async function setAccessPassword(userId, password, usersFile) {
100
+ async function setAccessPassword(userId, password, usersFile, audit) {
72
101
  const users = readUsers(usersFile);
73
102
  const idx = users.findIndex(u => u.userId === userId);
74
103
  if (idx === -1)
75
104
  throw new Error(`no users.json record for userId=${userId}`);
105
+ const rows = rowIds(users);
76
106
  users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) };
77
107
  writeUsersAtomic(usersFile, users);
108
+ appendUsersAuditLine(audit, { action: 'set-access', field: 'accessHash', rowsBefore: rows, rowsAfter: rows });
78
109
  }
79
- /** Remove `accessHash` from the userId's record. No-op if absent. */
80
- function clearAccessPassword(userId, usersFile) {
110
+ /** Remove `accessHash` from the userId's record. No-op (no write, no audit
111
+ * line) when the record is absent or already carries no accessHash. */
112
+ function clearAccessPassword(userId, usersFile, audit) {
81
113
  const users = readUsers(usersFile);
82
114
  const idx = users.findIndex(u => u.userId === userId);
83
115
  if (idx === -1)
84
116
  return;
117
+ const existing = users[idx].accessHash;
118
+ if (typeof existing !== 'string' || existing.length === 0)
119
+ return;
120
+ const rows = rowIds(users);
85
121
  const { accessHash, ...rest } = users[idx];
86
122
  void accessHash;
87
123
  users[idx] = rest;
88
124
  writeUsersAtomic(usersFile, users);
125
+ appendUsersAuditLine(audit, { action: 'clear-access', field: 'accessHash', rowsBefore: rows, rowsAfter: rows });
89
126
  }
90
127
  /** Return the userId whose accessHash matches, or null. */
91
128
  async function verifyAccessPassword(password, usersFile) {
@@ -165,7 +202,7 @@ function accessStoreUnreadable(usersFile) {
165
202
  * exists yet, leave the legacy file untouched (verify still reads it as a
166
203
  * candidate during this window — see readLegacyRemoteHash).
167
204
  */
168
- function migrateLegacyRemotePassword(usersFile, legacyFile, ownerUserId) {
205
+ function migrateLegacyRemotePassword(usersFile, legacyFile, ownerUserId, audit) {
169
206
  if (!(0, node_fs_1.existsSync)(legacyFile))
170
207
  return 'noop-no-legacy';
171
208
  const legacy = (0, node_fs_1.readFileSync)(legacyFile, 'utf-8').trim();
@@ -182,9 +219,11 @@ function migrateLegacyRemotePassword(usersFile, legacyFile, ownerUserId) {
182
219
  (0, node_fs_1.unlinkSync)(legacyFile);
183
220
  return 'noop-no-legacy';
184
221
  }
222
+ const rows = rowIds(users);
185
223
  users[idx] = { ...users[idx], accessHash: legacy };
186
224
  writeUsersAtomic(usersFile, users);
187
225
  (0, node_fs_1.unlinkSync)(legacyFile);
226
+ appendUsersAuditLine(audit, { action: 'migrate', field: 'accessHash', rowsBefore: rows, rowsAfter: rows });
188
227
  return 'migrated';
189
228
  }
190
229
  /** Read a still-present legacy single-password hash, or null. Used by verify
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,8 @@
2
2
  // on users.json admin records. Imported by both platform/ui (src) and the
3
3
  // admin MCP plugin (dist) — mirrors platform/lib/admins-write.
4
4
  import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'
5
- import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs'
5
+ import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync, mkdirSync } from 'node:fs'
6
+ import { dirname } from 'node:path'
6
7
 
7
8
  const SCRYPT_N = 16384
8
9
  const SCRYPT_R = 8
@@ -17,6 +18,58 @@ export interface AccessUserEntry {
17
18
  [k: string]: unknown
18
19
  }
19
20
 
21
+ /**
22
+ * Identity + destination for one credential-store audit line. The writers are
23
+ * pure (they only know the file paths); the caller is the only party that knows
24
+ * WHO is acting and WHERE the audit log lives, so it is threaded in. `actor` is
25
+ * an admin userId or a non-user sentinel (`onboarding` | `install` | `boot` |
26
+ * `unknown` when an MCP caller has no resolvable USER_ID); `session` is the
27
+ * spawning agent session (SESSION_NODE_ID) when one drove the write. `logFile`
28
+ * is the absolute path to the append-only users-audit log, distinct from
29
+ * server.log. Mirrored in platform/lib/admins-write.
30
+ */
31
+ export interface AuditContext {
32
+ actor: string
33
+ session?: string
34
+ logFile: string
35
+ }
36
+
37
+ /** First 8 chars — the short-id convention used across the auth logs. */
38
+ function id8(value: string): string {
39
+ return value.slice(0, 8)
40
+ }
41
+
42
+ function rowIds(users: AccessUserEntry[]): string {
43
+ return users.map(u => id8(u.userId)).join(',')
44
+ }
45
+
46
+ /**
47
+ * Append one canonical `[users-audit]` line. Append failures never propagate —
48
+ * an audit-write fault must not break the credential write it records; the
49
+ * fault is surfaced once on stderr instead.
50
+ */
51
+ function appendUsersAuditLine(
52
+ audit: AuditContext,
53
+ fields: { action: string; field: string; rowsBefore: string; rowsAfter: string },
54
+ ): void {
55
+ const sess = audit.session ? ` session=${id8(audit.session)}` : ''
56
+ // userIds are uuids (shortened to id8); sentinels (boot|install|onboarding|
57
+ // unknown) carry no dash and are written whole so attribution stays readable.
58
+ const actor = audit.actor.includes('-') ? id8(audit.actor) : audit.actor
59
+ const line =
60
+ `[users-audit] action=${fields.action} actor=${actor}${sess} ` +
61
+ `field=${fields.field} rowsBefore=${fields.rowsBefore} rowsAfter=${fields.rowsAfter} ` +
62
+ `ts=${new Date().toISOString()}\n`
63
+ try {
64
+ mkdirSync(dirname(audit.logFile), { recursive: true, mode: 0o700 })
65
+ appendFileSync(audit.logFile, line, { mode: 0o600 })
66
+ } catch (err) {
67
+ console.error(
68
+ `[users-audit] op=write-failed logFile=${audit.logFile} error=${err instanceof Error ? err.message : String(err)}`,
69
+ )
70
+ }
71
+ }
72
+
20
73
  function scryptAsync(password: string, salt: Buffer): Promise<Buffer> {
21
74
  return new Promise((resolve, reject) => {
22
75
  scrypt(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, key) => {
@@ -59,23 +112,30 @@ function writeUsersAtomic(usersFile: string, users: AccessUserEntry[]): void {
59
112
  }
60
113
 
61
114
  /** Write `accessHash` onto the userId's record. Throws if the record is absent. */
62
- export async function setAccessPassword(userId: string, password: string, usersFile: string): Promise<void> {
115
+ export async function setAccessPassword(userId: string, password: string, usersFile: string, audit: AuditContext): Promise<void> {
63
116
  const users = readUsers(usersFile)
64
117
  const idx = users.findIndex(u => u.userId === userId)
65
118
  if (idx === -1) throw new Error(`no users.json record for userId=${userId}`)
119
+ const rows = rowIds(users)
66
120
  users[idx] = { ...users[idx], accessHash: await hashAccessPassword(password) }
67
121
  writeUsersAtomic(usersFile, users)
122
+ appendUsersAuditLine(audit, { action: 'set-access', field: 'accessHash', rowsBefore: rows, rowsAfter: rows })
68
123
  }
69
124
 
70
- /** Remove `accessHash` from the userId's record. No-op if absent. */
71
- export function clearAccessPassword(userId: string, usersFile: string): void {
125
+ /** Remove `accessHash` from the userId's record. No-op (no write, no audit
126
+ * line) when the record is absent or already carries no accessHash. */
127
+ export function clearAccessPassword(userId: string, usersFile: string, audit: AuditContext): void {
72
128
  const users = readUsers(usersFile)
73
129
  const idx = users.findIndex(u => u.userId === userId)
74
130
  if (idx === -1) return
131
+ const existing = users[idx].accessHash
132
+ if (typeof existing !== 'string' || existing.length === 0) return
133
+ const rows = rowIds(users)
75
134
  const { accessHash, ...rest } = users[idx]
76
135
  void accessHash
77
136
  users[idx] = rest as AccessUserEntry
78
137
  writeUsersAtomic(usersFile, users)
138
+ appendUsersAuditLine(audit, { action: 'clear-access', field: 'accessHash', rowsBefore: rows, rowsAfter: rows })
79
139
  }
80
140
 
81
141
  /** Return the userId whose accessHash matches, or null. */
@@ -147,6 +207,7 @@ export function migrateLegacyRemotePassword(
147
207
  usersFile: string,
148
208
  legacyFile: string,
149
209
  ownerUserId: string,
210
+ audit: AuditContext,
150
211
  ): MigrateResult {
151
212
  if (!existsSync(legacyFile)) return 'noop-no-legacy'
152
213
  const legacy = readFileSync(legacyFile, 'utf-8').trim()
@@ -159,9 +220,11 @@ export function migrateLegacyRemotePassword(
159
220
  return 'noop-owner-has-hash'
160
221
  }
161
222
  if (!legacy.includes(':')) { unlinkSync(legacyFile); return 'noop-no-legacy' }
223
+ const rows = rowIds(users)
162
224
  users[idx] = { ...users[idx], accessHash: legacy }
163
225
  writeUsersAtomic(usersFile, users)
164
226
  unlinkSync(legacyFile)
227
+ appendUsersAuditLine(audit, { action: 'migrate', field: 'accessHash', rowsBefore: rows, rowsAfter: rows })
165
228
  return 'migrated'
166
229
  }
167
230