@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.
- package/package.json +1 -1
- package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +96 -14
- package/payload/platform/lib/admin-access-password/dist/index.d.ts +20 -4
- package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admin-access-password/dist/index.js +43 -4
- package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
- package/payload/platform/lib/admin-access-password/src/index.ts +67 -4
- package/payload/platform/lib/admins-write/__tests__/index.test.ts +182 -0
- package/payload/platform/lib/admins-write/dist/index.d.ts +65 -11
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +93 -31
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +133 -36
- package/payload/platform/package-lock.json +76 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +15 -3
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
- package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +2 -2
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.css +41 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/booking.js +236 -47
- package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +6 -0
- package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts +27 -9
- package/payload/platform/plugins/connector/mcp/dist/lib/call.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/call.js +102 -29
- package/payload/platform/plugins/connector/mcp/dist/lib/call.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts +17 -6
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js +28 -10
- package/payload/platform/plugins/connector/mcp/dist/lib/ssrf.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/package.json +13 -3
- package/payload/platform/plugins/docs/references/troubleshooting.md +15 -0
- package/payload/server/{chunk-QAKVFSEJ.js → chunk-7GNXBVGY.js} +38 -11
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-lt72mYrz.js → AdminLoginScreens-6WgO-Z3m.js} +1 -1
- package/payload/server/public/assets/{AdminShell-B-GeoG3j.js → AdminShell-GGnPXCOe.js} +1 -1
- package/payload/server/public/assets/{Checkbox-DoGMXVpF.js → Checkbox-D3XFVUMT.js} +1 -1
- package/payload/server/public/assets/OperatorConversations-CFWXdnjB.css +1 -0
- package/payload/server/public/assets/{OperatorConversations-CfG1EYyP.js → OperatorConversations-DeZ2wDEK.js} +1 -1
- package/payload/server/public/assets/{admin-DzLxv-nz.js → admin-B2yNhVvH.js} +1 -1
- package/payload/server/public/assets/{browser-Cyi6v8BS.js → browser-ByaQ0ItQ.js} +1 -1
- package/payload/server/public/assets/calendar-BjqCT8cW.js +1 -0
- package/payload/server/public/assets/chat-kl-Ips-o.js +1 -0
- package/payload/server/public/assets/{data-CrJZjlQ1.js → data-p9p-181v.js} +1 -1
- package/payload/server/public/assets/{graph-CutaO0w1.js → graph-BuunC8eP.js} +1 -1
- package/payload/server/public/assets/{graph-labels-CH0sWvcA.js → graph-labels-BF3Ak8q0.js} +1 -1
- package/payload/server/public/assets/{operator-MASl7IqX.js → operator-D7c8mN4J.js} +1 -1
- package/payload/server/public/assets/page-C6uMZdT1.js +32 -0
- package/payload/server/public/assets/{public-77Ubicd7.js → public-B9_UhjEa.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +1127 -1042
- package/payload/server/public/assets/OperatorConversations-C8G6Gfgu.css +0 -1
- package/payload/server/public/assets/calendar-CA3_Dm8j.js +0 -1
- package/payload/server/public/assets/chat-B9aNK1FG.js +0 -1
- package/payload/server/public/assets/page-CZy33qUH.js +0 -30
package/package.json
CHANGED
|
@@ -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
|
|
13
|
-
|
|
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":"
|
|
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
|
|
80
|
-
|
|
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":";;
|
|
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
|
|
71
|
-
|
|
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
|
|