@jeffusion/bungee-core 5.8.1 → 5.10.0
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/dist/main.js +46 -45
- package/dist/plugins/key-access/control.js.map +1 -1
- package/dist/plugins/key-access/index.js.map +1 -1
- package/dist/plugins/key-access/ingress.js.map +1 -1
- package/dist/plugins/local-accounts/control.js +68 -18
- package/dist/plugins/local-accounts/control.js.map +4 -4
- package/dist/plugins/local-accounts/manifest.json +48 -2
- package/dist/plugins/local-accounts/ui/Settings.svelte +73 -1
- package/package.json +2 -2
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../plugins/local-accounts/server/control.ts", "../../../src/plugin-durable-state.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';\nimport type { ControlHostContext, ControlApiHandlerContext, PluginControl, ControlPlugin } from '../../../packages/core/src/plugin-control/contracts';\nimport type { ManagementProvider, ManagementSubject } from '../../../packages/core/src/plugin-extensions';\nimport { DurableStateConflictError, type DurableJson, type PluginDurableState } from '../../../packages/core/src/plugin-durable-state';\n\ninterface Administrator { id: string; username: string; disabled: boolean; passwordHash: string; temporary: boolean; generation: number }\ninterface Session { digest: string; administratorId: string; generation: number; created: number; touched: number; transport: 'cookie' | 'bearer' }\ninterface Failure { key: string; count: number; until: number }\ninterface State { schema: 2; administrator: Administrator | null; sessions: Session[]; failures: Failure[] }\nexport interface Dependencies { now?: () => number; trustedSource?: (request: Request) => string }\nconst PROVIDER = 'local-accounts';\nconst COOKIE = 'bungee_local_session';\nconst IDLE = 30 * 60_000, ABSOLUTE = 8 * 60 * 60_000;\nconst ALL = ['config.read', 'config.write', 'logs.read', 'logs.body', 'keys.read', 'keys.write', 'plugins.read', 'plugins.toggle', 'plugins.code', 'auth.mode', 'self.password', 'self.logout'];\nclass AccountError extends Error { constructor(readonly code: string, readonly status = 400) { super(code); } }\nfunction fail(code: string, status = 400): never { throw new AccountError(code, status); }\nfunction object(v: unknown): Record<string, unknown> { if (!v || typeof v !== 'object' || Array.isArray(v)) fail('invalid_input'); return v as Record<string, unknown>; }\nfunction exact(v: Record<string, unknown>, fields: string[]) { if (Object.keys(v).some(k => !fields.includes(k))) fail('invalid_input'); }\nfunction username(v: unknown): string { if (typeof v !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,63}$/.test(v)) fail('invalid_input'); return v.toLowerCase(); }\nfunction password(v: unknown): string { if (typeof v !== 'string' || [...v].length < 6 || [...v].length > 64) fail('invalid_password'); return v; }\nfunction digest(v: string): string { return createHash('sha256').update(v).digest('hex'); }\nfunction csrf(token: string): string { return createHmac('sha256', token).update('local-accounts/csrf/v1').digest('base64url'); }\nfunction constant(a: string, b: string): boolean { const aa = Buffer.from(a), bb = Buffer.from(b); return aa.length === bb.length && timingSafeEqual(aa, bb); }\nfunction dto(m: Administrator) { return { id: m.id, username: m.username, requiresPasswordChange: m.temporary }; }\nfunction json(v: unknown, status = 200, headers: Record<string, string> = {}): Response { return Response.json(v, { status, headers: { 'cache-control': 'no-store', ...headers } }); }\nfunction safe(error: unknown): Response { return error instanceof AccountError ? json({ error: error.code }, error.status) : json({ error: 'provider_unavailable' }, 503); }\nasync function body(request: Request): Promise<Record<string, unknown>> {\n const reader = request.body?.getReader(); if (!reader) fail('invalid_input');\n let size = 0; const chunks: Uint8Array[] = [];\n try { for (;;) { const r = await reader.read(); if (r.done) break; size += r.value.length; if (size > 8192) { await reader.cancel(); fail('body_limit', 413); } chunks.push(r.value); } }\n finally { reader.releaseLock(); }\n try { return object(JSON.parse(Buffer.concat(chunks).toString('utf8'))); } catch (e) { if (e instanceof AccountError) throw e; return fail('invalid_input'); }\n}\nfunction credentials(request: Request): { token: string; transport: Session['transport'] } | null {\n const auth = request.headers.get('authorization');\n if (auth !== null) { const m = /^Bearer ([A-Za-z0-9_-]{43})$/.exec(auth); return m ? { token: m[1]!, transport: 'bearer' } : null; }\n const tokens = (request.headers.get('cookie') ?? '').split(';').map(x => x.trim()).filter(x => x.startsWith(COOKIE + '='));\n if (tokens.length !== 1) return null; const token = tokens[0]!.slice(COOKIE.length + 1);\n return /^[A-Za-z0-9_-]{43}$/.test(token) ? { token, transport: 'cookie' } : null;\n}\nfunction origin(request: Request, expected?: string) { if (request.headers.get('origin') !== (expected ?? new URL(request.url).origin)) fail('invalid_origin', 403); }\nfunction cookie(request: Request, token: string, clear = false, managementOrigin?: string) { return `${COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${clear ? 0 : ABSOLUTE / 1000}${new URL(managementOrigin ?? request.url).protocol === 'https:' ? '; Secure' : ''}`; }\n\nfunction validateAdministrator(m: unknown): asserts m is Administrator {\n const a = m as Administrator;\n if (!a || typeof a.id !== 'string' || !a.id || typeof a.username !== 'string' || !/^[a-z0-9][a-z0-9_.@-]{0,63}$/.test(a.username) || typeof a.disabled !== 'boolean' || typeof a.temporary !== 'boolean' || typeof a.passwordHash !== 'string' || !a.passwordHash.startsWith('$argon2id$') || !Number.isSafeInteger(a.generation) || a.generation < 1) fail('corrupt_state', 503);\n}\n/** Migration never promotes a former admin/viewer. Ambiguous owners require offline selection. */\nfunction validateAccountState(value: unknown, recoveryUsername?: string): State {\n if (!value || typeof value !== 'object' || Array.isArray(value)) fail('corrupt_state', 503);\n const raw = value as Record<string, unknown>;\n if (raw.schema === 1) {\n if (!Array.isArray(raw.members) || !Array.isArray(raw.sessions) || !Array.isArray(raw.failures)) fail('corrupt_state', 503);\n const ids = new Set<string>(), names = new Set<string>();\n for (const member of raw.members) {\n validateAdministrator(member);\n const legacy = member as Administrator & {role: string};\n if (!['owner', 'admin', 'viewer'].includes(legacy.role) || ids.has(member.id) || names.has(member.username)) fail('corrupt_state', 503);\n ids.add(member.id); names.add(member.username);\n }\n const candidates = raw.members.filter(m => m.role === 'owner' && (recoveryUsername ? m.username === recoveryUsername : !m.disabled));\n if (raw.members.length && candidates.length !== 1) fail(recoveryUsername ? 'recovery_requires_existing_administrator' : 'administrator_migration_required', 503);\n const chosen = candidates[0];\n const administrator = chosen ? { id: chosen.id, username: chosen.username, disabled: chosen.disabled, temporary: chosen.temporary, passwordHash: chosen.passwordHash, generation: chosen.generation } : null;\n return {schema: 2, administrator, sessions: [], failures: []};\n }\n const s = raw as unknown as State;\n if (s.schema !== 2 || !Array.isArray(s.sessions) || !Array.isArray(s.failures)) fail('corrupt_state', 503);\n if (s.administrator !== null) validateAdministrator(s.administrator);\n for (const x of s.sessions) if (!x || !/^[a-f0-9]{64}$/.test(x.digest) || x.administratorId !== s.administrator?.id || !Number.isSafeInteger(x.generation) || x.generation < 1 || !Number.isFinite(x.created) || !Number.isFinite(x.touched) || !['cookie','bearer'].includes(x.transport)) fail('corrupt_state', 503);\n for (const x of s.failures) if (!x || typeof x.key !== 'string' || !Number.isSafeInteger(x.count) || x.count < 0 || !Number.isFinite(x.until)) fail('corrupt_state', 503);\n return s;\n}\n\nexport class LocalAccountsControl implements PluginControl, ManagementProvider {\n readonly management = this;\n readonly rpc = [];\n readonly api;\n private readonly state: PluginDurableState;\n private readonly now: () => number;\n private readonly subjects = new WeakMap<ManagementSubject, { generation: number; digest: string }>();\n private dummy = '';\n private disposed = false;\n constructor(private readonly host: ControlHostContext, private readonly dependencies: Dependencies = {}) {\n if (!host.durableState) fail('durable_state_required', 503);\n this.state = host.durableState;\n this.now = dependencies.now ?? Date.now;\n const invoke = (fn: (c: ControlApiHandlerContext) => Promise<Response> | Response) => async (c: ControlApiHandlerContext) => {\n try { this.alive(); if (c.requestSignal.aborted) fail('cancelled'); return await fn(c); } catch (e) { return safe(e); }\n };\n this.api = [\n { path: '/password', methods: ['POST'], handler: 'changePassword', invoke: invoke(async c => { this.requireCapability(c, 'self.password', true); const b = await body(c.request); exact(b, ['currentPassword', 'password', 'passwordConfirmation']); await this.changePassword(c.subject!.id, b, () => this.requireCapability(c, 'self.password', true)); return json({ ok: true }, 200, { 'set-cookie': cookie(c.request, '', true, this.host.managementOrigin) }); }) },\n { path: '/logout', methods: ['POST'], handler: 'logout', invoke: invoke(c => { this.requireCapability(c, 'self.logout', true); return this.logout(c.request); }) },\n { path: '/self', methods: ['GET'], handler: 'self', invoke: invoke(c => { this.requireCapability(c, 'self.password'); const m = this.read().value.administrator!; const creds = credentials(c.request); return json({ administrator: dto(m), ...(creds?.transport === 'cookie' ? { csrfToken: csrf(creds.token) } : {}) }); }) },\n ];\n }\n private alive() { if (this.disposed || this.host.signal.aborted) fail('provider_unavailable', 503); }\n private read(): { version: number; value: State } {\n this.alive(); const record = this.state.get('accounts');\n if (!record) return { version: 0, value: { schema: 2, administrator: null, sessions: [], failures: [] } };\n const s = validateAccountState(record.value);\n return { version: record.version, value: s };\n }\n private mutate<T>(fn: (s: State) => T): T {\n for (let i = 0; i < 8; i++) { const {version, value} = this.read(); const result = fn(value);\n try { this.state.execute({ commandId: randomUUID(), mutations: [{ key: 'accounts', expectedVersion: version, value: value as unknown as DurableJson }] }); return result; }\n catch (e) { if (!(e instanceof DurableStateConflictError)) throw e; }\n } return fail('version_conflict', 409);\n }\n async start() { const record = this.state.get('accounts'); this.read(); if (record && (record.value as any).schema === 1) this.mutate(() => undefined); this.dummy = await this.hash(randomBytes(32).toString('base64url')); }\n dispose() { this.disposed = true; }\n private hash(p: string) { return Bun.password.hash(p, { algorithm: 'argon2id', memoryCost: 65536, timeCost: 3 }); }\n hasIdentity() { const a = this.read().value.administrator; return !!a && !a.disabled; }\n revokeSessions() { this.mutate(s => { s.sessions = []; }); }\n async bootstrap(input: unknown) {\n const b = object(input); exact(b, ['username', 'password', 'passwordConfirmation']); const name = username(b.username), p = password(b.password);\n if (b.passwordConfirmation !== p) fail('password_confirmation');\n const prior = this.read().value.administrator;\n if (prior) {\n if (prior.username !== name || prior.disabled || !await Bun.password.verify(p, prior.passwordHash)) fail('invalid_credentials', 401);\n const latest = this.read().value.administrator;\n if (!latest || latest.generation !== prior.generation || latest.disabled) fail('invalid_credentials', 401);\n return;\n }\n const hashed = await this.hash(p);\n this.mutate(s => { if (s.administrator) fail('bootstrap_conflict', 409); s.administrator = { id: randomUUID(), username: name, disabled: false, temporary: false, passwordHash: hashed, generation: 1 }; });\n }\n private live(s: State, session: Session): Administrator | undefined { const now = this.now(); if (now < session.touched || now - session.touched >= IDLE || now - session.created >= ABSOLUTE) return; const m = s.administrator; return m && m.id === session.administratorId && !m.disabled && m.generation === session.generation ? m : undefined; }\n async authenticate(request: Request): Promise<ManagementSubject | null> {\n this.alive(); const c = credentials(request); if (!c) return null;\n return this.mutate(s => { const session = s.sessions.find(x => x.digest === digest(c.token) && x.transport === c.transport); const administrator = session && this.live(s, session); if (!session || !administrator) return null; session.touched = this.now(); const subject = Object.freeze({ id: administrator.id, provider: PROVIDER, capabilities: Object.freeze([...ALL]), requiresPasswordChange: administrator.temporary }); this.subjects.set(subject, { generation: administrator.generation, digest: session.digest }); return subject; });\n }\n authorize(subject: ManagementSubject, _capability: string): boolean {\n this.alive(); const issued = this.subjects.get(subject); if (!issued || subject.provider !== PROVIDER) return false;\n const s = this.read().value, session = s.sessions.find(x => x.digest === issued.digest), m = session && this.live(s, session);\n return !!m && m.id === subject.id && m.generation === issued.generation;\n }\n private requireCapability(c: ControlApiHandlerContext, capability: string, write = false) { if (!c.subject || !this.authorize(c.subject, capability)) fail('forbidden', 403); if (write) this.checkWrite(c.request); }\n validateWrite(request: Request) { this.checkWrite(request); }\n csrfToken(request: Request): string | undefined { const c = credentials(request); return c?.transport === 'cookie' ? csrf(c.token) : undefined; }\n private checkWrite(request: Request) { const c = credentials(request); if (!c) fail('unauthorized', 401); if (c.transport === 'cookie') { origin(request, this.host.managementOrigin); if (!constant(request.headers.get('x-csrf-token') ?? '', csrf(c.token))) fail('invalid_csrf', 403); } }\n async login(request: Request): Promise<Response> {\n try {\n this.alive(); if (request.method !== 'POST') fail('method_not_allowed', 405);\n const b = await body(request); exact(b, ['username','password','transport']);\n const name = username(b.username); const p = typeof b.password === 'string' ? b.password : '';\n const transport = b.transport ?? 'cookie'; if (transport !== 'cookie' && transport !== 'bearer') fail('invalid_input');\n if (transport === 'cookie') origin(request, this.host.managementOrigin); else if (request.headers.has('origin')) origin(request, this.host.managementOrigin);\n const source = (this.dependencies.trustedSource ?? this.host.trustedSource)?.(request) ?? 'unknown';\n const keys = ['account:' + digest(name), 'source:' + digest(source)]; const now = this.now();\n // Reserve before password work so simultaneous requests cannot bypass throttling.\n this.mutate(s => { s.failures = s.failures.filter(x => x.until > now); if (keys.some(k => s.failures.some(x => x.key === k && x.count >= 10))) fail('login_limited', 429); for (const key of keys) { let f = s.failures.find(x => x.key === key); if (!f) { f = { key, count: 0, until: now + 15 * 60_000 }; s.failures.push(f); } f.count++; } if (s.failures.length > 1024) fail('login_limited', 429); });\n const candidate = this.read().value.administrator; const administrator = candidate?.username === name ? candidate : null;\n const valid = await Bun.password.verify(p, administrator?.passwordHash ?? (this.dummy || await this.hash('dummy-invalid-password')));\n if (!valid || !administrator || administrator.disabled) fail('invalid_credentials', 401);\n const token = randomBytes(32).toString('base64url');\n this.mutate(s => { const m = s.administrator; if (!m || m.id !== administrator.id || m.disabled || m.generation !== administrator.generation) fail('invalid_credentials', 401); s.sessions = s.sessions.filter(x => !!this.live(s, x)); if (s.sessions.length >= 512) fail('session_limit', 429); s.sessions.push({ digest: digest(token), administratorId: m.id, generation: m.generation, created: this.now(), touched: this.now(), transport }); s.failures = s.failures.filter(x => x.key !== keys[0]); const sourceFailure = s.failures.find(x => x.key === keys[1]); if (sourceFailure) sourceFailure.count = Math.max(0, sourceFailure.count - 1); });\n return transport === 'cookie' ? json({ administrator: dto(administrator), csrfToken: csrf(token) }, 200, { 'set-cookie': cookie(request, token, false, this.host.managementOrigin) }) : json({ administrator: dto(administrator), token, expiresIn: ABSOLUTE / 1000 });\n } catch (e) { return safe(e); }\n }\n async logout(request: Request): Promise<Response> {\n try { if (request.method !== 'POST') fail('method_not_allowed', 405); this.checkWrite(request); const c = credentials(request)!; this.mutate(s => { s.sessions = s.sessions.filter(x => x.digest !== digest(c.token)); }); return json({ ok: true }, 200, { 'set-cookie': cookie(request, '', true, this.host.managementOrigin) }); } catch(e) { return safe(e); }\n }\n private async changePassword(id: string, b: Record<string, unknown>, guard: () => void) {\n const p = password(b.password); if (b.passwordConfirmation !== p) fail('password_confirmation'); const m = this.read().value.administrator; if (!m || m.id !== id || typeof b.currentPassword !== 'string' || !await Bun.password.verify(b.currentPassword, m.passwordHash)) fail('invalid_credentials', 401); if (constant(p, b.currentPassword)) fail('password_unchanged'); const hashed = await this.hash(p);\n this.mutate(s => { guard(); const current = s.administrator; if (!current || current.id !== id || current.disabled || current.generation !== m.generation) fail('version_conflict', 409); if (!Number.isSafeInteger(current.generation + 1)) fail('generation_overflow'); current.passwordHash = hashed; current.temporary = false; current.generation++; s.sessions = []; });\n }\n}\nexport function createControl(host: ControlHostContext, dependencies: Dependencies = {}): LocalAccountsControl { return new LocalAccountsControl(host, dependencies); }\nexport const api = [\n { path: '/password', methods: ['POST'], handler: 'changePassword' },\n { path: '/logout', methods: ['POST'], handler: 'logout' },\n { path: '/self', methods: ['GET'], handler: 'self' },\n] as const;\nexport const rpc = [];\nexport const controlApi = api, controlRpc = rpc;\nexport async function recoverIdentity(input: unknown, context: { durableState: PluginDurableState }) {\n const b = object(input); exact(b, ['username', 'newUsername', 'password', 'reason']);\n const name = b.username === undefined ? undefined : username(b.username), p = password(b.password);\n const newName = b.newUsername === undefined ? undefined : username(b.newUsername);\n if (typeof b.reason !== 'string' || b.reason.trim().length < 1 || b.reason.length > 512) fail('invalid_reason');\n const state = context.durableState, record = state.get('accounts');\n const s = record ? validateAccountState(record.value, name) : { schema: 2 as const, administrator: null, sessions: [], failures: [] };\n let administrator = s.administrator;\n if (administrator && name !== undefined && administrator.username !== name) fail('recovery_requires_existing_administrator');\n const passwordHash = await Bun.password.hash(p, { algorithm: 'argon2id', memoryCost: 65536, timeCost: 3 });\n if (!administrator) { administrator = { id: randomUUID(), username: newName ?? name ?? fail('invalid_input'), disabled: false, temporary: false, passwordHash, generation: 1 }; s.administrator = administrator; }\n else { if (!Number.isSafeInteger(administrator.generation + 1)) fail('generation_overflow'); administrator.username = newName ?? administrator.username; administrator.passwordHash = passwordHash; administrator.disabled = false; administrator.temporary = false; administrator.generation++; }\n s.sessions = [];\n s.failures = [];\n state.execute({commandId: randomUUID(), mutations: [\n {key: 'accounts', expectedVersion: record?.version ?? 0, value: s as unknown as DurableJson},\n {key: 'recovery:' + randomUUID(), expectedVersion: 0, value: {administratorId: administrator.id, reason: b.reason, at: Date.now()}},\n ]});\n return dto(administrator);\n}\nexport function readManagementSetup(state: Pick<PluginDurableState,'get'|'list'>) {\n const record = state.get('accounts');\n return {initialized:record ? !!validateAccountState(record.value).administrator : false};\n}\nexport default { createControl, readManagementSetup, offlineRecovery: {kind: 'identity', recover: recoverIdentity} } satisfies ControlPlugin;\n",
|
|
6
|
-
"import type { Database } from 'bun:sqlite';\nimport { createHash } from 'node:crypto';\n\n/** Configuration migration owns this DDL; constructors never modify schema. */\nexport const PLUGIN_DURABLE_STATE_SCHEMA_SQL = `\nCREATE TABLE plugin_durable_records (\n namespace TEXT NOT NULL, key TEXT NOT NULL, version INTEGER NOT NULL CHECK(version > 0),\n value_json TEXT NOT NULL, PRIMARY KEY(namespace, key)\n) STRICT;\nCREATE TABLE plugin_durable_commands (\n namespace TEXT NOT NULL, command_id TEXT NOT NULL, fingerprint TEXT NOT NULL,\n result_json TEXT NOT NULL, PRIMARY KEY(namespace, command_id)\n) STRICT;\n`;\nexport type DurableJson = null | boolean | number | string | DurableJson[] | { [key: string]: DurableJson };\nexport interface DurableRecord { readonly key: string; readonly version: number; readonly value: DurableJson }\nexport interface DurableMutation { key: string; expectedVersion: number; value: DurableJson }\nexport interface DurableCommand { commandId: string; mutations: readonly DurableMutation[] }\nexport interface PluginDurableState {\n get(key: string): DurableRecord | null;\n list(): readonly DurableRecord[];\n execute(command: DurableCommand): readonly DurableRecord[];\n}\nexport class DurableStateConflictError extends Error {\n constructor() { super('Durable state version conflict'); this.name = 'DurableStateConflictError'; }\n}\nconst MAX_BYTES = 1024 * 1024;\nfunction identifier(value: unknown): asserts value is string {\n if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(value)) throw new Error('Invalid durable state identifier');\n}\n/** Reject objects with behavior, lossy JSON values and cycles before JSON.stringify. */\nfunction encode(value: unknown): string {\n const ancestors = new Set<object>();\n function check(current: unknown, depth: number): void {\n if (depth > 64) throw new Error('Durable state nesting limit exceeded');\n if (current === null || typeof current === 'string' || typeof current === 'boolean') return;\n if (typeof current === 'number' && Number.isFinite(current)) return;\n if (typeof current !== 'object' || current === null || ancestors.has(current)) throw new Error('Durable state must be pure JSON');\n const array = Array.isArray(current);\n if (!array && Object.getPrototypeOf(current) !== Object.prototype && Object.getPrototypeOf(current) !== null) throw new Error('Durable state must be pure JSON');\n ancestors.add(current);\n const descriptors = Object.getOwnPropertyDescriptors(current);\n if (Reflect.ownKeys(current).some(key => typeof key === 'symbol')) throw new Error('Durable state must be pure JSON');\n if (array) {\n const length = (current as unknown[]).length;\n const indices = Object.keys(descriptors).filter(key => key !== 'length');\n if (indices.length !== length || indices.some((key, index) => key !== String(index))) throw new Error('Durable state must be pure JSON');\n }\n for (const [key, descriptor] of Object.entries(descriptors)) {\n if (array && key === 'length') continue;\n if (!descriptor.enumerable || !('value' in descriptor)) throw new Error('Durable state must be pure JSON');\n check(descriptor.value, depth + 1);\n }\n ancestors.delete(current);\n }\n check(value, 0);\n const json = JSON.stringify(value);\n if (Buffer.byteLength(json) > MAX_BYTES) throw new Error('Durable state size limit exceeded');\n return json;\n}\ntype Row = { key: string; version: number; value_json: string };\nfunction record(row: Row): DurableRecord {\n return Object.freeze({ key: row.key, version: row.version, value: JSON.parse(row.value_json) as DurableJson });\n}\n\n/** Host-only factory. Inject only the returned namespace capability into trusted plugins.\n * Consumers validate their own record schema. expectedVersion=0 requires absence.\n * Null values are retained records, so versions and historical command IDs never reset.\n */\nexport class PluginDurableStateStore {\n constructor(private readonly db: Database) {}\n forNamespace(namespace: string): PluginDurableState {\n identifier(namespace);\n const db = this.db;\n const get = (key: string): DurableRecord | null => {\n identifier(key);\n const row = db.query<Row, [string, string]>('SELECT key,version,value_json FROM plugin_durable_records WHERE namespace = ? AND key = ?').get(namespace, key);\n return row ? record(row) : null;\n };\n return Object.freeze({\n get,\n list: () => Object.freeze(db.query<Row, [string]>('SELECT key,version,value_json FROM plugin_durable_records WHERE namespace = ? ORDER BY key').all(namespace).map(record)),\n execute: (command: DurableCommand): readonly DurableRecord[] => {\n identifier(command.commandId);\n if (!Array.isArray(command.mutations) || command.mutations.length === 0 || command.mutations.length > 256) throw new Error('Invalid durable state command');\n const keys = new Set<string>();\n const mutations = command.mutations.map(mutation => {\n identifier(mutation.key);\n if (keys.has(mutation.key) || !Number.isSafeInteger(mutation.expectedVersion) || mutation.expectedVersion < 0 || mutation.expectedVersion >= Number.MAX_SAFE_INTEGER) throw new Error('Invalid durable state mutation');\n keys.add(mutation.key);\n return { key: mutation.key, expectedVersion: mutation.expectedVersion, json: encode(mutation.value) };\n });\n const commandJson = JSON.stringify(mutations);\n if (Buffer.byteLength(commandJson) > MAX_BYTES) throw new Error('Durable state command size limit exceeded');\n const fingerprint = createHash('sha256').update(commandJson).digest('hex');\n return db.transaction(() => {\n const prior = db.query<{ fingerprint: string; result_json: string }, [string, string]>('SELECT fingerprint,result_json FROM plugin_durable_commands WHERE namespace = ? AND command_id = ?').get(namespace, command.commandId);\n if (prior) {\n if (prior.fingerprint !== fingerprint) throw new Error('Durable state command ID already used');\n return Object.freeze((JSON.parse(prior.result_json) as DurableRecord[]).map(item => Object.freeze(item)));\n }\n const results: DurableRecord[] = [];\n for (const mutation of mutations) {\n const current = get(mutation.key);\n if ((current?.version ?? 0) !== mutation.expectedVersion) throw new DurableStateConflictError();\n const version = mutation.expectedVersion + 1;\n db.query(`INSERT INTO plugin_durable_records(namespace,key,version,value_json) VALUES (?,?,?,?)\n ON CONFLICT(namespace,key) DO UPDATE SET version=excluded.version,value_json=excluded.value_json`).run(namespace, mutation.key, version, mutation.json);\n results.push(record({ key: mutation.key, version, value_json: mutation.json }));\n }\n db.query('INSERT INTO plugin_durable_commands(namespace,command_id,fingerprint,result_json) VALUES (?,?,?,?)').run(namespace, command.commandId, fingerprint, JSON.stringify(results));\n return Object.freeze(results);\n }).immediate();\n },\n });\n }\n}\n"
|
|
5
|
+
"import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';\nimport type { ControlHostContext, ControlApiHandlerContext, PluginControl, ControlPlugin } from '../../../packages/core/src/plugin-control/contracts';\nimport type { ManagementProvider, ManagementSubject } from '../../../packages/core/src/plugin-extensions';\nimport { isDurableStateConflictError, type DurableJson, type PluginDurableState } from '../../../packages/core/src/plugin-durable-state';\n\ninterface Administrator { id: string; username: string; disabled: boolean; passwordHash: string; temporary: boolean; generation: number }\ninterface SessionPolicy { idleTimeoutMinutes: number; absoluteTimeoutMinutes: number }\ninterface Session { digest: string; administratorId: string; generation: number; created: number; touched: number; transport: 'cookie' | 'bearer'; policy?: SessionPolicy }\ninterface Failure { key: string; count: number; until: number }\ninterface State { schema: 2; administrator: Administrator | null; sessions: Session[]; failures: Failure[] }\nexport interface Dependencies { now?: () => number; trustedSource?: (request: Request) => string }\nconst PROVIDER = 'local-accounts';\nconst COOKIE = 'bungee_local_session';\nconst DEFAULT_SESSION_POLICY: Readonly<SessionPolicy> = Object.freeze({ idleTimeoutMinutes: 30, absoluteTimeoutMinutes: 480 });\n// Chromium caps persistent cookies at 400 days. Verification renews this browser lease,\n// never the server-side absolute deadline. Browser/user cleanup remains outside our control.\nconst COOKIE_LEASE_SECONDS = 400 * 24 * 60 * 60;\nconst MAX_TIMEOUT_MINUTES = Math.floor(Number.MAX_SAFE_INTEGER / 60_000);\nconst ALL = ['config.read', 'config.write', 'logs.read', 'logs.body', 'keys.read', 'keys.write', 'plugins.read', 'plugins.toggle', 'plugins.code', 'auth.mode', 'self.password', 'self.logout'];\nclass AccountError extends Error { constructor(readonly code: string, readonly status = 400) { super(code); } }\nfunction fail(code: string, status = 400): never { throw new AccountError(code, status); }\nfunction object(v: unknown): Record<string, unknown> { if (!v || typeof v !== 'object' || Array.isArray(v)) fail('invalid_input'); return v as Record<string, unknown>; }\nfunction exact(v: Record<string, unknown>, fields: string[]) { if (Object.keys(v).some(k => !fields.includes(k))) fail('invalid_input'); }\nfunction username(v: unknown): string { if (typeof v !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,63}$/.test(v)) fail('invalid_input'); return v.toLowerCase(); }\nfunction password(v: unknown): string { if (typeof v !== 'string' || [...v].length < 6 || [...v].length > 64) fail('invalid_password'); return v; }\nfunction digest(v: string): string { return createHash('sha256').update(v).digest('hex'); }\nfunction csrf(token: string): string { return createHmac('sha256', token).update('local-accounts/csrf/v1').digest('base64url'); }\nfunction constant(a: string, b: string): boolean { const aa = Buffer.from(a), bb = Buffer.from(b); return aa.length === bb.length && timingSafeEqual(aa, bb); }\nfunction dto(m: Administrator) { return { id: m.id, username: m.username, requiresPasswordChange: m.temporary }; }\nfunction json(v: unknown, status = 200, headers: Record<string, string> = {}): Response { return Response.json(v, { status, headers: { 'cache-control': 'no-store', ...headers } }); }\nfunction safe(error: unknown): Response { return error instanceof AccountError ? json({ error: error.code }, error.status) : json({ error: 'provider_unavailable' }, 503); }\nasync function body(request: Request): Promise<Record<string, unknown>> {\n const reader = request.body?.getReader(); if (!reader) fail('invalid_input');\n let size = 0; const chunks: Uint8Array[] = [];\n try { for (;;) { const r = await reader.read(); if (r.done) break; size += r.value.length; if (size > 8192) { await reader.cancel(); fail('body_limit', 413); } chunks.push(r.value); } }\n finally { reader.releaseLock(); }\n try { return object(JSON.parse(Buffer.concat(chunks).toString('utf8'))); } catch (e) { if (e instanceof AccountError) throw e; return fail('invalid_input'); }\n}\nfunction credentials(request: Request): { token: string; transport: Session['transport'] } | null {\n const auth = request.headers.get('authorization');\n if (auth !== null) { const m = /^Bearer ([A-Za-z0-9_-]{43})$/.exec(auth); return m ? { token: m[1]!, transport: 'bearer' } : null; }\n const tokens = (request.headers.get('cookie') ?? '').split(';').map(x => x.trim()).filter(x => x.startsWith(COOKIE + '='));\n if (tokens.length !== 1) return null; const token = tokens[0]!.slice(COOKIE.length + 1);\n return /^[A-Za-z0-9_-]{43}$/.test(token) ? { token, transport: 'cookie' } : null;\n}\nfunction origin(request: Request, expected?: string) { if (request.headers.get('origin') !== (expected ?? new URL(request.url).origin)) fail('invalid_origin', 403); }\nfunction cookie(request: Request, token: string, maxAge: number, managementOrigin?: string) { return `${COOKIE}=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${new URL(managementOrigin ?? request.url).protocol === 'https:' ? '; Secure' : ''}`; }\n\nfunction validateSessionPolicy(value: unknown, code = 'invalid_session_policy', status = 400): SessionPolicy {\n if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code, status);\n const p = value as SessionPolicy;\n if (Object.keys(p).some(key => !['idleTimeoutMinutes', 'absoluteTimeoutMinutes'].includes(key))\n || ![p.idleTimeoutMinutes, p.absoluteTimeoutMinutes].every(n => Number.isSafeInteger(n) && n >= 0 && n <= MAX_TIMEOUT_MINUTES)) fail(code, status);\n return { idleTimeoutMinutes: p.idleTimeoutMinutes, absoluteTimeoutMinutes: p.absoluteTimeoutMinutes };\n}\n\nfunction validateAdministrator(m: unknown): asserts m is Administrator {\n const a = m as Administrator;\n if (!a || typeof a.id !== 'string' || !a.id || typeof a.username !== 'string' || !/^[a-z0-9][a-z0-9_.@-]{0,63}$/.test(a.username) || typeof a.disabled !== 'boolean' || typeof a.temporary !== 'boolean' || typeof a.passwordHash !== 'string' || !a.passwordHash.startsWith('$argon2id$') || !Number.isSafeInteger(a.generation) || a.generation < 1) fail('corrupt_state', 503);\n}\n/** Migration never promotes a former admin/viewer. Ambiguous owners require offline selection. */\nfunction validateAccountState(value: unknown, recoveryUsername?: string): State {\n if (!value || typeof value !== 'object' || Array.isArray(value)) fail('corrupt_state', 503);\n const raw = value as Record<string, unknown>;\n if (raw.schema === 1) {\n if (!Array.isArray(raw.members) || !Array.isArray(raw.sessions) || !Array.isArray(raw.failures)) fail('corrupt_state', 503);\n const ids = new Set<string>(), names = new Set<string>();\n for (const member of raw.members) {\n validateAdministrator(member);\n const legacy = member as Administrator & {role: string};\n if (!['owner', 'admin', 'viewer'].includes(legacy.role) || ids.has(member.id) || names.has(member.username)) fail('corrupt_state', 503);\n ids.add(member.id); names.add(member.username);\n }\n const candidates = raw.members.filter(m => m.role === 'owner' && (recoveryUsername ? m.username === recoveryUsername : !m.disabled));\n if (raw.members.length && candidates.length !== 1) fail(recoveryUsername ? 'recovery_requires_existing_administrator' : 'administrator_migration_required', 503);\n const chosen = candidates[0];\n const administrator = chosen ? { id: chosen.id, username: chosen.username, disabled: chosen.disabled, temporary: chosen.temporary, passwordHash: chosen.passwordHash, generation: chosen.generation } : null;\n return {schema: 2, administrator, sessions: [], failures: []};\n }\n const s = raw as unknown as State;\n if (s.schema !== 2 || !Array.isArray(s.sessions) || !Array.isArray(s.failures)) fail('corrupt_state', 503);\n if (s.administrator !== null) validateAdministrator(s.administrator);\n for (const x of s.sessions) if (!x || !/^[a-f0-9]{64}$/.test(x.digest) || x.administratorId !== s.administrator?.id || !Number.isSafeInteger(x.generation) || x.generation < 1 || !Number.isFinite(x.created) || !Number.isFinite(x.touched) || !['cookie','bearer'].includes(x.transport)) fail('corrupt_state', 503);\n for (const x of s.sessions) if (x.policy !== undefined) validateSessionPolicy(x.policy, 'corrupt_state', 503);\n for (const x of s.failures) if (!x || typeof x.key !== 'string' || !Number.isSafeInteger(x.count) || x.count < 0 || !Number.isFinite(x.until)) fail('corrupt_state', 503);\n return s;\n}\n\nexport class LocalAccountsControl implements PluginControl, ManagementProvider {\n readonly management = this;\n readonly rpc = [];\n readonly api;\n private readonly state: PluginDurableState;\n private readonly now: () => number;\n private readonly subjects = new WeakMap<ManagementSubject, { generation: number; digest: string }>();\n private dummy = '';\n private disposed = false;\n constructor(private readonly host: ControlHostContext, private readonly dependencies: Dependencies = {}) {\n if (!host.durableState) fail('durable_state_required', 503);\n this.state = host.durableState;\n this.now = dependencies.now ?? Date.now;\n const invoke = (fn: (c: ControlApiHandlerContext) => Promise<Response> | Response) => async (c: ControlApiHandlerContext) => {\n try { this.alive(); if (c.requestSignal.aborted) fail('cancelled'); return await fn(c); } catch (e) { return safe(e); }\n };\n this.api = [\n { path: '/password', methods: ['POST'], handler: 'changePassword', invoke: invoke(async c => { this.requireCapability(c, 'self.password', true); const b = await body(c.request); exact(b, ['currentPassword', 'password', 'passwordConfirmation']); await this.changePassword(c.subject!.id, b, () => this.requireCapability(c, 'self.password', true)); return json({ ok: true }, 200, { 'set-cookie': cookie(c.request, '', 0, this.host.managementOrigin) }); }) },\n { path: '/logout', methods: ['POST'], handler: 'logout', invoke: invoke(c => { this.requireCapability(c, 'self.logout', true); return this.logout(c.request); }) },\n { path: '/self', methods: ['GET'], handler: 'self', invoke: invoke(c => { this.requireCapability(c, 'self.password'); const m = this.read().value.administrator!; const creds = credentials(c.request); return json({ administrator: dto(m), ...(creds?.transport === 'cookie' ? { csrfToken: csrf(creds.token) } : {}) }); }) },\n { path: '/session-policy', methods: ['GET', 'PUT'], handler: 'sessionPolicy', invoke: invoke(async c => {\n this.requireCapability(c, 'config.write', c.request.method === 'PUT');\n if (c.request.method === 'GET') return json(this.readSessionPolicy());\n const b = await body(c.request); exact(b, ['version', 'policy']);\n if (!Number.isSafeInteger(b.version) || (b.version as number) < 0) fail('invalid_session_policy');\n const policy = validateSessionPolicy(b.policy);\n this.requireCapability(c, 'config.write', true);\n try { this.state.execute({commandId: randomUUID(), mutations: [{key: 'session-policy', expectedVersion: b.version as number, value: policy as unknown as DurableJson}]}); }\n catch (e) { if (isDurableStateConflictError(e)) fail('version_conflict', 409); throw e; }\n return json(this.readSessionPolicy());\n }) },\n ];\n }\n private alive() { if (this.disposed || this.host.signal.aborted) fail('provider_unavailable', 503); }\n private read(): { version: number; value: State } {\n this.alive(); const record = this.state.get('accounts');\n if (!record) return { version: 0, value: { schema: 2, administrator: null, sessions: [], failures: [] } };\n const s = validateAccountState(record.value);\n return { version: record.version, value: s };\n }\n private mutate<T>(fn: (s: State) => T): T {\n for (let i = 0; i < 8; i++) { const {version, value} = this.read(); const result = fn(value);\n try { this.state.execute({ commandId: randomUUID(), mutations: [{ key: 'accounts', expectedVersion: version, value: value as unknown as DurableJson }] }); return result; }\n catch (e) { if (!isDurableStateConflictError(e)) throw e; }\n } return fail('version_conflict', 409);\n }\n private readSessionPolicy(): {version: number; policy: SessionPolicy} {\n this.alive(); const record = this.state.get('session-policy');\n return {version: record?.version ?? 0, policy: record ? validateSessionPolicy(record.value, 'corrupt_state', 503) : {...DEFAULT_SESSION_POLICY}};\n }\n async start() { const record = this.state.get('accounts'); this.read(); this.readSessionPolicy(); if (record && (record.value as any).schema === 1) this.mutate(() => undefined); this.dummy = await this.hash(randomBytes(32).toString('base64url')); }\n dispose() { this.disposed = true; }\n private hash(p: string) { return Bun.password.hash(p, { algorithm: 'argon2id', memoryCost: 65536, timeCost: 3 }); }\n hasIdentity() { const a = this.read().value.administrator; return !!a && !a.disabled; }\n revokeSessions() { this.mutate(s => { s.sessions = []; }); }\n async bootstrap(input: unknown) {\n const b = object(input); exact(b, ['username', 'password', 'passwordConfirmation']); const name = username(b.username), p = password(b.password);\n if (b.passwordConfirmation !== p) fail('password_confirmation');\n const prior = this.read().value.administrator;\n if (prior) {\n if (prior.username !== name || prior.disabled || !await Bun.password.verify(p, prior.passwordHash)) fail('invalid_credentials', 401);\n const latest = this.read().value.administrator;\n if (!latest || latest.generation !== prior.generation || latest.disabled) fail('invalid_credentials', 401);\n return;\n }\n const hashed = await this.hash(p);\n this.mutate(s => { if (s.administrator) fail('bootstrap_conflict', 409); s.administrator = { id: randomUUID(), username: name, disabled: false, temporary: false, passwordHash: hashed, generation: 1 }; });\n }\n private live(s: State, session: Session): Administrator | undefined {\n const now = this.now(), policy = session.policy ?? DEFAULT_SESSION_POLICY;\n if (now < session.touched || now < session.created\n || (policy.idleTimeoutMinutes > 0 && now - session.touched >= policy.idleTimeoutMinutes * 60_000)\n || (policy.absoluteTimeoutMinutes > 0 && now - session.created >= policy.absoluteTimeoutMinutes * 60_000)) return;\n const m = s.administrator; return m && m.id === session.administratorId && !m.disabled && m.generation === session.generation ? m : undefined;\n }\n private cookieAge(policy: SessionPolicy, elapsedMs = 0): number {\n return policy.absoluteTimeoutMinutes === 0 ? COOKIE_LEASE_SECONDS\n : Math.max(0, Math.min(COOKIE_LEASE_SECONDS, Math.floor((policy.absoluteTimeoutMinutes * 60_000 - elapsedMs) / 1000)));\n }\n sessionCookie(request: Request): string | undefined {\n this.alive(); const creds = credentials(request); if (creds?.transport !== 'cookie') return;\n const s = this.read().value, session = s.sessions.find(x => x.digest === digest(creds.token) && x.transport === 'cookie');\n if (!session || !this.live(s, session)) return;\n return cookie(request, creds.token, this.cookieAge(session.policy ?? DEFAULT_SESSION_POLICY, this.now() - session.created), this.host.managementOrigin);\n }\n async authenticate(request: Request): Promise<ManagementSubject | null> {\n this.alive(); const c = credentials(request); if (!c) return null;\n return this.mutate(s => { const session = s.sessions.find(x => x.digest === digest(c.token) && x.transport === c.transport); const administrator = session && this.live(s, session); if (!session || !administrator) return null; session.touched = this.now(); const subject = Object.freeze({ id: administrator.id, provider: PROVIDER, capabilities: Object.freeze([...ALL]), requiresPasswordChange: administrator.temporary }); this.subjects.set(subject, { generation: administrator.generation, digest: session.digest }); return subject; });\n }\n authorize(subject: ManagementSubject, _capability: string): boolean {\n this.alive(); const issued = this.subjects.get(subject); if (!issued || subject.provider !== PROVIDER) return false;\n const s = this.read().value, session = s.sessions.find(x => x.digest === issued.digest), m = session && this.live(s, session);\n return !!m && m.id === subject.id && m.generation === issued.generation;\n }\n private requireCapability(c: ControlApiHandlerContext, capability: string, write = false) { if (!c.subject || !this.authorize(c.subject, capability)) fail('forbidden', 403); if (write) this.checkWrite(c.request); }\n validateWrite(request: Request) { this.checkWrite(request); }\n csrfToken(request: Request): string | undefined { const c = credentials(request); return c?.transport === 'cookie' ? csrf(c.token) : undefined; }\n private checkWrite(request: Request) { const c = credentials(request); if (!c) fail('unauthorized', 401); if (c.transport === 'cookie') { origin(request, this.host.managementOrigin); if (!constant(request.headers.get('x-csrf-token') ?? '', csrf(c.token))) fail('invalid_csrf', 403); } }\n async login(request: Request): Promise<Response> {\n try {\n this.alive(); if (request.method !== 'POST') fail('method_not_allowed', 405);\n const b = await body(request); exact(b, ['username','password','transport']);\n const name = username(b.username); const p = typeof b.password === 'string' ? b.password : '';\n const transport = b.transport ?? 'cookie'; if (transport !== 'cookie' && transport !== 'bearer') fail('invalid_input');\n if (transport === 'cookie') origin(request, this.host.managementOrigin); else if (request.headers.has('origin')) origin(request, this.host.managementOrigin);\n const source = (this.dependencies.trustedSource ?? this.host.trustedSource)?.(request) ?? 'unknown';\n const keys = ['account:' + digest(name), 'source:' + digest(source)]; const now = this.now();\n // Reserve before password work so simultaneous requests cannot bypass throttling.\n this.mutate(s => { s.failures = s.failures.filter(x => x.until > now); if (keys.some(k => s.failures.some(x => x.key === k && x.count >= 10))) fail('login_limited', 429); for (const key of keys) { let f = s.failures.find(x => x.key === key); if (!f) { f = { key, count: 0, until: now + 15 * 60_000 }; s.failures.push(f); } f.count++; } if (s.failures.length > 1024) fail('login_limited', 429); });\n const candidate = this.read().value.administrator; const administrator = candidate?.username === name ? candidate : null;\n const valid = await Bun.password.verify(p, administrator?.passwordHash ?? (this.dummy || await this.hash('dummy-invalid-password')));\n if (!valid || !administrator || administrator.disabled) fail('invalid_credentials', 401);\n const token = randomBytes(32).toString('base64url');\n const policy = this.readSessionPolicy().policy;\n const created = this.now();\n this.mutate(s => { const m = s.administrator; if (!m || m.id !== administrator.id || m.disabled || m.generation !== administrator.generation) fail('invalid_credentials', 401); s.sessions = s.sessions.filter(x => !!this.live(s, x)); if (s.sessions.length >= 512) fail('session_limit', 429); s.sessions.push({ digest: digest(token), administratorId: m.id, generation: m.generation, created, touched: created, transport, policy }); s.failures = s.failures.filter(x => x.key !== keys[0]); const sourceFailure = s.failures.find(x => x.key === keys[1]); if (sourceFailure) sourceFailure.count = Math.max(0, sourceFailure.count - 1); });\n return transport === 'cookie' ? json({ administrator: dto(administrator), csrfToken: csrf(token) }, 200, { 'set-cookie': cookie(request, token, this.cookieAge(policy, this.now() - created), this.host.managementOrigin) }) : json({ administrator: dto(administrator), token, expiresIn: policy.absoluteTimeoutMinutes === 0 ? null : policy.absoluteTimeoutMinutes * 60 });\n } catch (e) { return safe(e); }\n }\n async logout(request: Request): Promise<Response> {\n try { if (request.method !== 'POST') fail('method_not_allowed', 405); this.checkWrite(request); const c = credentials(request)!; this.mutate(s => { s.sessions = s.sessions.filter(x => x.digest !== digest(c.token)); }); return json({ ok: true }, 200, { 'set-cookie': cookie(request, '', 0, this.host.managementOrigin) }); } catch(e) { return safe(e); }\n }\n private async changePassword(id: string, b: Record<string, unknown>, guard: () => void) {\n const p = password(b.password); if (b.passwordConfirmation !== p) fail('password_confirmation'); const m = this.read().value.administrator; if (!m || m.id !== id || typeof b.currentPassword !== 'string' || !await Bun.password.verify(b.currentPassword, m.passwordHash)) fail('invalid_credentials', 401); if (constant(p, b.currentPassword)) fail('password_unchanged'); const hashed = await this.hash(p);\n this.mutate(s => { guard(); const current = s.administrator; if (!current || current.id !== id || current.disabled || current.generation !== m.generation) fail('version_conflict', 409); if (!Number.isSafeInteger(current.generation + 1)) fail('generation_overflow'); current.passwordHash = hashed; current.temporary = false; current.generation++; s.sessions = []; });\n }\n}\nexport function createControl(host: ControlHostContext, dependencies: Dependencies = {}): LocalAccountsControl { return new LocalAccountsControl(host, dependencies); }\nexport const api = [\n { path: '/password', methods: ['POST'], handler: 'changePassword' },\n { path: '/logout', methods: ['POST'], handler: 'logout' },\n { path: '/self', methods: ['GET'], handler: 'self' },\n { path: '/session-policy', methods: ['GET', 'PUT'], handler: 'sessionPolicy' },\n] as const;\nexport const rpc = [];\nexport const controlApi = api, controlRpc = rpc;\nexport async function recoverIdentity(input: unknown, context: { durableState: PluginDurableState }) {\n const b = object(input); exact(b, ['username', 'newUsername', 'password', 'reason']);\n const name = b.username === undefined ? undefined : username(b.username), p = password(b.password);\n const newName = b.newUsername === undefined ? undefined : username(b.newUsername);\n if (typeof b.reason !== 'string' || b.reason.trim().length < 1 || b.reason.length > 512) fail('invalid_reason');\n const state = context.durableState, record = state.get('accounts');\n const s = record ? validateAccountState(record.value, name) : { schema: 2 as const, administrator: null, sessions: [], failures: [] };\n let administrator = s.administrator;\n if (administrator && name !== undefined && administrator.username !== name) fail('recovery_requires_existing_administrator');\n const passwordHash = await Bun.password.hash(p, { algorithm: 'argon2id', memoryCost: 65536, timeCost: 3 });\n if (!administrator) { administrator = { id: randomUUID(), username: newName ?? name ?? fail('invalid_input'), disabled: false, temporary: false, passwordHash, generation: 1 }; s.administrator = administrator; }\n else { if (!Number.isSafeInteger(administrator.generation + 1)) fail('generation_overflow'); administrator.username = newName ?? administrator.username; administrator.passwordHash = passwordHash; administrator.disabled = false; administrator.temporary = false; administrator.generation++; }\n s.sessions = [];\n s.failures = [];\n state.execute({commandId: randomUUID(), mutations: [\n {key: 'accounts', expectedVersion: record?.version ?? 0, value: s as unknown as DurableJson},\n {key: 'recovery:' + randomUUID(), expectedVersion: 0, value: {administratorId: administrator.id, reason: b.reason, at: Date.now()}},\n ]});\n return dto(administrator);\n}\nexport function readManagementSetup(state: Pick<PluginDurableState,'get'|'list'>) {\n const record = state.get('accounts');\n return {initialized:record ? !!validateAccountState(record.value).administrator : false};\n}\nexport default { createControl, readManagementSetup, offlineRecovery: {kind: 'identity', recover: recoverIdentity} } satisfies ControlPlugin;\n",
|
|
6
|
+
"import type { Database } from 'bun:sqlite';\nimport { createHash } from 'node:crypto';\n\n/** Configuration migration owns this DDL; constructors never modify schema. */\nexport const PLUGIN_DURABLE_STATE_SCHEMA_SQL = `\nCREATE TABLE plugin_durable_records (\n namespace TEXT NOT NULL, key TEXT NOT NULL, version INTEGER NOT NULL CHECK(version > 0),\n value_json TEXT NOT NULL, PRIMARY KEY(namespace, key)\n) STRICT;\nCREATE TABLE plugin_durable_commands (\n namespace TEXT NOT NULL, command_id TEXT NOT NULL, fingerprint TEXT NOT NULL,\n result_json TEXT NOT NULL, PRIMARY KEY(namespace, command_id)\n) STRICT;\n`;\nexport type DurableJson = null | boolean | number | string | DurableJson[] | { [key: string]: DurableJson };\nexport interface DurableRecord { readonly key: string; readonly version: number; readonly value: DurableJson }\nexport interface DurableMutation { key: string; expectedVersion: number; value: DurableJson }\nexport interface DurableCommand { commandId: string; mutations: readonly DurableMutation[] }\nexport interface PluginDurableState {\n get(key: string): DurableRecord | null;\n list(): readonly DurableRecord[];\n execute(command: DurableCommand): readonly DurableRecord[];\n}\nexport class DurableStateConflictError extends Error {\n readonly code = 'durable_state_conflict';\n constructor() { super('Durable state version conflict'); this.name = 'DurableStateConflictError'; }\n}\n/** Plugins load as independent bundles, so the host's error has a different constructor. */\nexport function isDurableStateConflictError(error: unknown): error is DurableStateConflictError {\n return error instanceof Error && 'code' in error && error.code === 'durable_state_conflict';\n}\nconst MAX_BYTES = 1024 * 1024;\nfunction identifier(value: unknown): asserts value is string {\n if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(value)) throw new Error('Invalid durable state identifier');\n}\n/** Reject objects with behavior, lossy JSON values and cycles before JSON.stringify. */\nfunction encode(value: unknown): string {\n const ancestors = new Set<object>();\n function check(current: unknown, depth: number): void {\n if (depth > 64) throw new Error('Durable state nesting limit exceeded');\n if (current === null || typeof current === 'string' || typeof current === 'boolean') return;\n if (typeof current === 'number' && Number.isFinite(current)) return;\n if (typeof current !== 'object' || current === null || ancestors.has(current)) throw new Error('Durable state must be pure JSON');\n const array = Array.isArray(current);\n if (!array && Object.getPrototypeOf(current) !== Object.prototype && Object.getPrototypeOf(current) !== null) throw new Error('Durable state must be pure JSON');\n ancestors.add(current);\n const descriptors = Object.getOwnPropertyDescriptors(current);\n if (Reflect.ownKeys(current).some(key => typeof key === 'symbol')) throw new Error('Durable state must be pure JSON');\n if (array) {\n const length = (current as unknown[]).length;\n const indices = Object.keys(descriptors).filter(key => key !== 'length');\n if (indices.length !== length || indices.some((key, index) => key !== String(index))) throw new Error('Durable state must be pure JSON');\n }\n for (const [key, descriptor] of Object.entries(descriptors)) {\n if (array && key === 'length') continue;\n if (!descriptor.enumerable || !('value' in descriptor)) throw new Error('Durable state must be pure JSON');\n check(descriptor.value, depth + 1);\n }\n ancestors.delete(current);\n }\n check(value, 0);\n const json = JSON.stringify(value);\n if (Buffer.byteLength(json) > MAX_BYTES) throw new Error('Durable state size limit exceeded');\n return json;\n}\ntype Row = { key: string; version: number; value_json: string };\nfunction record(row: Row): DurableRecord {\n return Object.freeze({ key: row.key, version: row.version, value: JSON.parse(row.value_json) as DurableJson });\n}\n\n/** Host-only factory. Inject only the returned namespace capability into trusted plugins.\n * Consumers validate their own record schema. expectedVersion=0 requires absence.\n * Null values are retained records, so versions and historical command IDs never reset.\n */\nexport class PluginDurableStateStore {\n constructor(private readonly db: Database) {}\n forNamespace(namespace: string): PluginDurableState {\n identifier(namespace);\n const db = this.db;\n const get = (key: string): DurableRecord | null => {\n identifier(key);\n const row = db.query<Row, [string, string]>('SELECT key,version,value_json FROM plugin_durable_records WHERE namespace = ? AND key = ?').get(namespace, key);\n return row ? record(row) : null;\n };\n return Object.freeze({\n get,\n list: () => Object.freeze(db.query<Row, [string]>('SELECT key,version,value_json FROM plugin_durable_records WHERE namespace = ? ORDER BY key').all(namespace).map(record)),\n execute: (command: DurableCommand): readonly DurableRecord[] => {\n identifier(command.commandId);\n if (!Array.isArray(command.mutations) || command.mutations.length === 0 || command.mutations.length > 256) throw new Error('Invalid durable state command');\n const keys = new Set<string>();\n const mutations = command.mutations.map(mutation => {\n identifier(mutation.key);\n if (keys.has(mutation.key) || !Number.isSafeInteger(mutation.expectedVersion) || mutation.expectedVersion < 0 || mutation.expectedVersion >= Number.MAX_SAFE_INTEGER) throw new Error('Invalid durable state mutation');\n keys.add(mutation.key);\n return { key: mutation.key, expectedVersion: mutation.expectedVersion, json: encode(mutation.value) };\n });\n const commandJson = JSON.stringify(mutations);\n if (Buffer.byteLength(commandJson) > MAX_BYTES) throw new Error('Durable state command size limit exceeded');\n const fingerprint = createHash('sha256').update(commandJson).digest('hex');\n return db.transaction(() => {\n const prior = db.query<{ fingerprint: string; result_json: string }, [string, string]>('SELECT fingerprint,result_json FROM plugin_durable_commands WHERE namespace = ? AND command_id = ?').get(namespace, command.commandId);\n if (prior) {\n if (prior.fingerprint !== fingerprint) throw new Error('Durable state command ID already used');\n return Object.freeze((JSON.parse(prior.result_json) as DurableRecord[]).map(item => Object.freeze(item)));\n }\n const results: DurableRecord[] = [];\n for (const mutation of mutations) {\n const current = get(mutation.key);\n if ((current?.version ?? 0) !== mutation.expectedVersion) throw new DurableStateConflictError();\n const version = mutation.expectedVersion + 1;\n db.query(`INSERT INTO plugin_durable_records(namespace,key,version,value_json) VALUES (?,?,?,?)\n ON CONFLICT(namespace,key) DO UPDATE SET version=excluded.version,value_json=excluded.value_json`).run(namespace, mutation.key, version, mutation.json);\n results.push(record({ key: mutation.key, version, value_json: mutation.json }));\n }\n db.query('INSERT INTO plugin_durable_commands(namespace,command_id,fingerprint,result_json) VALUES (?,?,?,?)').run(namespace, command.commandId, fingerprint, JSON.stringify(results));\n return Object.freeze(results);\n }).immediate();\n },\n });\n }\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "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",
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": ";;AAAA;;;AC4BO,SAAS,2BAA2B,CAAC,OAAoD;AAAA,EAC9F,OAAO,iBAAiB,SAAS,UAAU,SAAS,MAAM,SAAS;AAAA;AAErE,IAAM,YAAY,OAAO;;;ADpBzB,IAAM,WAAW;AACjB,IAAM,SAAS;AACf,IAAM,yBAAkD,OAAO,OAAO,EAAE,oBAAoB,IAAI,wBAAwB,IAAI,CAAC;AAG7H,IAAM,uBAAuB,MAAM,KAAK,KAAK;AAC7C,IAAM,sBAAsB,KAAK,MAAM,OAAO,mBAAmB,KAAM;AACvE,IAAM,MAAM,CAAC,eAAe,gBAAgB,aAAa,aAAa,aAAa,cAAc,gBAAgB,kBAAkB,gBAAgB,aAAa,iBAAiB,aAAa;AAAA;AAC9L,MAAM,qBAAqB,MAAM;AAAA,EAAuB;AAAA,EAAuB;AAAA,EAA5C,WAAW,CAAU,MAAuB,SAAS,KAAK;AAAA,IAAE,MAAM,IAAI;AAAA,IAAjD;AAAA,IAAuB;AAAA;AAA+B;AAC9G,SAAS,IAAI,CAAC,MAAc,SAAS,KAAY;AAAA,EAAE,MAAM,IAAI,aAAa,MAAM,MAAM;AAAA;AACtF,SAAS,MAAM,CAAC,GAAqC;AAAA,EAAE,IAAI,CAAC,KAAK,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC;AAAA,IAAG,KAAK,eAAe;AAAA,EAAG,OAAO;AAAA;AAC1I,SAAS,KAAK,CAAC,GAA4B,QAAkB;AAAA,EAAE,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,OAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAAA,IAAG,KAAK,eAAe;AAAA;AACtI,SAAS,QAAQ,CAAC,GAAoB;AAAA,EAAE,IAAI,OAAO,MAAM,YAAY,CAAC,qCAAqC,KAAK,CAAC;AAAA,IAAG,KAAK,eAAe;AAAA,EAAG,OAAO,EAAE,YAAY;AAAA;AAChK,SAAS,QAAQ,CAAC,GAAoB;AAAA,EAAE,IAAI,OAAO,MAAM,YAAY,CAAC,GAAG,CAAC,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,SAAS;AAAA,IAAI,KAAK,kBAAkB;AAAA,EAAG,OAAO;AAAA;AAC/I,SAAS,MAAM,CAAC,GAAmB;AAAA,EAAE,OAAO,WAAW,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,KAAK;AAAA;AACvF,SAAS,IAAI,CAAC,OAAuB;AAAA,EAAE,OAAO,WAAW,UAAU,KAAK,EAAE,OAAO,wBAAwB,EAAE,OAAO,WAAW;AAAA;AAC7H,SAAS,QAAQ,CAAC,GAAW,GAAoB;AAAA,EAAE,MAAM,KAAK,OAAO,KAAK,CAAC,GAAG,KAAK,OAAO,KAAK,CAAC;AAAA,EAAG,OAAO,GAAG,WAAW,GAAG,UAAU,gBAAgB,IAAI,EAAE;AAAA;AAC3J,SAAS,GAAG,CAAC,GAAkB;AAAA,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,UAAU,wBAAwB,EAAE,UAAU;AAAA;AAC9G,SAAS,IAAI,CAAC,GAAY,SAAS,KAAK,UAAkC,CAAC,GAAa;AAAA,EAAE,OAAO,SAAS,KAAK,GAAG,EAAE,QAAQ,SAAS,EAAE,iBAAiB,eAAe,QAAQ,EAAE,CAAC;AAAA;AAClL,SAAS,IAAI,CAAC,OAA0B;AAAA,EAAE,OAAO,iBAAiB,eAAe,KAAK,EAAE,OAAO,MAAM,KAAK,GAAG,MAAM,MAAM,IAAI,KAAK,EAAE,OAAO,uBAAuB,GAAG,GAAG;AAAA;AACxK,eAAe,IAAI,CAAC,SAAoD;AAAA,EACtE,MAAM,SAAS,QAAQ,MAAM,UAAU;AAAA,EAAG,IAAI,CAAC;AAAA,IAAQ,KAAK,eAAe;AAAA,EAC3E,IAAI,OAAO;AAAA,EAAG,MAAM,SAAuB,CAAC;AAAA,EAC5C,IAAI;AAAA,IAAE,UAAS;AAAA,MAAE,MAAM,IAAI,MAAM,OAAO,KAAK;AAAA,MAAG,IAAI,EAAE;AAAA,QAAM;AAAA,MAAO,QAAQ,EAAE,MAAM;AAAA,MAAQ,IAAI,OAAO,MAAM;AAAA,QAAE,MAAM,OAAO,OAAO;AAAA,QAAG,KAAK,cAAc,GAAG;AAAA,MAAG;AAAA,MAAE,OAAO,KAAK,EAAE,KAAK;AAAA,IAAG;AAAA,YACtL;AAAA,IAAU,OAAO,YAAY;AAAA;AAAA,EAC7B,IAAI;AAAA,IAAE,OAAO,OAAO,KAAK,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,CAAC,CAAC;AAAA,IAAK,OAAO,GAAG;AAAA,IAAE,IAAI,aAAa;AAAA,MAAc,MAAM;AAAA,IAAG,OAAO,KAAK,eAAe;AAAA;AAAA;AAE5J,SAAS,WAAW,CAAC,SAA6E;AAAA,EAChG,MAAM,OAAO,QAAQ,QAAQ,IAAI,eAAe;AAAA,EAChD,IAAI,SAAS,MAAM;AAAA,IAAE,MAAM,IAAI,+BAA+B,KAAK,IAAI;AAAA,IAAG,OAAO,IAAI,EAAE,OAAO,EAAE,IAAK,WAAW,SAAS,IAAI;AAAA,EAAM;AAAA,EACnI,MAAM,UAAU,QAAQ,QAAQ,IAAI,QAAQ,KAAK,IAAI,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC,EAAE,OAAO,OAAK,EAAE,WAAW,SAAS,GAAG,CAAC;AAAA,EACzH,IAAI,OAAO,WAAW;AAAA,IAAG,OAAO;AAAA,EAAM,MAAM,QAAQ,OAAO,GAAI,MAAM,OAAO,SAAS,CAAC;AAAA,EACtF,OAAO,sBAAsB,KAAK,KAAK,IAAI,EAAE,OAAO,WAAW,SAAS,IAAI;AAAA;AAE9E,SAAS,MAAM,CAAC,SAAkB,UAAmB;AAAA,EAAE,IAAI,QAAQ,QAAQ,IAAI,QAAQ,OAAO,YAAY,IAAI,IAAI,QAAQ,GAAG,EAAE;AAAA,IAAS,KAAK,kBAAkB,GAAG;AAAA;AAClK,SAAS,MAAM,CAAC,SAAkB,OAAe,QAAgB,kBAA2B;AAAA,EAAE,OAAO,GAAG,UAAU,qDAAqD,SAAS,IAAI,IAAI,oBAAoB,QAAQ,GAAG,EAAE,aAAa,WAAW,aAAa;AAAA;AAE9P,SAAS,qBAAqB,CAAC,OAAgB,OAAO,0BAA0B,SAAS,KAAoB;AAAA,EAC3G,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK;AAAA,IAAG,KAAK,MAAM,MAAM;AAAA,EAClF,MAAM,IAAI;AAAA,EACV,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,SAAO,CAAC,CAAC,sBAAsB,wBAAwB,EAAE,SAAS,GAAG,CAAC,KACzF,CAAC,CAAC,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,OAAK,OAAO,cAAc,CAAC,KAAK,KAAK,KAAK,KAAK,mBAAmB;AAAA,IAAG,KAAK,MAAM,MAAM;AAAA,EACnJ,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,wBAAwB,EAAE,uBAAuB;AAAA;AAGtG,SAAS,qBAAqB,CAAC,GAAwC;AAAA,EACrE,MAAM,IAAI;AAAA,EACV,IAAI,CAAC,KAAK,OAAO,EAAE,OAAO,YAAY,CAAC,EAAE,MAAM,OAAO,EAAE,aAAa,YAAY,CAAC,+BAA+B,KAAK,EAAE,QAAQ,KAAK,OAAO,EAAE,aAAa,aAAa,OAAO,EAAE,cAAc,aAAa,OAAO,EAAE,iBAAiB,YAAY,CAAC,EAAE,aAAa,WAAW,YAAY,KAAK,CAAC,OAAO,cAAc,EAAE,UAAU,KAAK,EAAE,aAAa;AAAA,IAAG,KAAK,iBAAiB,GAAG;AAAA;AAGlX,SAAS,oBAAoB,CAAC,OAAgB,kBAAkC;AAAA,EAC9E,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK;AAAA,IAAG,KAAK,iBAAiB,GAAG;AAAA,EAC1F,MAAM,MAAM;AAAA,EACZ,IAAI,IAAI,WAAW,GAAG;AAAA,IACpB,IAAI,CAAC,MAAM,QAAQ,IAAI,OAAO,KAAK,CAAC,MAAM,QAAQ,IAAI,QAAQ,KAAK,CAAC,MAAM,QAAQ,IAAI,QAAQ;AAAA,MAAG,KAAK,iBAAiB,GAAG;AAAA,IAC1H,MAAM,MAAM,IAAI,KAAe,QAAQ,IAAI;AAAA,IAC3C,WAAW,UAAU,IAAI,SAAS;AAAA,MAChC,sBAAsB,MAAM;AAAA,MAC5B,MAAM,SAAS;AAAA,MACf,IAAI,CAAC,CAAC,SAAS,SAAS,QAAQ,EAAE,SAAS,OAAO,IAAI,KAAK,IAAI,IAAI,OAAO,EAAE,KAAK,MAAM,IAAI,OAAO,QAAQ;AAAA,QAAG,KAAK,iBAAiB,GAAG;AAAA,MACtI,IAAI,IAAI,OAAO,EAAE;AAAA,MAAG,MAAM,IAAI,OAAO,QAAQ;AAAA,IAC/C;AAAA,IACA,MAAM,aAAa,IAAI,QAAQ,OAAO,OAAK,EAAE,SAAS,YAAY,mBAAmB,EAAE,aAAa,mBAAmB,CAAC,EAAE,SAAS;AAAA,IACnI,IAAI,IAAI,QAAQ,UAAU,WAAW,WAAW;AAAA,MAAG,KAAK,mBAAmB,6CAA6C,oCAAoC,GAAG;AAAA,IAC/J,MAAM,SAAS,WAAW;AAAA,IAC1B,MAAM,gBAAgB,SAAS,EAAE,IAAI,OAAO,IAAI,UAAU,OAAO,UAAU,UAAU,OAAO,UAAU,WAAW,OAAO,WAAW,cAAc,OAAO,cAAc,YAAY,OAAO,WAAW,IAAI;AAAA,IACxM,OAAO,EAAC,QAAQ,GAAG,eAAe,UAAU,CAAC,GAAG,UAAU,CAAC,EAAC;AAAA,EAC9D;AAAA,EACA,MAAM,IAAI;AAAA,EACV,IAAI,EAAE,WAAW,KAAK,CAAC,MAAM,QAAQ,EAAE,QAAQ,KAAK,CAAC,MAAM,QAAQ,EAAE,QAAQ;AAAA,IAAG,KAAK,iBAAiB,GAAG;AAAA,EACzG,IAAI,EAAE,kBAAkB;AAAA,IAAM,sBAAsB,EAAE,aAAa;AAAA,EACnE,WAAW,KAAK,EAAE;AAAA,IAAU,IAAI,CAAC,KAAK,CAAC,iBAAiB,KAAK,EAAE,MAAM,KAAK,EAAE,oBAAoB,EAAE,eAAe,MAAM,CAAC,OAAO,cAAc,EAAE,UAAU,KAAK,EAAE,aAAa,KAAK,CAAC,OAAO,SAAS,EAAE,OAAO,KAAK,CAAC,OAAO,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,UAAS,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,MAAG,KAAK,iBAAiB,GAAG;AAAA,EACrT,WAAW,KAAK,EAAE;AAAA,IAAU,IAAI,EAAE,WAAW;AAAA,MAAW,sBAAsB,EAAE,QAAQ,iBAAiB,GAAG;AAAA,EAC5G,WAAW,KAAK,EAAE;AAAA,IAAU,IAAI,CAAC,KAAK,OAAO,EAAE,QAAQ,YAAY,CAAC,OAAO,cAAc,EAAE,KAAK,KAAK,EAAE,QAAQ,KAAK,CAAC,OAAO,SAAS,EAAE,KAAK;AAAA,MAAG,KAAK,iBAAiB,GAAG;AAAA,EACxK,OAAO;AAAA;AAAA;AAGF,MAAM,qBAAkE;AAAA,EAShD;AAAA,EAA2C;AAAA,EAR/D,aAAa;AAAA,EACb,MAAM,CAAC;AAAA,EACP;AAAA,EACQ;AAAA,EACA;AAAA,EACA,WAAW,IAAI;AAAA,EACxB,QAAQ;AAAA,EACR,WAAW;AAAA,EACnB,WAAW,CAAkB,MAA2C,eAA6B,CAAC,GAAG;AAAA,IAA5E;AAAA,IAA2C;AAAA,IACtE,IAAI,CAAC,KAAK;AAAA,MAAc,KAAK,0BAA0B,GAAG;AAAA,IAC1D,KAAK,QAAQ,KAAK;AAAA,IAClB,KAAK,MAAM,aAAa,OAAO,KAAK;AAAA,IACpC,MAAM,SAAS,CAAC,OAAsE,OAAO,MAAgC;AAAA,MAC3H,IAAI;AAAA,QAAE,KAAK,MAAM;AAAA,QAAG,IAAI,EAAE,cAAc;AAAA,UAAS,KAAK,WAAW;AAAA,QAAG,OAAO,MAAM,GAAG,CAAC;AAAA,QAAK,OAAO,GAAG;AAAA,QAAE,OAAO,KAAK,CAAC;AAAA;AAAA;AAAA,IAErH,KAAK,MAAM;AAAA,MACT,EAAE,MAAM,aAAa,SAAS,CAAC,MAAM,GAAG,SAAS,kBAAkB,QAAQ,OAAO,OAAM,MAAK;AAAA,QAAE,KAAK,kBAAkB,GAAG,iBAAiB,IAAI;AAAA,QAAG,MAAM,IAAI,MAAM,KAAK,EAAE,OAAO;AAAA,QAAG,MAAM,GAAG,CAAC,mBAAmB,YAAY,sBAAsB,CAAC;AAAA,QAAG,MAAM,KAAK,eAAe,EAAE,QAAS,IAAI,GAAG,MAAM,KAAK,kBAAkB,GAAG,iBAAiB,IAAI,CAAC;AAAA,QAAG,OAAO,KAAK,EAAE,IAAI,KAAK,GAAG,KAAK,EAAE,cAAc,OAAO,EAAE,SAAS,IAAI,GAAG,KAAK,KAAK,gBAAgB,EAAE,CAAC;AAAA,OAAI,EAAE;AAAA,MACrc,EAAE,MAAM,WAAW,SAAS,CAAC,MAAM,GAAG,SAAS,UAAU,QAAQ,OAAO,OAAK;AAAA,QAAE,KAAK,kBAAkB,GAAG,eAAe,IAAI;AAAA,QAAG,OAAO,KAAK,OAAO,EAAE,OAAO;AAAA,OAAI,EAAE;AAAA,MACjK,EAAE,MAAM,SAAS,SAAS,CAAC,KAAK,GAAG,SAAS,QAAQ,QAAQ,OAAO,OAAK;AAAA,QAAE,KAAK,kBAAkB,GAAG,eAAe;AAAA,QAAG,MAAM,IAAI,KAAK,KAAK,EAAE,MAAM;AAAA,QAAgB,MAAM,QAAQ,YAAY,EAAE,OAAO;AAAA,QAAG,OAAO,KAAK,EAAE,eAAe,IAAI,CAAC,MAAO,OAAO,cAAc,WAAW,EAAE,WAAW,KAAK,MAAM,KAAK,EAAE,IAAI,CAAC,EAAG,CAAC;AAAA,OAAI,EAAE;AAAA,MAC/T,EAAE,MAAM,mBAAmB,SAAS,CAAC,OAAO,KAAK,GAAG,SAAS,iBAAiB,QAAQ,OAAO,OAAM,MAAK;AAAA,QACtG,KAAK,kBAAkB,GAAG,gBAAgB,EAAE,QAAQ,WAAW,KAAK;AAAA,QACpE,IAAI,EAAE,QAAQ,WAAW;AAAA,UAAO,OAAO,KAAK,KAAK,kBAAkB,CAAC;AAAA,QACpE,MAAM,IAAI,MAAM,KAAK,EAAE,OAAO;AAAA,QAAG,MAAM,GAAG,CAAC,WAAW,QAAQ,CAAC;AAAA,QAC/D,IAAI,CAAC,OAAO,cAAc,EAAE,OAAO,KAAM,EAAE,UAAqB;AAAA,UAAG,KAAK,wBAAwB;AAAA,QAChG,MAAM,SAAS,sBAAsB,EAAE,MAAM;AAAA,QAC7C,KAAK,kBAAkB,GAAG,gBAAgB,IAAI;AAAA,QAC9C,IAAI;AAAA,UAAE,KAAK,MAAM,QAAQ,EAAC,WAAW,WAAW,GAAG,WAAW,CAAC,EAAC,KAAK,kBAAkB,iBAAiB,EAAE,SAAmB,OAAO,OAAgC,CAAC,EAAC,CAAC;AAAA,UACvK,OAAO,GAAG;AAAA,UAAE,IAAI,4BAA4B,CAAC;AAAA,YAAG,KAAK,oBAAoB,GAAG;AAAA,UAAG,MAAM;AAAA;AAAA,QACrF,OAAO,KAAK,KAAK,kBAAkB,CAAC;AAAA,OACrC,EAAE;AAAA,IACL;AAAA;AAAA,EAEM,KAAK,GAAG;AAAA,IAAE,IAAI,KAAK,YAAY,KAAK,KAAK,OAAO;AAAA,MAAS,KAAK,wBAAwB,GAAG;AAAA;AAAA,EACzF,IAAI,GAAsC;AAAA,IAChD,KAAK,MAAM;AAAA,IAAG,MAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AAAA,IACtD,IAAI,CAAC;AAAA,MAAQ,OAAO,EAAE,SAAS,GAAG,OAAO,EAAE,QAAQ,GAAG,eAAe,MAAM,UAAU,CAAC,GAAG,UAAU,CAAC,EAAE,EAAE;AAAA,IACxG,MAAM,IAAI,qBAAqB,OAAO,KAAK;AAAA,IAC3C,OAAO,EAAE,SAAS,OAAO,SAAS,OAAO,EAAE;AAAA;AAAA,EAErC,MAAS,CAAC,IAAwB;AAAA,IACxC,SAAS,IAAI,EAAG,IAAI,GAAG,KAAK;AAAA,MAAE,QAAO,SAAS,UAAS,KAAK,KAAK;AAAA,MAAG,MAAM,SAAS,GAAG,KAAK;AAAA,MACzF,IAAI;AAAA,QAAE,KAAK,MAAM,QAAQ,EAAE,WAAW,WAAW,GAAG,WAAW,CAAC,EAAE,KAAK,YAAY,iBAAiB,SAAS,MAAuC,CAAC,EAAE,CAAC;AAAA,QAAG,OAAO;AAAA,QAClK,OAAO,GAAG;AAAA,QAAE,IAAI,CAAC,4BAA4B,CAAC;AAAA,UAAG,MAAM;AAAA;AAAA,IACzD;AAAA,IAAE,OAAO,KAAK,oBAAoB,GAAG;AAAA;AAAA,EAE/B,iBAAiB,GAA6C;AAAA,IACpE,KAAK,MAAM;AAAA,IAAG,MAAM,SAAS,KAAK,MAAM,IAAI,gBAAgB;AAAA,IAC5D,OAAO,EAAC,SAAS,QAAQ,WAAW,GAAG,QAAQ,SAAS,sBAAsB,OAAO,OAAO,iBAAiB,GAAG,IAAI,KAAI,uBAAsB,EAAC;AAAA;AAAA,OAE3I,MAAK,GAAG;AAAA,IAAE,MAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AAAA,IAAG,KAAK,KAAK;AAAA,IAAG,KAAK,kBAAkB;AAAA,IAAG,IAAI,UAAW,OAAO,MAAc,WAAW;AAAA,MAAG,KAAK,OAAO,MAAG;AAAA,QAAG;AAAA,OAAS;AAAA,IAAG,KAAK,QAAQ,MAAM,KAAK,KAAK,YAAY,EAAE,EAAE,SAAS,WAAW,CAAC;AAAA;AAAA,EACpP,OAAO,GAAG;AAAA,IAAE,KAAK,WAAW;AAAA;AAAA,EACpB,IAAI,CAAC,GAAW;AAAA,IAAE,OAAO,IAAI,SAAS,KAAK,GAAG,EAAE,WAAW,YAAY,YAAY,OAAO,UAAU,EAAE,CAAC;AAAA;AAAA,EAC/G,WAAW,GAAG;AAAA,IAAE,MAAM,IAAI,KAAK,KAAK,EAAE,MAAM;AAAA,IAAe,OAAO,CAAC,CAAC,KAAK,CAAC,EAAE;AAAA;AAAA,EAC5E,cAAc,GAAG;AAAA,IAAE,KAAK,OAAO,OAAK;AAAA,MAAE,EAAE,WAAW,CAAC;AAAA,KAAI;AAAA;AAAA,OAClD,UAAS,CAAC,OAAgB;AAAA,IAC9B,MAAM,IAAI,OAAO,KAAK;AAAA,IAAG,MAAM,GAAG,CAAC,YAAY,YAAY,sBAAsB,CAAC;AAAA,IAAG,MAAM,OAAO,SAAS,EAAE,QAAQ,GAAG,IAAI,SAAS,EAAE,QAAQ;AAAA,IAC/I,IAAI,EAAE,yBAAyB;AAAA,MAAG,KAAK,uBAAuB;AAAA,IAC9D,MAAM,QAAQ,KAAK,KAAK,EAAE,MAAM;AAAA,IAChC,IAAI,OAAO;AAAA,MACT,IAAI,MAAM,aAAa,QAAQ,MAAM,YAAY,CAAC,MAAM,IAAI,SAAS,OAAO,GAAG,MAAM,YAAY;AAAA,QAAG,KAAK,uBAAuB,GAAG;AAAA,MACnI,MAAM,SAAS,KAAK,KAAK,EAAE,MAAM;AAAA,MACjC,IAAI,CAAC,UAAU,OAAO,eAAe,MAAM,cAAc,OAAO;AAAA,QAAU,KAAK,uBAAuB,GAAG;AAAA,MACzG;AAAA,IACF;AAAA,IACA,MAAM,SAAS,MAAM,KAAK,KAAK,CAAC;AAAA,IAChC,KAAK,OAAO,OAAK;AAAA,MAAE,IAAI,EAAE;AAAA,QAAe,KAAK,sBAAsB,GAAG;AAAA,MAAG,EAAE,gBAAgB,EAAE,IAAI,WAAW,GAAG,UAAU,MAAM,UAAU,OAAO,WAAW,OAAO,cAAc,QAAQ,YAAY,EAAE;AAAA,KAAI;AAAA;AAAA,EAEpM,IAAI,CAAC,GAAU,SAA6C;AAAA,IAClE,MAAM,MAAM,KAAK,IAAI,GAAG,SAAS,QAAQ,UAAU;AAAA,IACnD,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,WACrC,OAAO,qBAAqB,KAAK,MAAM,QAAQ,WAAW,OAAO,qBAAqB,SACtF,OAAO,yBAAyB,KAAK,MAAM,QAAQ,WAAW,OAAO,yBAAyB;AAAA,MAAS;AAAA,IAC7G,MAAM,IAAI,EAAE;AAAA,IAAe,OAAO,KAAK,EAAE,OAAO,QAAQ,mBAAmB,CAAC,EAAE,YAAY,EAAE,eAAe,QAAQ,aAAa,IAAI;AAAA;AAAA,EAE9H,SAAS,CAAC,QAAuB,YAAY,GAAW;AAAA,IAC9D,OAAO,OAAO,2BAA2B,IAAI,uBACzC,KAAK,IAAI,GAAG,KAAK,IAAI,sBAAsB,KAAK,OAAO,OAAO,yBAAyB,QAAS,aAAa,IAAI,CAAC,CAAC;AAAA;AAAA,EAEzH,aAAa,CAAC,SAAsC;AAAA,IAClD,KAAK,MAAM;AAAA,IAAG,MAAM,QAAQ,YAAY,OAAO;AAAA,IAAG,IAAI,OAAO,cAAc;AAAA,MAAU;AAAA,IACrF,MAAM,IAAI,KAAK,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,KAAK,OAAK,EAAE,WAAW,OAAO,MAAM,KAAK,KAAK,EAAE,cAAc,QAAQ;AAAA,IACxH,IAAI,CAAC,WAAW,CAAC,KAAK,KAAK,GAAG,OAAO;AAAA,MAAG;AAAA,IACxC,OAAO,OAAO,SAAS,MAAM,OAAO,KAAK,UAAU,QAAQ,UAAU,wBAAwB,KAAK,IAAI,IAAI,QAAQ,OAAO,GAAG,KAAK,KAAK,gBAAgB;AAAA;AAAA,OAElJ,aAAY,CAAC,SAAqD;AAAA,IACtE,KAAK,MAAM;AAAA,IAAG,MAAM,IAAI,YAAY,OAAO;AAAA,IAAG,IAAI,CAAC;AAAA,MAAG,OAAO;AAAA,IAC7D,OAAO,KAAK,OAAO,OAAK;AAAA,MAAE,MAAM,UAAU,EAAE,SAAS,KAAK,OAAK,EAAE,WAAW,OAAO,EAAE,KAAK,KAAK,EAAE,cAAc,EAAE,SAAS;AAAA,MAAG,MAAM,gBAAgB,WAAW,KAAK,KAAK,GAAG,OAAO;AAAA,MAAG,IAAI,CAAC,WAAW,CAAC;AAAA,QAAe,OAAO;AAAA,MAAM,QAAQ,UAAU,KAAK,IAAI;AAAA,MAAG,MAAM,UAAU,OAAO,OAAO,EAAE,IAAI,cAAc,IAAI,UAAU,UAAU,cAAc,OAAO,OAAO,CAAC,GAAG,GAAG,CAAC,GAAG,wBAAwB,cAAc,UAAU,CAAC;AAAA,MAAG,KAAK,SAAS,IAAI,SAAS,EAAE,YAAY,cAAc,YAAY,QAAQ,QAAQ,OAAO,CAAC;AAAA,MAAG,OAAO;AAAA,KAAU;AAAA;AAAA,EAEthB,SAAS,CAAC,SAA4B,aAA8B;AAAA,IAClE,KAAK,MAAM;AAAA,IAAG,MAAM,SAAS,KAAK,SAAS,IAAI,OAAO;AAAA,IAAG,IAAI,CAAC,UAAU,QAAQ,aAAa;AAAA,MAAU,OAAO;AAAA,IAC9G,MAAM,IAAI,KAAK,KAAK,EAAE,OAAO,UAAU,EAAE,SAAS,KAAK,OAAK,EAAE,WAAW,OAAO,MAAM,GAAG,IAAI,WAAW,KAAK,KAAK,GAAG,OAAO;AAAA,IAC5H,OAAO,CAAC,CAAC,KAAK,EAAE,OAAO,QAAQ,MAAM,EAAE,eAAe,OAAO;AAAA;AAAA,EAEvD,iBAAiB,CAAC,GAA6B,YAAoB,QAAQ,OAAO;AAAA,IAAE,IAAI,CAAC,EAAE,WAAW,CAAC,KAAK,UAAU,EAAE,SAAS,UAAU;AAAA,MAAG,KAAK,aAAa,GAAG;AAAA,IAAG,IAAI;AAAA,MAAO,KAAK,WAAW,EAAE,OAAO;AAAA;AAAA,EAClN,aAAa,CAAC,SAAkB;AAAA,IAAE,KAAK,WAAW,OAAO;AAAA;AAAA,EACzD,SAAS,CAAC,SAAsC;AAAA,IAAE,MAAM,IAAI,YAAY,OAAO;AAAA,IAAG,OAAO,GAAG,cAAc,WAAW,KAAK,EAAE,KAAK,IAAI;AAAA;AAAA,EAC7H,UAAU,CAAC,SAAkB;AAAA,IAAE,MAAM,IAAI,YAAY,OAAO;AAAA,IAAG,IAAI,CAAC;AAAA,MAAG,KAAK,gBAAgB,GAAG;AAAA,IAAG,IAAI,EAAE,cAAc,UAAU;AAAA,MAAE,OAAO,SAAS,KAAK,KAAK,gBAAgB;AAAA,MAAG,IAAI,CAAC,SAAS,QAAQ,QAAQ,IAAI,cAAc,KAAK,IAAI,KAAK,EAAE,KAAK,CAAC;AAAA,QAAG,KAAK,gBAAgB,GAAG;AAAA,IAAG;AAAA;AAAA,OACrR,MAAK,CAAC,SAAqC;AAAA,IAC/C,IAAI;AAAA,MACF,KAAK,MAAM;AAAA,MAAG,IAAI,QAAQ,WAAW;AAAA,QAAQ,KAAK,sBAAsB,GAAG;AAAA,MAC3E,MAAM,IAAI,MAAM,KAAK,OAAO;AAAA,MAAG,MAAM,GAAG,CAAC,YAAW,YAAW,WAAW,CAAC;AAAA,MAC3E,MAAM,OAAO,SAAS,EAAE,QAAQ;AAAA,MAAG,MAAM,IAAI,OAAO,EAAE,aAAa,WAAW,EAAE,WAAW;AAAA,MAC3F,MAAM,YAAY,EAAE,aAAa;AAAA,MAAU,IAAI,cAAc,YAAY,cAAc;AAAA,QAAU,KAAK,eAAe;AAAA,MACrH,IAAI,cAAc;AAAA,QAAU,OAAO,SAAS,KAAK,KAAK,gBAAgB;AAAA,MAAQ,SAAI,QAAQ,QAAQ,IAAI,QAAQ;AAAA,QAAG,OAAO,SAAS,KAAK,KAAK,gBAAgB;AAAA,MAC3J,MAAM,UAAU,KAAK,aAAa,iBAAiB,KAAK,KAAK,iBAAiB,OAAO,KAAK;AAAA,MAC1F,MAAM,OAAO,CAAC,aAAa,OAAO,IAAI,GAAG,YAAY,OAAO,MAAM,CAAC;AAAA,MAAG,MAAM,MAAM,KAAK,IAAI;AAAA,MAE3F,KAAK,OAAO,OAAK;AAAA,QAAE,EAAE,WAAW,EAAE,SAAS,OAAO,OAAK,EAAE,QAAQ,GAAG;AAAA,QAAG,IAAI,KAAK,KAAK,OAAK,EAAE,SAAS,KAAK,OAAK,EAAE,QAAQ,KAAK,EAAE,SAAS,EAAE,CAAC;AAAA,UAAG,KAAK,iBAAiB,GAAG;AAAA,QAAG,WAAW,OAAO,MAAM;AAAA,UAAE,IAAI,IAAI,EAAE,SAAS,KAAK,OAAK,EAAE,QAAQ,GAAG;AAAA,UAAG,IAAI,CAAC,GAAG;AAAA,YAAE,IAAI,EAAE,KAAK,OAAO,GAAG,OAAO,MAAM,KAAK,MAAO;AAAA,YAAG,EAAE,SAAS,KAAK,CAAC;AAAA,UAAG;AAAA,UAAE,EAAE;AAAA,QAAS;AAAA,QAAE,IAAI,EAAE,SAAS,SAAS;AAAA,UAAM,KAAK,iBAAiB,GAAG;AAAA,OAAI;AAAA,MAC3Y,MAAM,YAAY,KAAK,KAAK,EAAE,MAAM;AAAA,MAAe,MAAM,gBAAgB,WAAW,aAAa,OAAO,YAAY;AAAA,MACpH,MAAM,QAAQ,MAAM,IAAI,SAAS,OAAO,GAAG,eAAe,iBAAiB,KAAK,SAAS,MAAM,KAAK,KAAK,wBAAwB,EAAE;AAAA,MACnI,IAAI,CAAC,SAAS,CAAC,iBAAiB,cAAc;AAAA,QAAU,KAAK,uBAAuB,GAAG;AAAA,MACvF,MAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,WAAW;AAAA,MAClD,MAAM,SAAS,KAAK,kBAAkB,EAAE;AAAA,MACxC,MAAM,UAAU,KAAK,IAAI;AAAA,MACzB,KAAK,OAAO,OAAK;AAAA,QAAE,MAAM,IAAI,EAAE;AAAA,QAAe,IAAI,CAAC,KAAK,EAAE,OAAO,cAAc,MAAM,EAAE,YAAY,EAAE,eAAe,cAAc;AAAA,UAAY,KAAK,uBAAuB,GAAG;AAAA,QAAG,EAAE,WAAW,EAAE,SAAS,OAAO,OAAK,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC;AAAA,QAAG,IAAI,EAAE,SAAS,UAAU;AAAA,UAAK,KAAK,iBAAiB,GAAG;AAAA,QAAG,EAAE,SAAS,KAAK,EAAE,QAAQ,OAAO,KAAK,GAAG,iBAAiB,EAAE,IAAI,YAAY,EAAE,YAAY,SAAS,SAAS,SAAS,WAAW,OAAO,CAAC;AAAA,QAAG,EAAE,WAAW,EAAE,SAAS,OAAO,OAAK,EAAE,QAAQ,KAAK,EAAE;AAAA,QAAG,MAAM,gBAAgB,EAAE,SAAS,KAAK,OAAK,EAAE,QAAQ,KAAK,EAAE;AAAA,QAAG,IAAI;AAAA,UAAe,cAAc,QAAQ,KAAK,IAAI,GAAG,cAAc,QAAQ,CAAC;AAAA,OAAI;AAAA,MACpnB,OAAO,cAAc,WAAW,KAAK,EAAE,eAAe,IAAI,aAAa,GAAG,WAAW,KAAK,KAAK,EAAE,GAAG,KAAK,EAAE,cAAc,OAAO,SAAS,OAAO,KAAK,UAAU,QAAQ,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,KAAK,gBAAgB,EAAE,CAAC,IAAI,KAAK,EAAE,eAAe,IAAI,aAAa,GAAG,OAAO,WAAW,OAAO,2BAA2B,IAAI,OAAO,OAAO,yBAAyB,GAAG,CAAC;AAAA,MAC5W,OAAO,GAAG;AAAA,MAAE,OAAO,KAAK,CAAC;AAAA;AAAA;AAAA,OAEvB,OAAM,CAAC,SAAqC;AAAA,IAChD,IAAI;AAAA,MAAE,IAAI,QAAQ,WAAW;AAAA,QAAQ,KAAK,sBAAsB,GAAG;AAAA,MAAG,KAAK,WAAW,OAAO;AAAA,MAAG,MAAM,IAAI,YAAY,OAAO;AAAA,MAAI,KAAK,OAAO,OAAK;AAAA,QAAE,EAAE,WAAW,EAAE,SAAS,OAAO,OAAK,EAAE,WAAW,OAAO,EAAE,KAAK,CAAC;AAAA,OAAI;AAAA,MAAG,OAAO,KAAK,EAAE,IAAI,KAAK,GAAG,KAAK,EAAE,cAAc,OAAO,SAAS,IAAI,GAAG,KAAK,KAAK,gBAAgB,EAAE,CAAC;AAAA,MAAK,OAAM,GAAG;AAAA,MAAE,OAAO,KAAK,CAAC;AAAA;AAAA;AAAA,OAE/U,eAAc,CAAC,IAAY,GAA4B,OAAmB;AAAA,IACtF,MAAM,IAAI,SAAS,EAAE,QAAQ;AAAA,IAAG,IAAI,EAAE,yBAAyB;AAAA,MAAG,KAAK,uBAAuB;AAAA,IAAG,MAAM,IAAI,KAAK,KAAK,EAAE,MAAM;AAAA,IAAe,IAAI,CAAC,KAAK,EAAE,OAAO,MAAM,OAAO,EAAE,oBAAoB,YAAY,CAAC,MAAM,IAAI,SAAS,OAAO,EAAE,iBAAiB,EAAE,YAAY;AAAA,MAAG,KAAK,uBAAuB,GAAG;AAAA,IAAG,IAAI,SAAS,GAAG,EAAE,eAAe;AAAA,MAAG,KAAK,oBAAoB;AAAA,IAAG,MAAM,SAAS,MAAM,KAAK,KAAK,CAAC;AAAA,IAC/Y,KAAK,OAAO,OAAK;AAAA,MAAE,MAAM;AAAA,MAAG,MAAM,UAAU,EAAE;AAAA,MAAe,IAAI,CAAC,WAAW,QAAQ,OAAO,MAAM,QAAQ,YAAY,QAAQ,eAAe,EAAE;AAAA,QAAY,KAAK,oBAAoB,GAAG;AAAA,MAAG,IAAI,CAAC,OAAO,cAAc,QAAQ,aAAa,CAAC;AAAA,QAAG,KAAK,qBAAqB;AAAA,MAAG,QAAQ,eAAe;AAAA,MAAQ,QAAQ,YAAY;AAAA,MAAO,QAAQ;AAAA,MAAc,EAAE,WAAW,CAAC;AAAA,KAAI;AAAA;AAEhX;AACO,SAAS,aAAa,CAAC,MAA0B,eAA6B,CAAC,GAAyB;AAAA,EAAE,OAAO,IAAI,qBAAqB,MAAM,YAAY;AAAA;AAC5J,IAAM,MAAM;AAAA,EACjB,EAAE,MAAM,aAAa,SAAS,CAAC,MAAM,GAAG,SAAS,iBAAiB;AAAA,EAClE,EAAE,MAAM,WAAW,SAAS,CAAC,MAAM,GAAG,SAAS,SAAS;AAAA,EACxD,EAAE,MAAM,SAAS,SAAS,CAAC,KAAK,GAAG,SAAS,OAAO;AAAA,EACnD,EAAE,MAAM,mBAAmB,SAAS,CAAC,OAAO,KAAK,GAAG,SAAS,gBAAgB;AAC/E;AACO,IAAM,MAAM,CAAC;AACb,IAAM,aAAa;AAAnB,IAAwB,aAAa;AAC5C,eAAsB,eAAe,CAAC,OAAgB,SAA+C;AAAA,EACnG,MAAM,IAAI,OAAO,KAAK;AAAA,EAAG,MAAM,GAAG,CAAC,YAAY,eAAe,YAAY,QAAQ,CAAC;AAAA,EACnF,MAAM,OAAO,EAAE,aAAa,YAAY,YAAY,SAAS,EAAE,QAAQ,GAAG,IAAI,SAAS,EAAE,QAAQ;AAAA,EACjG,MAAM,UAAU,EAAE,gBAAgB,YAAY,YAAY,SAAS,EAAE,WAAW;AAAA,EAChF,IAAI,OAAO,EAAE,WAAW,YAAY,EAAE,OAAO,KAAK,EAAE,SAAS,KAAK,EAAE,OAAO,SAAS;AAAA,IAAK,KAAK,gBAAgB;AAAA,EAC9G,MAAM,QAAQ,QAAQ,cAAc,SAAS,MAAM,IAAI,UAAU;AAAA,EACjE,MAAM,IAAI,SAAS,qBAAqB,OAAO,OAAO,IAAI,IAAI,EAAE,QAAQ,GAAY,eAAe,MAAM,UAAU,CAAC,GAAG,UAAU,CAAC,EAAE;AAAA,EACpI,IAAI,gBAAgB,EAAE;AAAA,EACtB,IAAI,iBAAiB,SAAS,aAAa,cAAc,aAAa;AAAA,IAAM,KAAK,0CAA0C;AAAA,EAC3H,MAAM,eAAe,MAAM,IAAI,SAAS,KAAK,GAAG,EAAE,WAAW,YAAY,YAAY,OAAO,UAAU,EAAE,CAAC;AAAA,EACzG,IAAI,CAAC,eAAe;AAAA,IAAE,gBAAgB,EAAE,IAAI,WAAW,GAAG,UAAU,WAAW,QAAQ,KAAK,eAAe,GAAG,UAAU,OAAO,WAAW,OAAO,cAAc,YAAY,EAAE;AAAA,IAAG,EAAE,gBAAgB;AAAA,EAAe,EAC5M;AAAA,IAAE,IAAI,CAAC,OAAO,cAAc,cAAc,aAAa,CAAC;AAAA,MAAG,KAAK,qBAAqB;AAAA,IAAG,cAAc,WAAW,WAAW,cAAc;AAAA,IAAU,cAAc,eAAe;AAAA,IAAc,cAAc,WAAW;AAAA,IAAO,cAAc,YAAY;AAAA,IAAO,cAAc;AAAA;AAAA,EACnR,EAAE,WAAW,CAAC;AAAA,EACd,EAAE,WAAW,CAAC;AAAA,EACd,MAAM,QAAQ,EAAC,WAAW,WAAW,GAAG,WAAW;AAAA,IACjD,EAAC,KAAK,YAAY,iBAAiB,QAAQ,WAAW,GAAG,OAAO,EAA2B;AAAA,IAC3F,EAAC,KAAK,cAAc,WAAW,GAAG,iBAAiB,GAAG,OAAO,EAAC,iBAAiB,cAAc,IAAI,QAAQ,EAAE,QAAQ,IAAI,KAAK,IAAI,EAAC,EAAC;AAAA,EACpI,EAAC,CAAC;AAAA,EACF,OAAO,IAAI,aAAa;AAAA;AAEnB,SAAS,mBAAmB,CAAC,OAA8C;AAAA,EAChF,MAAM,SAAS,MAAM,IAAI,UAAU;AAAA,EACnC,OAAO,EAAC,aAAY,SAAS,CAAC,CAAC,qBAAqB,OAAO,KAAK,EAAE,gBAAgB,MAAK;AAAA;AAEzF,IAAe,oBAAE,eAAe,qBAAqB,iBAAiB,EAAC,MAAM,YAAY,SAAS,gBAAe,EAAE;",
|
|
9
|
+
"debugId": "9FABE2F2EC4EEDD964756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -47,6 +47,16 @@
|
|
|
47
47
|
"execution": "control",
|
|
48
48
|
"capability": "self.password"
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
"path": "/session-policy",
|
|
52
|
+
"methods": [
|
|
53
|
+
"GET",
|
|
54
|
+
"PUT"
|
|
55
|
+
],
|
|
56
|
+
"handler": "sessionPolicy",
|
|
57
|
+
"execution": "control",
|
|
58
|
+
"capability": "config.write"
|
|
59
|
+
},
|
|
50
60
|
{
|
|
51
61
|
"path": "/logout",
|
|
52
62
|
"methods": [
|
|
@@ -120,7 +130,25 @@
|
|
|
120
130
|
"settings.confirmation": "确认新密码",
|
|
121
131
|
"settings.updating": "正在更新…",
|
|
122
132
|
"settings.submit": "修改并重新登录",
|
|
123
|
-
"settings.logout": "退出当前会话"
|
|
133
|
+
"settings.logout": "退出当前会话",
|
|
134
|
+
"session.title": "登录会话",
|
|
135
|
+
"session.tag": "有效期",
|
|
136
|
+
"session.description": "保存后仅影响新登录的会话,已有会话保留登录时的设置。时长设为 0 可关闭对应超时。",
|
|
137
|
+
"session.idle": "空闲超时",
|
|
138
|
+
"session.absolute": "登录最长有效期",
|
|
139
|
+
"session.minutes": "分钟",
|
|
140
|
+
"session.hours": "小时",
|
|
141
|
+
"session.days": "天",
|
|
142
|
+
"session.unit": "时长单位",
|
|
143
|
+
"session.browserNote": "关闭超时不影响退出登录、改密和身份恢复时撤销会话。浏览器仍可能清理 Cookie;正常核验会话时会续期 Cookie,但不会延长登录最长有效期。",
|
|
144
|
+
"session.loading": "正在读取会话设置",
|
|
145
|
+
"session.save": "保存会话设置",
|
|
146
|
+
"session.saving": "正在保存…",
|
|
147
|
+
"session.saved": "已保存,下次登录生效。",
|
|
148
|
+
"session.invalid": "请输入非负整数时长;0 表示关闭超时。",
|
|
149
|
+
"session.conflict": "设置已被其他页面修改。请重新读取后再保存。",
|
|
150
|
+
"session.failed": "会话设置读取或保存失败。",
|
|
151
|
+
"session.reload": "重新读取"
|
|
124
152
|
},
|
|
125
153
|
"en": {
|
|
126
154
|
"metadata.name": "Management authentication",
|
|
@@ -165,7 +193,25 @@
|
|
|
165
193
|
"settings.confirmation": "Confirm new password",
|
|
166
194
|
"settings.updating": "Updating…",
|
|
167
195
|
"settings.submit": "Change and sign in again",
|
|
168
|
-
"settings.logout": "Sign out of current session"
|
|
196
|
+
"settings.logout": "Sign out of current session",
|
|
197
|
+
"session.title": "Sign-in sessions",
|
|
198
|
+
"session.tag": "LIFETIME",
|
|
199
|
+
"session.description": "Saved settings apply only to new sign-ins. Existing sessions retain their original settings. Set a duration to 0 to disable that timeout.",
|
|
200
|
+
"session.idle": "Idle timeout",
|
|
201
|
+
"session.absolute": "Maximum sign-in lifetime",
|
|
202
|
+
"session.minutes": "Minutes",
|
|
203
|
+
"session.hours": "Hours",
|
|
204
|
+
"session.days": "Days",
|
|
205
|
+
"session.unit": "Duration unit",
|
|
206
|
+
"session.browserNote": "Disabling timeouts does not prevent sign-out, password changes or identity recovery from revoking sessions. Browsers may still clear cookies. Session verification renews the cookie without extending the maximum sign-in lifetime.",
|
|
207
|
+
"session.loading": "Loading session settings",
|
|
208
|
+
"session.save": "Save session settings",
|
|
209
|
+
"session.saving": "Saving…",
|
|
210
|
+
"session.saved": "Saved. Applies to your next sign-in.",
|
|
211
|
+
"session.invalid": "Enter a non-negative whole-number duration. 0 disables the timeout.",
|
|
212
|
+
"session.conflict": "Another page changed these settings. Reload before saving.",
|
|
213
|
+
"session.failed": "Could not read or save session settings.",
|
|
214
|
+
"session.reload": "Reload settings"
|
|
169
215
|
}
|
|
170
216
|
}
|
|
171
217
|
}
|
|
@@ -4,15 +4,62 @@
|
|
|
4
4
|
import { onMount } from 'svelte';
|
|
5
5
|
import { requestPluginControl } from '$api/client';
|
|
6
6
|
import { logout } from '$stores/auth';
|
|
7
|
-
import { PanelCard, LoadingIndicator } from '$components/industrial';
|
|
7
|
+
import { PanelCard, LoadingIndicator, BSelect } from '$components/industrial';
|
|
8
|
+
import { Input } from '$components/ui/input';
|
|
9
|
+
import { Button } from '$components/ui/button';
|
|
8
10
|
import { accountError } from '$components/domain/plugin/activation-state';
|
|
9
11
|
const t = (key: string) => $isLoading ? '' : getPluginText(key, 'local-accounts', (id, options) => $_(id, options));
|
|
10
12
|
const core = (key: string, values: Record<string, string> = {}) => $isLoading ? '' : $_(key, { values });
|
|
11
13
|
let { pluginName = 'local-accounts' }: { pluginName?: string; apiBase?: string } = $props();
|
|
12
14
|
let administrator = $state<{id:string;username:string;requiresPasswordChange?:boolean} | null>(null);
|
|
13
15
|
let currentPassword = $state(''), password = $state(''), confirmation = $state(''), error = $state(''), detail = $state(''), busy = $state(false), loading = $state(true);
|
|
16
|
+
type SessionPolicy = {idleTimeoutMinutes: number; absoluteTimeoutMinutes: number};
|
|
17
|
+
type PolicyResult = {version: number; policy: SessionPolicy};
|
|
18
|
+
const factors: Record<string, number> = {minutes: 1, hours: 60, days: 1440};
|
|
19
|
+
const maxMinutes = Math.floor(Number.MAX_SAFE_INTEGER / 60_000);
|
|
20
|
+
let durations = $state<{key: keyof SessionPolicy; label: string; value: number | undefined; unit: string}[]>([
|
|
21
|
+
{key: 'idleTimeoutMinutes', label: 'session.idle', value: 30, unit: 'minutes'},
|
|
22
|
+
{key: 'absoluteTimeoutMinutes', label: 'session.absolute', value: 8, unit: 'hours'},
|
|
23
|
+
]);
|
|
24
|
+
let policyVersion = $state<number | null>(null), savedPolicy = $state<SessionPolicy | null>(null);
|
|
25
|
+
let policyLoading = $state(true), policyBusy = $state(false), policyError = $state(''), policyDetail = $state(''), policySaved = $state(false);
|
|
26
|
+
let units = $derived(['minutes', 'hours', 'days'].map(unit => ({value: unit, label: t('session.' + unit)})));
|
|
27
|
+
let draftPolicy = $derived(Object.fromEntries(durations.map(field => [field.key, field.value === undefined ? NaN : field.value * factors[field.unit]!])) as SessionPolicy);
|
|
28
|
+
let validPolicy = $derived(durations.every(field => Number.isSafeInteger(field.value) && field.value! >= 0)
|
|
29
|
+
&& Object.values(draftPolicy).every(value => Number.isSafeInteger(value) && value >= 0 && value <= maxMinutes));
|
|
30
|
+
let policyDirty = $derived(savedPolicy !== null && (draftPolicy.idleTimeoutMinutes !== savedPolicy.idleTimeoutMinutes || draftPolicy.absoluteTimeoutMinutes !== savedPolicy.absoluteTimeoutMinutes));
|
|
31
|
+
function applyPolicy(result: PolicyResult) {
|
|
32
|
+
policyVersion = result.version; savedPolicy = result.policy;
|
|
33
|
+
for (const field of durations) {
|
|
34
|
+
const minutes = result.policy[field.key];
|
|
35
|
+
field.unit = minutes > 0 && minutes % 1440 === 0 ? 'days' : minutes > 0 && minutes % 60 === 0 ? 'hours' : 'minutes';
|
|
36
|
+
field.value = minutes / factors[field.unit]!;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function policyFailure(e: unknown) {
|
|
40
|
+
const result = accountError(e); policyDetail = result.detail;
|
|
41
|
+
policyError = result.detail === 'version_conflict' ? 'session.conflict' : result.detail === 'invalid_session_policy' ? 'session.invalid' : 'session.failed';
|
|
42
|
+
}
|
|
43
|
+
async function loadPolicy() {
|
|
44
|
+
if (policyBusy) return;
|
|
45
|
+
policyLoading = true; policyError = ''; policyDetail = ''; policySaved = false;
|
|
46
|
+
try { applyPolicy(await requestPluginControl<PolicyResult>(pluginName, '/session-policy', 'GET')); }
|
|
47
|
+
catch (e) { policyFailure(e); }
|
|
48
|
+
finally { policyLoading = false; }
|
|
49
|
+
}
|
|
50
|
+
async function savePolicy(event: SubmitEvent) {
|
|
51
|
+
event.preventDefault(); if (policyBusy || policyLoading || policyVersion === null) return;
|
|
52
|
+
if (!validPolicy) { policyError = 'session.invalid'; return; }
|
|
53
|
+
policyBusy = true; policyError = ''; policyDetail = ''; policySaved = false;
|
|
54
|
+
try {
|
|
55
|
+
applyPolicy(await requestPluginControl<PolicyResult>(pluginName, '/session-policy', 'PUT', {version: policyVersion, policy: draftPolicy}));
|
|
56
|
+
policySaved = true;
|
|
57
|
+
} catch (e) { policyFailure(e); }
|
|
58
|
+
finally { policyBusy = false; }
|
|
59
|
+
}
|
|
14
60
|
function failure(e: unknown) { const result = accountError(e); error = result.key; detail = result.detail; }
|
|
15
61
|
onMount(() => { void requestPluginControl<{administrator: typeof administrator}>(pluginName, '/self', 'GET').then(result => { administrator = result.administrator; }).catch(failure).finally(() => { loading = false; }); });
|
|
62
|
+
onMount(() => { void loadPolicy(); });
|
|
16
63
|
async function change(event: SubmitEvent) {
|
|
17
64
|
event.preventDefault(); if (busy) return;
|
|
18
65
|
if (password !== confirmation) { failure(new Error('password_confirmation')); return; }
|
|
@@ -32,6 +79,31 @@
|
|
|
32
79
|
<PanelCard title={t('settings.accountTitle')} tag={t('settings.accountTag')}>
|
|
33
80
|
{#if loading}<LoadingIndicator label={t('settings.loading')} height="sm" />{:else if administrator}<p class="text-sm text-zinc-200">{administrator.username}</p><p class="text-sm text-zinc-400 mt-2">{t('settings.accountDescription')}</p>{/if}
|
|
34
81
|
</PanelCard>
|
|
82
|
+
<PanelCard title={t('session.title')} tag={t('session.tag')}>
|
|
83
|
+
<p class="text-sm text-zinc-400 mb-4">{t('session.description')}</p>
|
|
84
|
+
{#if policyLoading}<LoadingIndicator label={t('session.loading')} height="sm" />{:else}
|
|
85
|
+
<form class="space-y-3" onsubmit={savePolicy} aria-busy={policyBusy} data-testid="session-policy-form">
|
|
86
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
87
|
+
{#each durations as field (field.key)}
|
|
88
|
+
<div class="grid gap-1 min-w-0">
|
|
89
|
+
<label class="nx-field-label" for={'session-' + field.key}>{t(field.label)}</label>
|
|
90
|
+
<div class="flex items-center gap-2">
|
|
91
|
+
<Input id={'session-' + field.key} type="number" min={0} max={Math.floor(maxMinutes / factors[field.unit]!)} step={1} required bind:value={field.value} disabled={policyBusy || policyVersion === null} class="min-w-0 flex-1" />
|
|
92
|
+
<BSelect bind:value={field.unit} options={units} ariaLabel={t(field.label) + ' · ' + t('session.unit')} width="6rem" disabled={policyBusy || policyVersion === null} />
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
{/each}
|
|
96
|
+
</div>
|
|
97
|
+
<p class="text-sm text-zinc-400">{t('session.browserNote')}</p>
|
|
98
|
+
<div class="flex flex-wrap gap-2">
|
|
99
|
+
<Button type="submit" disabled={policyBusy || policyVersion === null || !policyDirty || !validPolicy}>{policyBusy ? t('session.saving') : t('session.save')}</Button>
|
|
100
|
+
{#if policyError}<Button type="button" variant="ghost" onclick={loadPolicy} disabled={policyBusy}>{t('session.reload')}</Button>{/if}
|
|
101
|
+
</div>
|
|
102
|
+
{#if policySaved && !policyDirty}<p role="status" class="text-sm text-emerald-400">{t('session.saved')}</p>{/if}
|
|
103
|
+
</form>
|
|
104
|
+
{/if}
|
|
105
|
+
{#if policyError}<p role="alert" class="text-sm text-red-400 mt-3">{t(policyError)}</p><details class="text-sm text-zinc-400"><summary>{core('pluginActivation.technicalDetails')}</summary><pre class="whitespace-pre-wrap break-all">{policyDetail}</pre></details>{/if}
|
|
106
|
+
</PanelCard>
|
|
35
107
|
<PanelCard title={t('settings.passwordTitle')} tag={t('settings.passwordTag')}>
|
|
36
108
|
<p class="text-sm text-zinc-400 mb-4">{t('settings.passwordDescription')}</p>
|
|
37
109
|
<form class="space-y-3" onsubmit={change} aria-busy={busy}>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffusion/bungee-core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"description": "Bungee reverse proxy core engine",
|
|
5
5
|
"main": "./dist/main.js",
|
|
6
6
|
"types": "./dist/main.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test": "bun test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@jeffusion/bungee-types": "^5.
|
|
15
|
+
"@jeffusion/bungee-types": "^5.10.0",
|
|
16
16
|
"@types/lodash-es": "^4.17.12",
|
|
17
17
|
"acorn": "^8.15.0",
|
|
18
18
|
"canonicalize": "3.0.0",
|