@nestm/storage 0.1.0-alpha.7 → 0.1.0-alpha.8

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 (74) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +217 -18
  3. package/SECURITY.md +34 -5
  4. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts +14 -3
  5. package/dist/ai-sdk/ai-sdk-workspace-tools.d.ts.map +1 -1
  6. package/dist/ai-sdk/ai-sdk-workspace-tools.js +54 -23
  7. package/dist/ai-sdk/ai-sdk-workspace-tools.js.map +1 -1
  8. package/dist/ai-sdk/index.d.ts +1 -1
  9. package/dist/ai-sdk/index.d.ts.map +1 -1
  10. package/dist/ai-sdk/index.js.map +1 -1
  11. package/dist/files-sdk/files-sdk.driver.d.ts +54 -10
  12. package/dist/files-sdk/files-sdk.driver.d.ts.map +1 -1
  13. package/dist/files-sdk/files-sdk.driver.js +784 -63
  14. package/dist/files-sdk/files-sdk.driver.js.map +1 -1
  15. package/dist/files-sdk/fs/index.d.ts +4 -4
  16. package/dist/files-sdk/fs/index.d.ts.map +1 -1
  17. package/dist/files-sdk/fs/index.js +242 -9
  18. package/dist/files-sdk/fs/index.js.map +1 -1
  19. package/dist/files-sdk/index.d.ts +1 -1
  20. package/dist/files-sdk/index.d.ts.map +1 -1
  21. package/dist/files-sdk/index.js.map +1 -1
  22. package/dist/files-sdk/provider/index.d.ts +9 -5
  23. package/dist/files-sdk/provider/index.d.ts.map +1 -1
  24. package/dist/files-sdk/provider/index.js +53 -15
  25. package/dist/files-sdk/provider/index.js.map +1 -1
  26. package/dist/files-sdk/s3/construction-metadata.d.ts +7 -0
  27. package/dist/files-sdk/s3/construction-metadata.d.ts.map +1 -0
  28. package/dist/files-sdk/s3/construction-metadata.js +13 -0
  29. package/dist/files-sdk/s3/construction-metadata.js.map +1 -0
  30. package/dist/files-sdk/s3/index.d.ts +39 -19
  31. package/dist/files-sdk/s3/index.d.ts.map +1 -1
  32. package/dist/files-sdk/s3/index.js +936 -113
  33. package/dist/files-sdk/s3/index.js.map +1 -1
  34. package/dist/gateway/storage-gateway.controller.d.ts.map +1 -1
  35. package/dist/gateway/storage-gateway.controller.js +25 -5
  36. package/dist/gateway/storage-gateway.controller.js.map +1 -1
  37. package/dist/storage-etag.d.ts +13 -0
  38. package/dist/storage-etag.d.ts.map +1 -0
  39. package/dist/storage-etag.js +31 -0
  40. package/dist/storage-etag.js.map +1 -0
  41. package/dist/storage.client.d.ts +3 -1
  42. package/dist/storage.client.d.ts.map +1 -1
  43. package/dist/storage.client.js +87 -22
  44. package/dist/storage.client.js.map +1 -1
  45. package/dist/storage.driver.d.ts +21 -1
  46. package/dist/storage.driver.d.ts.map +1 -1
  47. package/dist/storage.driver.js.map +1 -1
  48. package/dist/storage.types.d.ts +90 -19
  49. package/dist/storage.types.d.ts.map +1 -1
  50. package/dist/storage.types.js.map +1 -1
  51. package/dist/testing/index.d.ts +1 -0
  52. package/dist/testing/index.d.ts.map +1 -1
  53. package/dist/testing/index.js +1 -0
  54. package/dist/testing/index.js.map +1 -1
  55. package/dist/testing/provider-conformance.d.ts +65 -0
  56. package/dist/testing/provider-conformance.d.ts.map +1 -0
  57. package/dist/testing/provider-conformance.js +853 -0
  58. package/dist/testing/provider-conformance.js.map +1 -0
  59. package/dist/workspace/index.d.ts +1 -0
  60. package/dist/workspace/index.d.ts.map +1 -1
  61. package/dist/workspace/index.js +1 -0
  62. package/dist/workspace/index.js.map +1 -1
  63. package/dist/workspace/storage-workspace.cursor.d.ts +68 -3
  64. package/dist/workspace/storage-workspace.cursor.d.ts.map +1 -1
  65. package/dist/workspace/storage-workspace.cursor.js +402 -39
  66. package/dist/workspace/storage-workspace.cursor.js.map +1 -1
  67. package/dist/workspace/storage-workspace.d.ts.map +1 -1
  68. package/dist/workspace/storage-workspace.js +54 -46
  69. package/dist/workspace/storage-workspace.js.map +1 -1
  70. package/dist/workspace/storage-workspace.types.d.ts +10 -2
  71. package/dist/workspace/storage-workspace.types.d.ts.map +1 -1
  72. package/dist/workspace/storage-workspace.types.js +1 -0
  73. package/dist/workspace/storage-workspace.types.js.map +1 -1
  74. package/package.json +3 -3
@@ -1,45 +1,408 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { StorageErrorCode } from '../storage.error.js';
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, } from 'node:crypto';
2
+ import { StorageErrorCode, isStorageError } from '../storage.error.js';
3
3
  import { workspaceError } from './storage-workspace.error.js';
4
- const MAX_ACTIVE_CURSORS = 1024;
5
- export class StorageWorkspaceCursorStore {
6
- #records = new Map();
7
- issue(binding, state, ttlMs) {
8
- this.#prune();
9
- if (this.#records.size >= MAX_ACTIVE_CURSORS) {
10
- throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, 'Workspace has too many active cursors.', { permanent: true });
11
- }
12
- let cursor;
13
- do {
14
- cursor = randomBytes(24).toString('base64url');
15
- } while (this.#records.has(cursor));
16
- this.#records.set(cursor, {
17
- binding,
18
- expiresAt: Date.now() + ttlMs,
19
- state,
20
- });
21
- return cursor;
22
- }
23
- consume(cursor, binding) {
24
- if (!/^[A-Za-z0-9_-]{32}$/u.test(cursor)) {
25
- throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace cursor has an invalid format.', { permanent: true });
26
- }
27
- const record = this.#records.get(cursor);
28
- this.#records.delete(cursor);
29
- if (record === undefined ||
30
- record.expiresAt <= Date.now() ||
31
- record.binding !== binding) {
32
- throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace cursor is invalid, expired, or belongs to another query.', { permanent: true });
33
- }
34
- return record.state;
35
- }
36
- #prune() {
37
- const now = Date.now();
38
- for (const [cursor, record] of this.#records) {
39
- if (record.expiresAt <= now) {
40
- this.#records.delete(cursor);
4
+ export const STORAGE_WORKSPACE_CURSOR_VERSION = 1;
5
+ export const STORAGE_WORKSPACE_MAX_CURSOR_BYTES = 4_096;
6
+ const AES_KEY_BYTES = 32;
7
+ const AES_GCM_IV_BYTES = 12;
8
+ const AES_GCM_TAG_BYTES = 16;
9
+ const MAX_CURSOR_KEYS = 32;
10
+ const MAX_CURSOR_KEY_ID_BYTES = 64;
11
+ const MAX_CURSOR_PAYLOAD_BYTES = 2_990;
12
+ const MAX_CURSOR_SCOPE_BYTES = 1_024;
13
+ const TOKEN_VERSION = 'swc1';
14
+ const TOKEN_PART_PATTERN = /^[A-Za-z0-9_-]+$/u;
15
+ const TOKEN_PATTERN = /^[A-Za-z0-9._-]+$/u;
16
+ const BINDING_PATTERN = /^[A-Za-z0-9_-]{43}$/u;
17
+ const forbiddenIdentityCharacter = /\p{C}/u;
18
+ const encoder = new TextEncoder();
19
+ const decoder = new TextDecoder('utf-8', { fatal: true });
20
+ /** Stateless AES-256-GCM cursor codec suitable for shared multi-replica use. */
21
+ export class Aes256GcmStorageWorkspaceCursorCodec {
22
+ #activeKeyId;
23
+ #keys;
24
+ constructor(options) {
25
+ if (typeof options !== 'object' ||
26
+ options === null ||
27
+ !validKeyId(options.activeKeyId)) {
28
+ throw new TypeError('Workspace cursor activeKeyId must be a 1-64 byte base64url identifier.');
29
+ }
30
+ if (typeof options.keys !== 'object' ||
31
+ options.keys === null ||
32
+ Array.isArray(options.keys)) {
33
+ throw new TypeError('Workspace cursor keys must be a key-id record.');
34
+ }
35
+ const entries = Object.entries(options.keys);
36
+ if (entries.length === 0 || entries.length > MAX_CURSOR_KEYS) {
37
+ throw new TypeError(`Workspace cursor key ring must contain 1-${MAX_CURSOR_KEYS} keys.`);
38
+ }
39
+ const keys = new Map();
40
+ for (const [keyId, key] of entries) {
41
+ if (!validKeyId(keyId)) {
42
+ throw new TypeError('Workspace cursor key IDs must be 1-64 byte base64url identifiers.');
43
+ }
44
+ if (!(key instanceof Uint8Array) || key.byteLength !== AES_KEY_BYTES) {
45
+ throw new TypeError(`Workspace cursor key "${keyId}" must contain exactly ${AES_KEY_BYTES} bytes.`);
41
46
  }
47
+ keys.set(keyId, Buffer.from(key));
48
+ }
49
+ if (!keys.has(options.activeKeyId)) {
50
+ throw new TypeError('Workspace cursor activeKeyId must identify a configured key.');
51
+ }
52
+ this.#activeKeyId = options.activeKeyId;
53
+ this.#keys = keys;
54
+ }
55
+ encode(payload, options) {
56
+ if (!(payload instanceof Uint8Array) || payload.byteLength === 0) {
57
+ throw new TypeError('Workspace cursor payload must not be empty.');
58
+ }
59
+ if (!Number.isSafeInteger(options.expiresAt) || options.expiresAt <= 0) {
60
+ throw new TypeError('Workspace cursor expiry must be a positive safe integer.');
61
+ }
62
+ if (payload.byteLength > MAX_CURSOR_PAYLOAD_BYTES) {
63
+ throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, 'Workspace cursor payload exceeds the bounded token size.', { permanent: true });
64
+ }
65
+ const key = this.#keys.get(this.#activeKeyId);
66
+ if (key === undefined) {
67
+ throw new TypeError('Workspace cursor active key is unavailable.');
68
+ }
69
+ const iv = randomBytes(AES_GCM_IV_BYTES);
70
+ const cipher = createCipheriv('aes-256-gcm', key, iv, {
71
+ authTagLength: AES_GCM_TAG_BYTES,
72
+ });
73
+ cipher.setAAD(cursorAad(this.#activeKeyId));
74
+ const ciphertext = Buffer.concat([cipher.update(payload), cipher.final()]);
75
+ const body = Buffer.concat([iv, ciphertext, cipher.getAuthTag()]);
76
+ const token = `${TOKEN_VERSION}.${this.#activeKeyId}.${body.toString('base64url')}`;
77
+ if (token.length > STORAGE_WORKSPACE_MAX_CURSOR_BYTES) {
78
+ throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, 'Workspace cursor exceeds the bounded token size.', { permanent: true });
79
+ }
80
+ return token;
81
+ }
82
+ decode(token) {
83
+ if (typeof token !== 'string' ||
84
+ token.length === 0 ||
85
+ token.length > STORAGE_WORKSPACE_MAX_CURSOR_BYTES ||
86
+ !TOKEN_PATTERN.test(token)) {
87
+ throw invalidCursor();
88
+ }
89
+ const parts = token.split('.');
90
+ const version = parts[0];
91
+ const keyId = parts[1];
92
+ const encodedBody = parts[2];
93
+ if (parts.length !== 3 ||
94
+ version !== TOKEN_VERSION ||
95
+ keyId === undefined ||
96
+ encodedBody === undefined ||
97
+ !validKeyId(keyId) ||
98
+ !TOKEN_PART_PATTERN.test(encodedBody)) {
99
+ throw invalidCursor();
100
+ }
101
+ const key = this.#keys.get(keyId);
102
+ if (key === undefined) {
103
+ throw invalidCursor();
104
+ }
105
+ const body = Buffer.from(encodedBody, 'base64url');
106
+ if (body.toString('base64url') !== encodedBody ||
107
+ body.byteLength <= AES_GCM_IV_BYTES + AES_GCM_TAG_BYTES) {
108
+ throw invalidCursor();
42
109
  }
110
+ const iv = body.subarray(0, AES_GCM_IV_BYTES);
111
+ const ciphertext = body.subarray(AES_GCM_IV_BYTES, body.byteLength - AES_GCM_TAG_BYTES);
112
+ const tag = body.subarray(body.byteLength - AES_GCM_TAG_BYTES);
113
+ try {
114
+ const decipher = createDecipheriv('aes-256-gcm', key, iv, {
115
+ authTagLength: AES_GCM_TAG_BYTES,
116
+ });
117
+ decipher.setAAD(cursorAad(keyId));
118
+ decipher.setAuthTag(tag);
119
+ return Uint8Array.from(Buffer.concat([decipher.update(ciphertext), decipher.final()]));
120
+ }
121
+ catch {
122
+ throw invalidCursor();
123
+ }
124
+ }
125
+ }
126
+ export function resolveStorageWorkspaceCursorConfiguration(configuration) {
127
+ if (configuration === undefined) {
128
+ return undefined;
129
+ }
130
+ if (typeof configuration !== 'object' ||
131
+ configuration === null ||
132
+ typeof configuration.codec !== 'object' ||
133
+ configuration.codec === null ||
134
+ typeof configuration.codec.encode !== 'function' ||
135
+ typeof configuration.codec.decode !== 'function') {
136
+ throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace cursor configuration requires a codec.', { permanent: true });
137
+ }
138
+ assertStableIdentity(configuration.mountId, 'cursor.mountId');
139
+ assertStableIdentity(configuration.scope, 'cursor.scope');
140
+ return Object.freeze({
141
+ codec: configuration.codec,
142
+ mountId: configuration.mountId,
143
+ scope: configuration.scope,
144
+ });
145
+ }
146
+ export async function issueStorageWorkspaceCursor(configuration, binding, cursor) {
147
+ if (configuration === undefined) {
148
+ throw cursorNotConfigured();
149
+ }
150
+ const expiresAt = Date.now() + binding.limits.cursorTtlMs;
151
+ if (!Number.isSafeInteger(expiresAt)) {
152
+ throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, 'Workspace cursor expiry exceeds the supported range.', { permanent: true });
153
+ }
154
+ const common = {
155
+ b: cursorBindingDigest(configuration, binding, cursor.operation),
156
+ e: expiresAt,
157
+ v: STORAGE_WORKSPACE_CURSOR_VERSION,
158
+ };
159
+ const wire = cursor.operation === 'list'
160
+ ? {
161
+ ...common,
162
+ o: 'list',
163
+ q: {
164
+ d: cursor.directory,
165
+ l: cursor.limit,
166
+ r: cursor.recursive,
167
+ },
168
+ s: { c: cursor.backendCursor },
169
+ }
170
+ : {
171
+ ...common,
172
+ o: 'search',
173
+ q: {
174
+ c: cursor.caseInsensitive,
175
+ d: cursor.directory,
176
+ l: cursor.limit,
177
+ m: cursor.match,
178
+ q: cursor.query,
179
+ },
180
+ s: { c: cursor.backendCursor, n: cursor.scanned },
181
+ };
182
+ const payload = encoder.encode(JSON.stringify(wire));
183
+ if (payload.byteLength > MAX_CURSOR_PAYLOAD_BYTES) {
184
+ throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, 'Workspace cursor state exceeds the bounded token size.', { permanent: true });
185
+ }
186
+ let token;
187
+ try {
188
+ token = await configuration.codec.encode(payload, { expiresAt });
189
+ }
190
+ catch (error) {
191
+ throw issueFailure(error);
192
+ }
193
+ if (typeof token !== 'string' ||
194
+ token.length === 0 ||
195
+ !TOKEN_PATTERN.test(token)) {
196
+ throw workspaceError(StorageErrorCode.PROVIDER, 'Workspace cursor codec returned an invalid token.');
197
+ }
198
+ if (token.length >
199
+ Math.min(STORAGE_WORKSPACE_MAX_CURSOR_BYTES, binding.limits.maxCursorBytes)) {
200
+ throw workspaceError(StorageErrorCode.LIMIT_EXCEEDED, 'Workspace cursor exceeds the configured byte limit.', { permanent: true });
201
+ }
202
+ return token;
203
+ }
204
+ export async function decodeStorageWorkspaceCursor(configuration, binding, operation, token) {
205
+ if (configuration === undefined) {
206
+ throw cursorNotConfigured();
207
+ }
208
+ if (typeof token !== 'string' ||
209
+ token.length === 0 ||
210
+ token.length >
211
+ Math.min(STORAGE_WORKSPACE_MAX_CURSOR_BYTES, binding.limits.maxCursorBytes) ||
212
+ !TOKEN_PATTERN.test(token)) {
213
+ throw invalidCursor();
43
214
  }
215
+ let payload;
216
+ try {
217
+ payload = await configuration.codec.decode(token);
218
+ }
219
+ catch (error) {
220
+ throw decodeFailure(error);
221
+ }
222
+ if (!(payload instanceof Uint8Array) ||
223
+ payload.byteLength === 0 ||
224
+ payload.byteLength > MAX_CURSOR_PAYLOAD_BYTES) {
225
+ throw invalidCursor();
226
+ }
227
+ const wire = parseWireCursor(payload);
228
+ const now = Date.now();
229
+ if (wire.e <= now ||
230
+ wire.o !== operation ||
231
+ wire.b !== cursorBindingDigest(configuration, binding, operation)) {
232
+ throw invalidCursor();
233
+ }
234
+ if ((wire.o === 'list' && wire.q.l > binding.limits.maxPageSize) ||
235
+ (wire.o === 'search' &&
236
+ (wire.q.l > binding.limits.maxSearchResults ||
237
+ wire.s.n > binding.limits.maxSearchScan))) {
238
+ throw invalidCursor();
239
+ }
240
+ return wire.o === 'list'
241
+ ? {
242
+ backendCursor: wire.s.c,
243
+ directory: wire.q.d,
244
+ limit: wire.q.l,
245
+ operation: 'list',
246
+ recursive: wire.q.r,
247
+ }
248
+ : {
249
+ backendCursor: wire.s.c,
250
+ caseInsensitive: wire.q.c,
251
+ directory: wire.q.d,
252
+ limit: wire.q.l,
253
+ match: wire.q.m,
254
+ operation: 'search',
255
+ query: wire.q.q,
256
+ scanned: wire.s.n,
257
+ };
258
+ }
259
+ function parseWireCursor(payload) {
260
+ let value;
261
+ try {
262
+ value = JSON.parse(decoder.decode(payload));
263
+ }
264
+ catch {
265
+ throw invalidCursor();
266
+ }
267
+ if (!isRecord(value) ||
268
+ !hasExactKeys(value, ['b', 'e', 'o', 'q', 's', 'v'])) {
269
+ throw invalidCursor();
270
+ }
271
+ if (value.v !== STORAGE_WORKSPACE_CURSOR_VERSION ||
272
+ typeof value.b !== 'string' ||
273
+ !BINDING_PATTERN.test(value.b) ||
274
+ !positiveSafeInteger(value.e) ||
275
+ !isRecord(value.q) ||
276
+ !isRecord(value.s)) {
277
+ throw invalidCursor();
278
+ }
279
+ if (value.o === 'list') {
280
+ if (!hasExactKeys(value.q, ['d', 'l', 'r']) ||
281
+ !hasExactKeys(value.s, ['c']) ||
282
+ typeof value.q.d !== 'string' ||
283
+ !positiveSafeInteger(value.q.l) ||
284
+ typeof value.q.r !== 'boolean' ||
285
+ !nonEmptyString(value.s.c)) {
286
+ throw invalidCursor();
287
+ }
288
+ return {
289
+ b: value.b,
290
+ e: value.e,
291
+ o: 'list',
292
+ q: { d: value.q.d, l: value.q.l, r: value.q.r },
293
+ s: { c: value.s.c },
294
+ v: STORAGE_WORKSPACE_CURSOR_VERSION,
295
+ };
296
+ }
297
+ if (value.o === 'search') {
298
+ if (!hasExactKeys(value.q, ['c', 'd', 'l', 'm', 'q']) ||
299
+ !hasExactKeys(value.s, ['c', 'n']) ||
300
+ typeof value.q.c !== 'boolean' ||
301
+ typeof value.q.d !== 'string' ||
302
+ !positiveSafeInteger(value.q.l) ||
303
+ !searchMatch(value.q.m) ||
304
+ !nonEmptyString(value.q.q) ||
305
+ !nonEmptyString(value.s.c) ||
306
+ !nonNegativeSafeInteger(value.s.n)) {
307
+ throw invalidCursor();
308
+ }
309
+ return {
310
+ b: value.b,
311
+ e: value.e,
312
+ o: 'search',
313
+ q: {
314
+ c: value.q.c,
315
+ d: value.q.d,
316
+ l: value.q.l,
317
+ m: value.q.m,
318
+ q: value.q.q,
319
+ },
320
+ s: { c: value.s.c, n: value.s.n },
321
+ v: STORAGE_WORKSPACE_CURSOR_VERSION,
322
+ };
323
+ }
324
+ throw invalidCursor();
325
+ }
326
+ function cursorBindingDigest(configuration, binding, operation) {
327
+ const limits = binding.limits;
328
+ const canonical = JSON.stringify({
329
+ limits: {
330
+ cursorTtlMs: limits.cursorTtlMs,
331
+ maxCursorBytes: limits.maxCursorBytes,
332
+ maxPageSize: limits.maxPageSize,
333
+ maxPathBytes: limits.maxPathBytes,
334
+ maxReadBytes: limits.maxReadBytes,
335
+ maxSearchResults: limits.maxSearchResults,
336
+ maxSearchScan: limits.maxSearchScan,
337
+ maxWriteBytes: limits.maxWriteBytes,
338
+ },
339
+ mountId: configuration.mountId,
340
+ operation,
341
+ prefix: binding.prefix,
342
+ scope: configuration.scope,
343
+ store: binding.store,
344
+ });
345
+ return createHash('sha256').update(canonical, 'utf8').digest('base64url');
346
+ }
347
+ function issueFailure(error) {
348
+ if (isStorageError(error)) {
349
+ return workspaceError(error.code, 'Workspace cursor could not be issued.', {
350
+ permanent: error.permanent,
351
+ });
352
+ }
353
+ return workspaceError(StorageErrorCode.PROVIDER, 'Workspace cursor codec failed while issuing a token.');
354
+ }
355
+ function decodeFailure(error) {
356
+ if (isStorageError(error)) {
357
+ if (error.code === StorageErrorCode.INVALID_ARGUMENT ||
358
+ error.code === StorageErrorCode.NOT_FOUND) {
359
+ return invalidCursor();
360
+ }
361
+ return workspaceError(error.code, 'Workspace cursor codec failed while decoding a token.', { permanent: error.permanent });
362
+ }
363
+ return workspaceError(StorageErrorCode.PROVIDER, 'Workspace cursor codec failed while decoding a token.');
364
+ }
365
+ function cursorNotConfigured() {
366
+ return workspaceError(StorageErrorCode.NOT_SUPPORTED, 'Workspace pagination requires a configured cursor codec.', { permanent: true });
367
+ }
368
+ function invalidCursor() {
369
+ return workspaceError(StorageErrorCode.INVALID_ARGUMENT, 'Workspace cursor is invalid, expired, or belongs to another query.', { permanent: true });
370
+ }
371
+ function cursorAad(keyId) {
372
+ return Buffer.from(`@nestm/storage/workspace-cursor\0${TOKEN_VERSION}\0${keyId}`);
373
+ }
374
+ function validKeyId(value) {
375
+ return (typeof value === 'string' &&
376
+ TOKEN_PART_PATTERN.test(value) &&
377
+ encoder.encode(value).byteLength <= MAX_CURSOR_KEY_ID_BYTES);
378
+ }
379
+ function assertStableIdentity(value, label) {
380
+ if (typeof value !== 'string' ||
381
+ value.trim().length === 0 ||
382
+ value !== value.normalize('NFC') ||
383
+ forbiddenIdentityCharacter.test(value) ||
384
+ encoder.encode(value).byteLength > MAX_CURSOR_SCOPE_BYTES) {
385
+ throw workspaceError(StorageErrorCode.INVALID_ARGUMENT, `${label} must be a non-empty stable NFC identity of at most ${MAX_CURSOR_SCOPE_BYTES} bytes.`, { permanent: true });
386
+ }
387
+ }
388
+ function isRecord(value) {
389
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
390
+ }
391
+ function hasExactKeys(value, expected) {
392
+ const keys = Object.keys(value);
393
+ return (keys.length === expected.length &&
394
+ expected.every((key) => Object.hasOwn(value, key)));
395
+ }
396
+ function positiveSafeInteger(value) {
397
+ return Number.isSafeInteger(value) && value > 0;
398
+ }
399
+ function nonNegativeSafeInteger(value) {
400
+ return Number.isSafeInteger(value) && value >= 0;
401
+ }
402
+ function nonEmptyString(value) {
403
+ return typeof value === 'string' && value.length > 0;
404
+ }
405
+ function searchMatch(value) {
406
+ return value === 'glob' || value === 'substring' || value === 'exact';
44
407
  }
45
408
  //# sourceMappingURL=storage-workspace.cursor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage-workspace.cursor.js","sourceRoot":"","sources":["../../src/workspace/storage-workspace.cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAQ9D,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEhC,MAAM,OAAO,2BAA2B;IAC7B,QAAQ,GAAG,IAAI,GAAG,EAAiC,CAAC;IAE7D,KAAK,CAAQ,OAAe,EAAE,KAAY,EAAE,KAAa;QACvD,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,kBAAkB,EAAE,CAAC;YAC7C,MAAM,cAAc,CAClB,gBAAgB,CAAC,cAAc,EAC/B,wCAAwC,EACxC,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAC;QACJ,CAAC;QACD,IAAI,MAAc,CAAC;QACnB,GAAG,CAAC;YACF,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACjD,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;QACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE;YACxB,OAAO;YACP,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;YAC7B,KAAK;SACN,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,CAAQ,MAAc,EAAE,OAAe;QAC5C,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACzC,MAAM,cAAc,CAClB,gBAAgB,CAAC,gBAAgB,EACjC,yCAAyC,EACzC,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7B,IACE,MAAM,KAAK,SAAS;YACpB,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE;YAC9B,MAAM,CAAC,OAAO,KAAK,OAAO,EAC1B,CAAC;YACD,MAAM,cAAc,CAClB,gBAAgB,CAAC,gBAAgB,EACjC,oEAAoE,EACpE,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,KAAc,CAAC;IAC/B,CAAC;IAED,MAAM;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7C,IAAI,MAAM,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;gBAC5B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;CACF","sourcesContent":["import { randomBytes } from 'node:crypto';\n\nimport { StorageErrorCode } from '../storage.error.js';\n\nimport { workspaceError } from './storage-workspace.error.js';\n\ninterface CursorRecord<State> {\n readonly binding: string;\n readonly expiresAt: number;\n readonly state: State;\n}\n\nconst MAX_ACTIVE_CURSORS = 1024;\n\nexport class StorageWorkspaceCursorStore {\n readonly #records = new Map<string, CursorRecord<unknown>>();\n\n issue<State>(binding: string, state: State, ttlMs: number): string {\n this.#prune();\n if (this.#records.size >= MAX_ACTIVE_CURSORS) {\n throw workspaceError(\n StorageErrorCode.LIMIT_EXCEEDED,\n 'Workspace has too many active cursors.',\n { permanent: true },\n );\n }\n let cursor: string;\n do {\n cursor = randomBytes(24).toString('base64url');\n } while (this.#records.has(cursor));\n this.#records.set(cursor, {\n binding,\n expiresAt: Date.now() + ttlMs,\n state,\n });\n return cursor;\n }\n\n consume<State>(cursor: string, binding: string): State {\n if (!/^[A-Za-z0-9_-]{32}$/u.test(cursor)) {\n throw workspaceError(\n StorageErrorCode.INVALID_ARGUMENT,\n 'Workspace cursor has an invalid format.',\n { permanent: true },\n );\n }\n const record = this.#records.get(cursor);\n this.#records.delete(cursor);\n if (\n record === undefined ||\n record.expiresAt <= Date.now() ||\n record.binding !== binding\n ) {\n throw workspaceError(\n StorageErrorCode.INVALID_ARGUMENT,\n 'Workspace cursor is invalid, expired, or belongs to another query.',\n { permanent: true },\n );\n }\n return record.state as State;\n }\n\n #prune(): void {\n const now = Date.now();\n for (const [cursor, record] of this.#records) {\n if (record.expiresAt <= now) {\n this.#records.delete(cursor);\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"storage-workspace.cursor.js","sourceRoot":"","sources":["../../src/workspace/storage-workspace.cursor.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createCipheriv,\n createDecipheriv,\n createHash,\n randomBytes,\n} from 'node:crypto';\n\nimport { StorageErrorCode, isStorageError } from '../storage.error.js';\n\nimport { workspaceError } from './storage-workspace.error.js';\nimport type {\n StorageWorkspaceLimits,\n StorageWorkspaceSearchMatch,\n} from './storage-workspace.types.js';\n\nexport const STORAGE_WORKSPACE_CURSOR_VERSION = 1 as const;\nexport const STORAGE_WORKSPACE_MAX_CURSOR_BYTES = 4_096;\n\nconst AES_KEY_BYTES = 32;\nconst AES_GCM_IV_BYTES = 12;\nconst AES_GCM_TAG_BYTES = 16;\nconst MAX_CURSOR_KEYS = 32;\nconst MAX_CURSOR_KEY_ID_BYTES = 64;\nconst MAX_CURSOR_PAYLOAD_BYTES = 2_990;\nconst MAX_CURSOR_SCOPE_BYTES = 1_024;\nconst TOKEN_VERSION = 'swc1';\nconst TOKEN_PART_PATTERN = /^[A-Za-z0-9_-]+$/u;\nconst TOKEN_PATTERN = /^[A-Za-z0-9._-]+$/u;\nconst BINDING_PATTERN = /^[A-Za-z0-9_-]{43}$/u;\nconst forbiddenIdentityCharacter = /\\p{C}/u;\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder('utf-8', { fatal: true });\n\nexport interface StorageWorkspaceCursorEncodeOptions {\n /** Absolute Unix timestamp in milliseconds used by durable stores for TTL. */\n readonly expiresAt: number;\n}\n\n/**\n * Server-side opaque cursor contract. Implementations must either authenticate\n * and encrypt payloads or keep them behind a shared durable random token.\n * Tokens use only `[A-Za-z0-9._-]`, are at most 4,096 bytes, and `decode` must\n * be non-destructive so the same token remains replayable until its authenticated\n * expiry. Durable stores must expire records at `options.expiresAt` and read\n * rather than consume them. Missing or expired records should fail with\n * `StorageErrorCode.NOT_FOUND` or `StorageErrorCode.INVALID_ARGUMENT`;\n * availability failures retain their operational storage error code.\n */\nexport interface StorageWorkspaceCursorCodec {\n encode(\n payload: Uint8Array,\n options: StorageWorkspaceCursorEncodeOptions,\n ): Promise<string> | string;\n decode(token: string): Promise<Uint8Array> | Uint8Array;\n}\n\nexport interface StorageWorkspaceCursorConfiguration {\n /** Codec shared by every replica that serves this logical mount. */\n readonly codec: StorageWorkspaceCursorCodec;\n /** Stable server-derived mount or binding identity. */\n readonly mountId: string;\n /** Stable trusted tenant/workspace scope. */\n readonly scope: string;\n}\n\nexport interface Aes256GcmStorageWorkspaceCursorCodecOptions {\n /** Key used for newly issued cursors. */\n readonly activeKeyId: string;\n /** Decryption key ring. Retain rotated keys for at least one cursor TTL. */\n readonly keys: Readonly<Record<string, Uint8Array>>;\n}\n\n/** Stateless AES-256-GCM cursor codec suitable for shared multi-replica use. */\nexport class Aes256GcmStorageWorkspaceCursorCodec implements StorageWorkspaceCursorCodec {\n readonly #activeKeyId: string;\n readonly #keys: ReadonlyMap<string, Buffer>;\n\n constructor(options: Aes256GcmStorageWorkspaceCursorCodecOptions) {\n if (\n typeof options !== 'object' ||\n options === null ||\n !validKeyId(options.activeKeyId)\n ) {\n throw new TypeError(\n 'Workspace cursor activeKeyId must be a 1-64 byte base64url identifier.',\n );\n }\n if (\n typeof options.keys !== 'object' ||\n options.keys === null ||\n Array.isArray(options.keys)\n ) {\n throw new TypeError('Workspace cursor keys must be a key-id record.');\n }\n const entries = Object.entries(options.keys);\n if (entries.length === 0 || entries.length > MAX_CURSOR_KEYS) {\n throw new TypeError(\n `Workspace cursor key ring must contain 1-${MAX_CURSOR_KEYS} keys.`,\n );\n }\n const keys = new Map<string, Buffer>();\n for (const [keyId, key] of entries) {\n if (!validKeyId(keyId)) {\n throw new TypeError(\n 'Workspace cursor key IDs must be 1-64 byte base64url identifiers.',\n );\n }\n if (!(key instanceof Uint8Array) || key.byteLength !== AES_KEY_BYTES) {\n throw new TypeError(\n `Workspace cursor key \"${keyId}\" must contain exactly ${AES_KEY_BYTES} bytes.`,\n );\n }\n keys.set(keyId, Buffer.from(key));\n }\n if (!keys.has(options.activeKeyId)) {\n throw new TypeError(\n 'Workspace cursor activeKeyId must identify a configured key.',\n );\n }\n this.#activeKeyId = options.activeKeyId;\n this.#keys = keys;\n }\n\n encode(\n payload: Uint8Array,\n options: StorageWorkspaceCursorEncodeOptions,\n ): string {\n if (!(payload instanceof Uint8Array) || payload.byteLength === 0) {\n throw new TypeError('Workspace cursor payload must not be empty.');\n }\n if (!Number.isSafeInteger(options.expiresAt) || options.expiresAt <= 0) {\n throw new TypeError(\n 'Workspace cursor expiry must be a positive safe integer.',\n );\n }\n if (payload.byteLength > MAX_CURSOR_PAYLOAD_BYTES) {\n throw workspaceError(\n StorageErrorCode.LIMIT_EXCEEDED,\n 'Workspace cursor payload exceeds the bounded token size.',\n { permanent: true },\n );\n }\n\n const key = this.#keys.get(this.#activeKeyId);\n if (key === undefined) {\n throw new TypeError('Workspace cursor active key is unavailable.');\n }\n const iv = randomBytes(AES_GCM_IV_BYTES);\n const cipher = createCipheriv('aes-256-gcm', key, iv, {\n authTagLength: AES_GCM_TAG_BYTES,\n });\n cipher.setAAD(cursorAad(this.#activeKeyId));\n const ciphertext = Buffer.concat([cipher.update(payload), cipher.final()]);\n const body = Buffer.concat([iv, ciphertext, cipher.getAuthTag()]);\n const token = `${TOKEN_VERSION}.${this.#activeKeyId}.${body.toString('base64url')}`;\n if (token.length > STORAGE_WORKSPACE_MAX_CURSOR_BYTES) {\n throw workspaceError(\n StorageErrorCode.LIMIT_EXCEEDED,\n 'Workspace cursor exceeds the bounded token size.',\n { permanent: true },\n );\n }\n return token;\n }\n\n decode(token: string): Uint8Array {\n if (\n typeof token !== 'string' ||\n token.length === 0 ||\n token.length > STORAGE_WORKSPACE_MAX_CURSOR_BYTES ||\n !TOKEN_PATTERN.test(token)\n ) {\n throw invalidCursor();\n }\n const parts = token.split('.');\n const version = parts[0];\n const keyId = parts[1];\n const encodedBody = parts[2];\n if (\n parts.length !== 3 ||\n version !== TOKEN_VERSION ||\n keyId === undefined ||\n encodedBody === undefined ||\n !validKeyId(keyId) ||\n !TOKEN_PART_PATTERN.test(encodedBody)\n ) {\n throw invalidCursor();\n }\n const key = this.#keys.get(keyId);\n if (key === undefined) {\n throw invalidCursor();\n }\n const body = Buffer.from(encodedBody, 'base64url');\n if (\n body.toString('base64url') !== encodedBody ||\n body.byteLength <= AES_GCM_IV_BYTES + AES_GCM_TAG_BYTES\n ) {\n throw invalidCursor();\n }\n const iv = body.subarray(0, AES_GCM_IV_BYTES);\n const ciphertext = body.subarray(\n AES_GCM_IV_BYTES,\n body.byteLength - AES_GCM_TAG_BYTES,\n );\n const tag = body.subarray(body.byteLength - AES_GCM_TAG_BYTES);\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, iv, {\n authTagLength: AES_GCM_TAG_BYTES,\n });\n decipher.setAAD(cursorAad(keyId));\n decipher.setAuthTag(tag);\n return Uint8Array.from(\n Buffer.concat([decipher.update(ciphertext), decipher.final()]),\n );\n } catch {\n throw invalidCursor();\n }\n }\n}\n\ninterface StorageWorkspaceCursorBinding {\n readonly limits: Readonly<StorageWorkspaceLimits>;\n readonly prefix: string;\n readonly store: string;\n}\n\nexport interface StorageWorkspaceListCursorData {\n readonly backendCursor: string;\n readonly directory: string;\n readonly limit: number;\n readonly operation: 'list';\n readonly recursive: boolean;\n}\n\nexport interface StorageWorkspaceSearchCursorData {\n readonly backendCursor: string;\n readonly caseInsensitive: boolean;\n readonly directory: string;\n readonly limit: number;\n readonly match: StorageWorkspaceSearchMatch;\n readonly operation: 'search';\n readonly query: string;\n readonly scanned: number;\n}\n\nexport type StorageWorkspaceCursorData =\n StorageWorkspaceListCursorData | StorageWorkspaceSearchCursorData;\n\ninterface WireCursorCommon {\n readonly b: string;\n readonly e: number;\n readonly v: typeof STORAGE_WORKSPACE_CURSOR_VERSION;\n}\n\ninterface WireListCursor extends WireCursorCommon {\n readonly o: 'list';\n readonly q: {\n readonly d: string;\n readonly l: number;\n readonly r: boolean;\n };\n readonly s: { readonly c: string };\n}\n\ninterface WireSearchCursor extends WireCursorCommon {\n readonly o: 'search';\n readonly q: {\n readonly c: boolean;\n readonly d: string;\n readonly l: number;\n readonly m: StorageWorkspaceSearchMatch;\n readonly q: string;\n };\n readonly s: {\n readonly c: string;\n readonly n: number;\n };\n}\n\ntype WireCursor = WireListCursor | WireSearchCursor;\n\nexport function resolveStorageWorkspaceCursorConfiguration(\n configuration: StorageWorkspaceCursorConfiguration | undefined,\n): Readonly<StorageWorkspaceCursorConfiguration> | undefined {\n if (configuration === undefined) {\n return undefined;\n }\n if (\n typeof configuration !== 'object' ||\n configuration === null ||\n typeof configuration.codec !== 'object' ||\n configuration.codec === null ||\n typeof configuration.codec.encode !== 'function' ||\n typeof configuration.codec.decode !== 'function'\n ) {\n throw workspaceError(\n StorageErrorCode.INVALID_ARGUMENT,\n 'Workspace cursor configuration requires a codec.',\n { permanent: true },\n );\n }\n assertStableIdentity(configuration.mountId, 'cursor.mountId');\n assertStableIdentity(configuration.scope, 'cursor.scope');\n return Object.freeze({\n codec: configuration.codec,\n mountId: configuration.mountId,\n scope: configuration.scope,\n });\n}\n\nexport async function issueStorageWorkspaceCursor(\n configuration: Readonly<StorageWorkspaceCursorConfiguration> | undefined,\n binding: StorageWorkspaceCursorBinding,\n cursor: StorageWorkspaceCursorData,\n): Promise<string> {\n if (configuration === undefined) {\n throw cursorNotConfigured();\n }\n const expiresAt = Date.now() + binding.limits.cursorTtlMs;\n if (!Number.isSafeInteger(expiresAt)) {\n throw workspaceError(\n StorageErrorCode.LIMIT_EXCEEDED,\n 'Workspace cursor expiry exceeds the supported range.',\n { permanent: true },\n );\n }\n const common: WireCursorCommon = {\n b: cursorBindingDigest(configuration, binding, cursor.operation),\n e: expiresAt,\n v: STORAGE_WORKSPACE_CURSOR_VERSION,\n };\n const wire: WireCursor =\n cursor.operation === 'list'\n ? {\n ...common,\n o: 'list',\n q: {\n d: cursor.directory,\n l: cursor.limit,\n r: cursor.recursive,\n },\n s: { c: cursor.backendCursor },\n }\n : {\n ...common,\n o: 'search',\n q: {\n c: cursor.caseInsensitive,\n d: cursor.directory,\n l: cursor.limit,\n m: cursor.match,\n q: cursor.query,\n },\n s: { c: cursor.backendCursor, n: cursor.scanned },\n };\n const payload = encoder.encode(JSON.stringify(wire));\n if (payload.byteLength > MAX_CURSOR_PAYLOAD_BYTES) {\n throw workspaceError(\n StorageErrorCode.LIMIT_EXCEEDED,\n 'Workspace cursor state exceeds the bounded token size.',\n { permanent: true },\n );\n }\n\n let token: string;\n try {\n token = await configuration.codec.encode(payload, { expiresAt });\n } catch (error) {\n throw issueFailure(error);\n }\n if (\n typeof token !== 'string' ||\n token.length === 0 ||\n !TOKEN_PATTERN.test(token)\n ) {\n throw workspaceError(\n StorageErrorCode.PROVIDER,\n 'Workspace cursor codec returned an invalid token.',\n );\n }\n if (\n token.length >\n Math.min(STORAGE_WORKSPACE_MAX_CURSOR_BYTES, binding.limits.maxCursorBytes)\n ) {\n throw workspaceError(\n StorageErrorCode.LIMIT_EXCEEDED,\n 'Workspace cursor exceeds the configured byte limit.',\n { permanent: true },\n );\n }\n return token;\n}\n\nexport function decodeStorageWorkspaceCursor(\n configuration: Readonly<StorageWorkspaceCursorConfiguration> | undefined,\n binding: StorageWorkspaceCursorBinding,\n operation: 'list',\n token: string,\n): Promise<StorageWorkspaceListCursorData>;\nexport function decodeStorageWorkspaceCursor(\n configuration: Readonly<StorageWorkspaceCursorConfiguration> | undefined,\n binding: StorageWorkspaceCursorBinding,\n operation: 'search',\n token: string,\n): Promise<StorageWorkspaceSearchCursorData>;\nexport async function decodeStorageWorkspaceCursor(\n configuration: Readonly<StorageWorkspaceCursorConfiguration> | undefined,\n binding: StorageWorkspaceCursorBinding,\n operation: StorageWorkspaceCursorData['operation'],\n token: string,\n): Promise<StorageWorkspaceCursorData> {\n if (configuration === undefined) {\n throw cursorNotConfigured();\n }\n if (\n typeof token !== 'string' ||\n token.length === 0 ||\n token.length >\n Math.min(\n STORAGE_WORKSPACE_MAX_CURSOR_BYTES,\n binding.limits.maxCursorBytes,\n ) ||\n !TOKEN_PATTERN.test(token)\n ) {\n throw invalidCursor();\n }\n\n let payload: Uint8Array;\n try {\n payload = await configuration.codec.decode(token);\n } catch (error) {\n throw decodeFailure(error);\n }\n if (\n !(payload instanceof Uint8Array) ||\n payload.byteLength === 0 ||\n payload.byteLength > MAX_CURSOR_PAYLOAD_BYTES\n ) {\n throw invalidCursor();\n }\n const wire = parseWireCursor(payload);\n const now = Date.now();\n if (\n wire.e <= now ||\n wire.o !== operation ||\n wire.b !== cursorBindingDigest(configuration, binding, operation)\n ) {\n throw invalidCursor();\n }\n if (\n (wire.o === 'list' && wire.q.l > binding.limits.maxPageSize) ||\n (wire.o === 'search' &&\n (wire.q.l > binding.limits.maxSearchResults ||\n wire.s.n > binding.limits.maxSearchScan))\n ) {\n throw invalidCursor();\n }\n return wire.o === 'list'\n ? {\n backendCursor: wire.s.c,\n directory: wire.q.d,\n limit: wire.q.l,\n operation: 'list',\n recursive: wire.q.r,\n }\n : {\n backendCursor: wire.s.c,\n caseInsensitive: wire.q.c,\n directory: wire.q.d,\n limit: wire.q.l,\n match: wire.q.m,\n operation: 'search',\n query: wire.q.q,\n scanned: wire.s.n,\n };\n}\n\nfunction parseWireCursor(payload: Uint8Array): WireCursor {\n let value: unknown;\n try {\n value = JSON.parse(decoder.decode(payload));\n } catch {\n throw invalidCursor();\n }\n if (\n !isRecord(value) ||\n !hasExactKeys(value, ['b', 'e', 'o', 'q', 's', 'v'])\n ) {\n throw invalidCursor();\n }\n if (\n value.v !== STORAGE_WORKSPACE_CURSOR_VERSION ||\n typeof value.b !== 'string' ||\n !BINDING_PATTERN.test(value.b) ||\n !positiveSafeInteger(value.e) ||\n !isRecord(value.q) ||\n !isRecord(value.s)\n ) {\n throw invalidCursor();\n }\n if (value.o === 'list') {\n if (\n !hasExactKeys(value.q, ['d', 'l', 'r']) ||\n !hasExactKeys(value.s, ['c']) ||\n typeof value.q.d !== 'string' ||\n !positiveSafeInteger(value.q.l) ||\n typeof value.q.r !== 'boolean' ||\n !nonEmptyString(value.s.c)\n ) {\n throw invalidCursor();\n }\n return {\n b: value.b,\n e: value.e,\n o: 'list',\n q: { d: value.q.d, l: value.q.l, r: value.q.r },\n s: { c: value.s.c },\n v: STORAGE_WORKSPACE_CURSOR_VERSION,\n };\n }\n if (value.o === 'search') {\n if (\n !hasExactKeys(value.q, ['c', 'd', 'l', 'm', 'q']) ||\n !hasExactKeys(value.s, ['c', 'n']) ||\n typeof value.q.c !== 'boolean' ||\n typeof value.q.d !== 'string' ||\n !positiveSafeInteger(value.q.l) ||\n !searchMatch(value.q.m) ||\n !nonEmptyString(value.q.q) ||\n !nonEmptyString(value.s.c) ||\n !nonNegativeSafeInteger(value.s.n)\n ) {\n throw invalidCursor();\n }\n return {\n b: value.b,\n e: value.e,\n o: 'search',\n q: {\n c: value.q.c,\n d: value.q.d,\n l: value.q.l,\n m: value.q.m,\n q: value.q.q,\n },\n s: { c: value.s.c, n: value.s.n },\n v: STORAGE_WORKSPACE_CURSOR_VERSION,\n };\n }\n throw invalidCursor();\n}\n\nfunction cursorBindingDigest(\n configuration: Readonly<StorageWorkspaceCursorConfiguration>,\n binding: StorageWorkspaceCursorBinding,\n operation: StorageWorkspaceCursorData['operation'],\n): string {\n const limits = binding.limits;\n const canonical = JSON.stringify({\n limits: {\n cursorTtlMs: limits.cursorTtlMs,\n maxCursorBytes: limits.maxCursorBytes,\n maxPageSize: limits.maxPageSize,\n maxPathBytes: limits.maxPathBytes,\n maxReadBytes: limits.maxReadBytes,\n maxSearchResults: limits.maxSearchResults,\n maxSearchScan: limits.maxSearchScan,\n maxWriteBytes: limits.maxWriteBytes,\n },\n mountId: configuration.mountId,\n operation,\n prefix: binding.prefix,\n scope: configuration.scope,\n store: binding.store,\n });\n return createHash('sha256').update(canonical, 'utf8').digest('base64url');\n}\n\nfunction issueFailure(error: unknown): Error {\n if (isStorageError(error)) {\n return workspaceError(error.code, 'Workspace cursor could not be issued.', {\n permanent: error.permanent,\n });\n }\n return workspaceError(\n StorageErrorCode.PROVIDER,\n 'Workspace cursor codec failed while issuing a token.',\n );\n}\n\nfunction decodeFailure(error: unknown): Error {\n if (isStorageError(error)) {\n if (\n error.code === StorageErrorCode.INVALID_ARGUMENT ||\n error.code === StorageErrorCode.NOT_FOUND\n ) {\n return invalidCursor();\n }\n return workspaceError(\n error.code,\n 'Workspace cursor codec failed while decoding a token.',\n { permanent: error.permanent },\n );\n }\n return workspaceError(\n StorageErrorCode.PROVIDER,\n 'Workspace cursor codec failed while decoding a token.',\n );\n}\n\nfunction cursorNotConfigured(): Error {\n return workspaceError(\n StorageErrorCode.NOT_SUPPORTED,\n 'Workspace pagination requires a configured cursor codec.',\n { permanent: true },\n );\n}\n\nfunction invalidCursor(): Error {\n return workspaceError(\n StorageErrorCode.INVALID_ARGUMENT,\n 'Workspace cursor is invalid, expired, or belongs to another query.',\n { permanent: true },\n );\n}\n\nfunction cursorAad(keyId: string): Buffer {\n return Buffer.from(\n `@nestm/storage/workspace-cursor\\0${TOKEN_VERSION}\\0${keyId}`,\n );\n}\n\nfunction validKeyId(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n TOKEN_PART_PATTERN.test(value) &&\n encoder.encode(value).byteLength <= MAX_CURSOR_KEY_ID_BYTES\n );\n}\n\nfunction assertStableIdentity(value: string, label: string): void {\n if (\n typeof value !== 'string' ||\n value.trim().length === 0 ||\n value !== value.normalize('NFC') ||\n forbiddenIdentityCharacter.test(value) ||\n encoder.encode(value).byteLength > MAX_CURSOR_SCOPE_BYTES\n ) {\n throw workspaceError(\n StorageErrorCode.INVALID_ARGUMENT,\n `${label} must be a non-empty stable NFC identity of at most ${MAX_CURSOR_SCOPE_BYTES} bytes.`,\n { permanent: true },\n );\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactKeys(\n value: Record<string, unknown>,\n expected: readonly string[],\n): boolean {\n const keys = Object.keys(value);\n return (\n keys.length === expected.length &&\n expected.every((key) => Object.hasOwn(value, key))\n );\n}\n\nfunction positiveSafeInteger(value: unknown): value is number {\n return Number.isSafeInteger(value) && (value as number) > 0;\n}\n\nfunction nonNegativeSafeInteger(value: unknown): value is number {\n return Number.isSafeInteger(value) && (value as number) >= 0;\n}\n\nfunction nonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\nfunction searchMatch(value: unknown): value is StorageWorkspaceSearchMatch {\n return value === 'glob' || value === 'substring' || value === 'exact';\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"storage-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/storage-workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAwBrD,OAAO,KAAK,EACV,4BAA4B,EAC5B,gBAAgB,IAAI,wBAAwB,EAgB7C,MAAM,8BAA8B,CAAC;AAukCtC,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,4BAA4B,GACpC,wBAAwB,CAiB1B;AAED,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,8BAAwB,CAAC"}
1
+ {"version":3,"file":"storage-workspace.d.ts","sourceRoot":"","sources":["../../src/workspace/storage-workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA+BrD,OAAO,KAAK,EACV,4BAA4B,EAC5B,gBAAgB,IAAI,wBAAwB,EAgB7C,MAAM,8BAA8B,CAAC;AAmlCtC,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,4BAA4B,GACpC,wBAAwB,CAgB1B;AAED,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,8BAAwB,CAAC"}