@lazyingart/agent-web 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/docs/architecture.md +503 -0
  4. package/package.json +43 -0
  5. package/src/aginti-adapter.js +602 -0
  6. package/src/chat-context.js +1020 -0
  7. package/src/chat-migrations.js +947 -0
  8. package/src/chat-store.js +3308 -0
  9. package/src/cli.js +134 -0
  10. package/src/cloud-server.js +2043 -0
  11. package/src/contracts.js +103 -0
  12. package/src/deterministic-context-summarizer.js +254 -0
  13. package/src/direct-chat-capability-limits.js +66 -0
  14. package/src/direct-chat-contract.js +3 -0
  15. package/src/errors.js +50 -0
  16. package/src/http-contract.js +592 -0
  17. package/src/index.js +88 -0
  18. package/src/localllm-connector.js +667 -0
  19. package/src/migrations.js +231 -0
  20. package/src/operator-health.js +184 -0
  21. package/src/password-verifier.js +131 -0
  22. package/src/service-config.js +547 -0
  23. package/src/service.js +408 -0
  24. package/src/sqlite-health.js +83 -0
  25. package/src/storage-path.js +130 -0
  26. package/src/store.js +914 -0
  27. package/src/validation.js +181 -0
  28. package/src/vision-attachment.js +404 -0
  29. package/src/web/aginti-client.js +552 -0
  30. package/src/web/aginti-protocol.js +1146 -0
  31. package/src/web/asset-map.js +462 -0
  32. package/src/web/browser-app.js +6491 -0
  33. package/src/web/cloud-session-client.js +427 -0
  34. package/src/web/direct-chat-client.js +1482 -0
  35. package/src/web/index.js +10 -0
  36. package/src/web/presentation-state.js +107 -0
  37. package/src/web/pwa-assets.js +854 -0
  38. package/src/web/pwa-update-handoff-store.js +179 -0
  39. package/src/web/safe-rendering.js +836 -0
  40. package/src/web/vision-image-client.js +546 -0
  41. package/src/web/vision-image-sanitizer.js +168 -0
  42. package/src/web/web-release.js +28 -0
@@ -0,0 +1,231 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { StorageCorruptionError, UnsupportedSchemaError } from './errors.js';
4
+
5
+ export const SQLITE_APPLICATION_ID = 0x4c415757;
6
+
7
+ const INITIAL_SCHEMA = `
8
+ CREATE TABLE schema_migrations (
9
+ version INTEGER PRIMARY KEY,
10
+ name TEXT NOT NULL UNIQUE,
11
+ checksum TEXT NOT NULL CHECK (
12
+ length(checksum) = 64 AND checksum NOT GLOB '*[^0-9a-f]*'
13
+ ),
14
+ applied_at TEXT NOT NULL
15
+ ) STRICT;
16
+
17
+ CREATE TABLE accounts (
18
+ id TEXT PRIMARY KEY CHECK (length(id) BETWEEN 1 AND 128),
19
+ issuer TEXT NOT NULL CHECK (length(issuer) BETWEEN 1 AND 256),
20
+ subject TEXT NOT NULL CHECK (length(subject) BETWEEN 1 AND 512),
21
+ display_name TEXT CHECK (display_name IS NULL OR length(display_name) <= 256),
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL,
24
+ UNIQUE (issuer, subject)
25
+ ) STRICT;
26
+
27
+ CREATE TABLE browser_sessions (
28
+ session_digest TEXT PRIMARY KEY CHECK (
29
+ length(session_digest) = 64 AND session_digest NOT GLOB '*[^0-9a-f]*'
30
+ ),
31
+ account_id TEXT NOT NULL,
32
+ csrf_digest TEXT NOT NULL CHECK (
33
+ length(csrf_digest) = 64 AND csrf_digest NOT GLOB '*[^0-9a-f]*'
34
+ ),
35
+ created_at TEXT NOT NULL,
36
+ expires_at TEXT NOT NULL,
37
+ last_seen_at TEXT NOT NULL,
38
+ revoked_at TEXT,
39
+ FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
40
+ ) STRICT;
41
+
42
+ CREATE INDEX browser_sessions_account_expiry
43
+ ON browser_sessions(account_id, expires_at DESC);
44
+
45
+ CREATE TABLE thread_index (
46
+ thread_id TEXT PRIMARY KEY CHECK (length(thread_id) BETWEEN 1 AND 128),
47
+ account_id TEXT NOT NULL,
48
+ authority TEXT NOT NULL DEFAULT 'aginti' CHECK (authority = 'aginti'),
49
+ title TEXT NOT NULL DEFAULT '' CHECK (length(title) <= 120),
50
+ pinned INTEGER NOT NULL DEFAULT 0 CHECK (pinned IN (0, 1)),
51
+ routing_node_id TEXT CHECK (routing_node_id IS NULL OR length(routing_node_id) BETWEEN 1 AND 128),
52
+ authority_revision INTEGER CHECK (authority_revision IS NULL OR authority_revision >= 0),
53
+ last_run_id TEXT CHECK (last_run_id IS NULL OR length(last_run_id) BETWEEN 1 AND 128),
54
+ created_at TEXT NOT NULL,
55
+ updated_at TEXT NOT NULL,
56
+ last_seen_at TEXT NOT NULL,
57
+ UNIQUE (account_id, thread_id),
58
+ FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
59
+ ) STRICT;
60
+
61
+ CREATE INDEX thread_index_owner_updated
62
+ ON thread_index(account_id, updated_at DESC, thread_id DESC);
63
+
64
+ CREATE TABLE run_cursors (
65
+ run_id TEXT PRIMARY KEY CHECK (length(run_id) BETWEEN 1 AND 128),
66
+ account_id TEXT NOT NULL,
67
+ thread_id TEXT NOT NULL,
68
+ last_seq INTEGER NOT NULL CHECK (last_seq >= 0),
69
+ last_event_hash TEXT,
70
+ created_at TEXT NOT NULL,
71
+ updated_at TEXT NOT NULL,
72
+ CHECK (
73
+ (last_seq = 0 AND last_event_hash IS NULL)
74
+ OR (
75
+ last_seq > 0
76
+ AND length(last_event_hash) = 64
77
+ AND last_event_hash NOT GLOB '*[^0-9a-f]*'
78
+ )
79
+ ),
80
+ UNIQUE (account_id, run_id),
81
+ FOREIGN KEY (account_id, thread_id) REFERENCES thread_index(account_id, thread_id) ON DELETE CASCADE
82
+ ) STRICT;
83
+
84
+ CREATE INDEX run_cursors_owner_thread_updated
85
+ ON run_cursors(account_id, thread_id, updated_at DESC, run_id DESC);
86
+
87
+ CREATE TABLE idempotency_records (
88
+ account_id TEXT NOT NULL,
89
+ operation TEXT NOT NULL CHECK (length(operation) BETWEEN 1 AND 128),
90
+ key_hash TEXT NOT NULL CHECK (
91
+ length(key_hash) = 64 AND key_hash NOT GLOB '*[^0-9a-f]*'
92
+ ),
93
+ request_hash TEXT NOT NULL CHECK (
94
+ length(request_hash) = 64 AND request_hash NOT GLOB '*[^0-9a-f]*'
95
+ ),
96
+ outcome_code TEXT NOT NULL CHECK (outcome_code = 'succeeded'),
97
+ resource_kind TEXT NOT NULL CHECK (
98
+ resource_kind IN ('account', 'browser_session', 'thread_index')
99
+ ),
100
+ resource_id TEXT NOT NULL CHECK (length(resource_id) BETWEEN 1 AND 128),
101
+ result_digest TEXT NOT NULL CHECK (
102
+ length(result_digest) = 64 AND result_digest NOT GLOB '*[^0-9a-f]*'
103
+ ),
104
+ created_at TEXT NOT NULL,
105
+ expires_at TEXT NOT NULL,
106
+ PRIMARY KEY (account_id, operation, key_hash),
107
+ FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
108
+ ) STRICT;
109
+
110
+ CREATE INDEX idempotency_records_expiry
111
+ ON idempotency_records(expires_at);
112
+
113
+ CREATE INDEX idempotency_records_owner_created
114
+ ON idempotency_records(account_id, created_at DESC);
115
+ `;
116
+
117
+ function checksum(sql) {
118
+ return createHash('sha256').update(sql, 'utf8').digest('hex');
119
+ }
120
+
121
+ export const MIGRATIONS = Object.freeze([
122
+ Object.freeze({
123
+ version: 1,
124
+ name: 'cloud_presentation_index',
125
+ sql: INITIAL_SCHEMA,
126
+ checksum: checksum(INITIAL_SCHEMA)
127
+ })
128
+ ]);
129
+
130
+ export const LATEST_SCHEMA_VERSION = MIGRATIONS.at(-1).version;
131
+
132
+ function pragmaInteger(database, pragma) {
133
+ const row = database.prepare(`PRAGMA ${pragma}`).get();
134
+ const value = row?.[pragma];
135
+ if (!Number.isSafeInteger(value)) {
136
+ throw new StorageCorruptionError(`SQLite returned an invalid ${pragma} value.`);
137
+ }
138
+ return value;
139
+ }
140
+
141
+ function assertIntegrity(database) {
142
+ const integrity = database.prepare('PRAGMA integrity_check').get();
143
+ if (integrity?.integrity_check !== 'ok') {
144
+ throw new StorageCorruptionError();
145
+ }
146
+ const foreignKeyFailures = database.prepare('PRAGMA foreign_key_check').all();
147
+ if (foreignKeyFailures.length !== 0) {
148
+ throw new StorageCorruptionError('The control-plane database contains invalid ownership references.');
149
+ }
150
+ }
151
+
152
+ function existingUserTables(database) {
153
+ return database.prepare(`
154
+ SELECT name
155
+ FROM sqlite_schema
156
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
157
+ ORDER BY name
158
+ `).all();
159
+ }
160
+
161
+ function verifyMigrationLedger(database, currentVersion) {
162
+ let rows;
163
+ try {
164
+ rows = database.prepare(`
165
+ SELECT version, name, checksum
166
+ FROM schema_migrations
167
+ ORDER BY version
168
+ `).all();
169
+ } catch (error) {
170
+ throw new StorageCorruptionError('The migration ledger is missing or unreadable.', { cause: error });
171
+ }
172
+ if (rows.length !== currentVersion) {
173
+ throw new StorageCorruptionError('The migration ledger does not match the schema version.');
174
+ }
175
+ for (let index = 0; index < currentVersion; index += 1) {
176
+ const expected = MIGRATIONS[index];
177
+ const actual = rows[index];
178
+ if (
179
+ Number(actual?.version) !== expected.version ||
180
+ actual?.name !== expected.name ||
181
+ actual?.checksum !== expected.checksum
182
+ ) {
183
+ throw new StorageCorruptionError(`Migration ${expected.version} failed checksum validation.`);
184
+ }
185
+ }
186
+ }
187
+
188
+ export function applyMigrations(database, appliedAt) {
189
+ assertIntegrity(database);
190
+
191
+ const currentVersion = pragmaInteger(database, 'user_version');
192
+ const applicationId = pragmaInteger(database, 'application_id');
193
+ if (currentVersion > LATEST_SCHEMA_VERSION) {
194
+ throw new UnsupportedSchemaError();
195
+ }
196
+ if (currentVersion === 0 && applicationId === 0 && existingUserTables(database).length !== 0) {
197
+ throw new StorageCorruptionError('Refusing to claim a non-empty, unidentified SQLite database.');
198
+ }
199
+ if (currentVersion > 0 && applicationId !== SQLITE_APPLICATION_ID) {
200
+ throw new StorageCorruptionError('The database belongs to a different application.');
201
+ }
202
+ if (applicationId !== 0 && applicationId !== SQLITE_APPLICATION_ID) {
203
+ throw new StorageCorruptionError('The database application identifier is not recognized.');
204
+ }
205
+
206
+ if (currentVersion > 0) verifyMigrationLedger(database, currentVersion);
207
+
208
+ for (const migration of MIGRATIONS.slice(currentVersion)) {
209
+ database.exec('BEGIN IMMEDIATE');
210
+ try {
211
+ database.exec(migration.sql);
212
+ database.prepare(`
213
+ INSERT INTO schema_migrations(version, name, checksum, applied_at)
214
+ VALUES (?, ?, ?, ?)
215
+ `).run(migration.version, migration.name, migration.checksum, appliedAt);
216
+ database.exec(`PRAGMA application_id = ${SQLITE_APPLICATION_ID}`);
217
+ database.exec(`PRAGMA user_version = ${migration.version}`);
218
+ database.exec('COMMIT');
219
+ } catch (error) {
220
+ try {
221
+ database.exec('ROLLBACK');
222
+ } catch {
223
+ // Preserve the original migration failure.
224
+ }
225
+ throw new StorageCorruptionError(`Migration ${migration.version} could not be applied.`, { cause: error });
226
+ }
227
+ }
228
+
229
+ verifyMigrationLedger(database, LATEST_SCHEMA_VERSION);
230
+ assertIntegrity(database);
231
+ }
@@ -0,0 +1,184 @@
1
+ import { COMPONENT_ID, COMPONENT_ROLE } from './contracts.js';
2
+ import { ControlPlaneError, ValidationError } from './errors.js';
3
+ import { nowIso } from './validation.js';
4
+
5
+ export const OPERATOR_HEALTH_SCHEMA = 'lazying-agent-web/operator-health/v1';
6
+ export const OPERATOR_HEALTH_TIMEOUT_MS = 5_000;
7
+
8
+ const RELEASE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/u;
9
+ const MODEL_ALIAS_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
10
+ const SQLITE_VERSION_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}$/u;
11
+ const STORAGE_FAILURE_REASONS = new Set([
12
+ 'storage_corruption',
13
+ 'storage_security_error',
14
+ 'unsupported_schema'
15
+ ]);
16
+ const DEFAULT_CLOCK = () => new Date();
17
+
18
+ function deepFreeze(value) {
19
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
20
+ for (const child of Object.values(value)) deepFreeze(child);
21
+ Object.freeze(value);
22
+ }
23
+ return value;
24
+ }
25
+
26
+ function storageFailure(error) {
27
+ const reason = error instanceof ControlPlaneError && STORAGE_FAILURE_REASONS.has(error.code)
28
+ ? error.code
29
+ : 'storage_unavailable';
30
+ return Object.freeze({ state: 'unavailable', reason });
31
+ }
32
+
33
+ function storageSuccess(value) {
34
+ if (!value || value.ready !== true
35
+ || !Number.isSafeInteger(value.schemaVersion) || value.schemaVersion < 1
36
+ || typeof value.sqliteVersion !== 'string'
37
+ || !SQLITE_VERSION_PATTERN.test(value.sqliteVersion)) {
38
+ throw new TypeError('storage health response is invalid');
39
+ }
40
+ return Object.freeze({
41
+ state: 'ready',
42
+ schemaVersion: value.schemaVersion,
43
+ sqliteVersion: value.sqliteVersion
44
+ });
45
+ }
46
+
47
+ async function inspectStorage(probe) {
48
+ try {
49
+ return storageSuccess(await probe());
50
+ } catch (error) {
51
+ return storageFailure(error);
52
+ }
53
+ }
54
+
55
+ function timeoutError() {
56
+ const error = new Error('operator dependency health probe timed out');
57
+ error.name = 'TimeoutError';
58
+ return error;
59
+ }
60
+
61
+ async function withTimeout(probe, timeoutMs) {
62
+ const controller = new AbortController();
63
+ let timer;
64
+ const timeout = new Promise((_, reject) => {
65
+ timer = setTimeout(() => {
66
+ const error = timeoutError();
67
+ controller.abort(error);
68
+ reject(error);
69
+ }, timeoutMs);
70
+ });
71
+ try {
72
+ return await Promise.race([
73
+ Promise.resolve().then(() => probe({ signal: controller.signal })),
74
+ timeout
75
+ ]);
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ }
80
+
81
+ function dependencyFailure(error) {
82
+ return Object.freeze({
83
+ state: 'unavailable',
84
+ reason: error?.name === 'TimeoutError' ? 'timeout' : 'dependency_unavailable'
85
+ });
86
+ }
87
+
88
+ async function inspectLocalLlm(probe, timeoutMs) {
89
+ try {
90
+ const value = await withTimeout(probe, timeoutMs);
91
+ if (!value || typeof value.ready !== 'boolean'
92
+ || !Array.isArray(value.availableModelAliases)
93
+ || value.availableModelAliases.length > 64
94
+ || value.availableModelAliases.some((alias) => (
95
+ typeof alias !== 'string' || !MODEL_ALIAS_PATTERN.test(alias)
96
+ ))) {
97
+ throw new TypeError('LocalLLM health response is invalid');
98
+ }
99
+ if (!value.ready) return Object.freeze({ state: 'unavailable', reason: 'model_unavailable' });
100
+ return Object.freeze({
101
+ state: 'ready',
102
+ availableModelAliasCount: value.availableModelAliases.length
103
+ });
104
+ } catch (error) {
105
+ return dependencyFailure(error);
106
+ }
107
+ }
108
+
109
+ async function inspectAginti(probe, timeoutMs) {
110
+ if (probe === null) return Object.freeze({ state: 'not_configured' });
111
+ try {
112
+ const value = await withTimeout(probe, timeoutMs);
113
+ if (!value || typeof value.enabled !== 'boolean') {
114
+ throw new TypeError('AgInTi health response is invalid');
115
+ }
116
+ return Object.freeze({
117
+ state: 'ready',
118
+ capabilityEnabled: value.enabled
119
+ });
120
+ } catch (error) {
121
+ return dependencyFailure(error);
122
+ }
123
+ }
124
+
125
+ export async function createOperatorHealthReport({
126
+ releaseId,
127
+ cloudIndexProbe,
128
+ directChatProbe,
129
+ localLlmProbe,
130
+ agintiProbe = null,
131
+ dependencyTimeoutMs = OPERATOR_HEALTH_TIMEOUT_MS,
132
+ clock = DEFAULT_CLOCK
133
+ } = {}) {
134
+ if (typeof releaseId !== 'string' || !RELEASE_ID_PATTERN.test(releaseId)) {
135
+ throw new ValidationError('releaseId is invalid.');
136
+ }
137
+ if (typeof cloudIndexProbe !== 'function' || typeof directChatProbe !== 'function'
138
+ || typeof localLlmProbe !== 'function'
139
+ || (agintiProbe !== null && typeof agintiProbe !== 'function')) {
140
+ throw new ValidationError('operator health probes must be functions or an absent AgInTi probe.');
141
+ }
142
+ if (!Number.isSafeInteger(dependencyTimeoutMs)
143
+ || dependencyTimeoutMs < 10 || dependencyTimeoutMs > 30_000) {
144
+ throw new ValidationError('dependencyTimeoutMs is outside the operator health bound.');
145
+ }
146
+ if (typeof clock !== 'function') throw new ValidationError('clock must be a function.');
147
+
148
+ const checkedAt = nowIso(clock);
149
+ const [cloudIndexStore, directChatStore, localLlm, aginti] = await Promise.all([
150
+ inspectStorage(cloudIndexProbe),
151
+ inspectStorage(directChatProbe),
152
+ inspectLocalLlm(localLlmProbe, dependencyTimeoutMs),
153
+ inspectAginti(agintiProbe, dependencyTimeoutMs)
154
+ ]);
155
+ const storageReady = cloudIndexStore.state === 'ready' && directChatStore.state === 'ready';
156
+ const dependenciesReady = localLlm.state === 'ready'
157
+ && (aginti.state === 'ready' || aginti.state === 'not_configured');
158
+
159
+ return deepFreeze({
160
+ schema: OPERATOR_HEALTH_SCHEMA,
161
+ checkedAt,
162
+ status: storageReady ? (dependenciesReady ? 'ready' : 'degraded') : 'unavailable',
163
+ component: {
164
+ id: COMPONENT_ID,
165
+ role: COMPONENT_ROLE,
166
+ releaseId
167
+ },
168
+ scope: {
169
+ audience: 'operator',
170
+ publicHttpEndpoint: false,
171
+ staticShell: 'independent'
172
+ },
173
+ storage: { cloudIndexStore, directChatStore },
174
+ dependencies: {
175
+ localLlm,
176
+ aginti,
177
+ lazyEdge: {
178
+ state: 'not_probed',
179
+ healthClaim: false,
180
+ authority: 'lazyedge'
181
+ }
182
+ }
183
+ });
184
+ }
@@ -0,0 +1,131 @@
1
+ import { scrypt, timingSafeEqual } from 'node:crypto';
2
+
3
+ const PREFIX = 'scrypt$v=1$n=131072,r=8,p=1$';
4
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/u;
5
+ const SCRYPT_PARAMETERS = Object.freeze({
6
+ N: 131_072,
7
+ r: 8,
8
+ p: 1,
9
+ keyLength: 64,
10
+ maxmem: 256 * 1024 * 1024
11
+ });
12
+ const COMPATIBLE_KEY_LENGTHS = Object.freeze([32, SCRYPT_PARAMETERS.keyLength]);
13
+
14
+ function canonicalBase64Url(value, name, { minimumBytes, maximumBytes }) {
15
+ if (typeof value !== 'string' || !BASE64URL_PATTERN.test(value) || value.includes('=')) {
16
+ throw new TypeError(`${name} must be canonical unpadded base64url`);
17
+ }
18
+ let decoded;
19
+ try {
20
+ decoded = Buffer.from(value, 'base64url');
21
+ } catch (error) {
22
+ throw new TypeError(`${name} is not valid base64url`, { cause: error });
23
+ }
24
+ if (decoded.toString('base64url') !== value
25
+ || decoded.byteLength < minimumBytes || decoded.byteLength > maximumBytes) {
26
+ decoded.fill(0);
27
+ throw new TypeError(`${name} has an invalid canonical length`);
28
+ }
29
+ return decoded;
30
+ }
31
+
32
+ function parseRecord(record) {
33
+ if (typeof record !== 'string' || record.length < PREFIX.length + 2
34
+ || record.length > 1_024 || /[\s\u0000-\u001f\u007f]/u.test(record)
35
+ || !record.startsWith(PREFIX)) {
36
+ throw new TypeError('password credential is not the required fixed scrypt record');
37
+ }
38
+ const fields = record.slice(PREFIX.length).split('$');
39
+ if (fields.length !== 2) throw new TypeError('password credential has an invalid field count');
40
+ const salt = canonicalBase64Url(fields[0], 'scrypt salt', {
41
+ minimumBytes: 16,
42
+ maximumBytes: 64
43
+ });
44
+ let digest;
45
+ try {
46
+ digest = canonicalBase64Url(fields[1], 'scrypt digest', {
47
+ minimumBytes: COMPATIBLE_KEY_LENGTHS[0],
48
+ maximumBytes: SCRYPT_PARAMETERS.keyLength
49
+ });
50
+ if (!COMPATIBLE_KEY_LENGTHS.includes(digest.byteLength)) {
51
+ digest.fill(0);
52
+ throw new TypeError('scrypt digest has an unsupported canonical length');
53
+ }
54
+ } catch (error) {
55
+ salt.fill(0);
56
+ throw error;
57
+ }
58
+ return { salt, digest, keyLength: digest.byteLength };
59
+ }
60
+
61
+ function passwordInput(value) {
62
+ if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > 4_096) {
63
+ throw new TypeError('password must be a bounded string');
64
+ }
65
+ return value;
66
+ }
67
+
68
+ function derive(password, salt, keyLength) {
69
+ return new Promise((resolve, reject) => {
70
+ scrypt(password, salt, keyLength, {
71
+ N: SCRYPT_PARAMETERS.N,
72
+ r: SCRYPT_PARAMETERS.r,
73
+ p: SCRYPT_PARAMETERS.p,
74
+ maxmem: SCRYPT_PARAMETERS.maxmem
75
+ }, (error, result) => error ? reject(error) : resolve(result));
76
+ });
77
+ }
78
+
79
+ function abortReason(signal) {
80
+ return signal?.reason ?? new DOMException('password verification aborted', 'AbortError');
81
+ }
82
+
83
+ export function createScryptPasswordVerifier(encodedHash) {
84
+ const parsed = parseRecord(encodedHash);
85
+ const salt = Buffer.from(parsed.salt);
86
+ const expected = Buffer.from(parsed.digest);
87
+ const keyLength = parsed.keyLength;
88
+ parsed.salt.fill(0);
89
+ parsed.digest.fill(0);
90
+
91
+ return Object.freeze({
92
+ algorithm: 'scrypt',
93
+ parameters: Object.freeze({
94
+ version: 1,
95
+ n: SCRYPT_PARAMETERS.N,
96
+ r: SCRYPT_PARAMETERS.r,
97
+ p: SCRYPT_PARAMETERS.p,
98
+ keyLength
99
+ }),
100
+ async verify(candidate, { signal } = {}) {
101
+ const password = passwordInput(candidate);
102
+ if (signal !== undefined && !(signal instanceof AbortSignal)) {
103
+ throw new TypeError('signal must be an AbortSignal');
104
+ }
105
+ if (signal?.aborted) throw abortReason(signal);
106
+ const actual = await derive(password, salt, keyLength);
107
+ try {
108
+ if (signal?.aborted) throw abortReason(signal);
109
+ return actual.byteLength === expected.byteLength && timingSafeEqual(actual, expected);
110
+ } finally {
111
+ actual.fill(0);
112
+ }
113
+ }
114
+ });
115
+ }
116
+
117
+ export function validateScryptPasswordHash(encodedHash) {
118
+ const parsed = parseRecord(encodedHash);
119
+ parsed.salt.fill(0);
120
+ parsed.digest.fill(0);
121
+ return true;
122
+ }
123
+
124
+ export const PASSWORD_SCRYPT_FORMAT = Object.freeze({
125
+ prefix: PREFIX,
126
+ version: 1,
127
+ n: SCRYPT_PARAMETERS.N,
128
+ r: SCRYPT_PARAMETERS.r,
129
+ p: SCRYPT_PARAMETERS.p,
130
+ keyLength: SCRYPT_PARAMETERS.keyLength
131
+ });