@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,115 @@
1
1
  # @nestm/storage
2
2
 
3
+ ## 0.1.0-alpha.8
4
+
5
+ ### Minor Changes
6
+
7
+ - d81c6f4: Add a typed `mapCreateConflict` hook to the AI SDK workspace adapter so
8
+ applications can represent atomic create collisions as domain results without
9
+ mutating generated tools. Keep replace/ETag conflicts fail-closed and sanitize
10
+ mapper failures at the tool boundary.
11
+
12
+ Mark workspace tools with optional inputs or a combined create/replace union as
13
+ non-strict for provider schema generation while retaining strict Zod runtime
14
+ validation.
15
+
16
+ - a0ea392: Add injectable, replica-safe workspace pagination cursors. The workspace now
17
+ binds versioned cursor payloads to stable store, mount, tenant/workspace, prefix,
18
+ operation, query, limit, and expiry context; authorizes non-consuming replay
19
+ before that expiry; and rejects altered or cross-context continuations.
20
+ Successful continuation still depends on the embedded provider cursor remaining
21
+ valid and available.
22
+
23
+ Export an AES-256-GCM key-ring codec for stateless multi-replica deployments and
24
+ an asynchronous byte-payload codec contract for shared durable opaque-token
25
+ stores. Cursor payloads and tokens are bounded, provider continuations remain
26
+ opaque, and pagination fails closed when no cursor mechanism is configured.
27
+ Compatible replicas rely on the universal driver contract for non-consuming,
28
+ instance-portable provider cursors whose position is independent of page size
29
+ while the provider token remains valid. Cursor expiry is not a provider-token
30
+ retention, snapshot-isolation, or uptime promise; provider invalidation remains
31
+ an operational list failure.
32
+
33
+ - d996b92: Split the aggregate S3 conditional-mutation and copy declarations into exact
34
+ create, replace, delete, read, source-copy, destination-copy, atomic-promotion,
35
+ and multipart-completion capabilities. Add independent AWS S3, Cloudflare R2,
36
+ and fail-closed custom-endpoint profiles that force unverified drivers
37
+ read-only; enforce complete physical-key byte budgets; normalize provider
38
+ errors without retaining raw provider payloads or causes; and publish a reusable
39
+ provider conformance contract with gated filesystem, AWS, R2, and custom suites.
40
+
41
+ Normalize every provider ETag to a canonical bare 1–1024-byte visible ASCII
42
+ token and serialize exactly one HTTP quote pair at S3 request boundaries.
43
+ Quoted, weak, wildcard, list-shaped, control-bearing, non-ASCII, and otherwise
44
+ unsafe values now fail closed instead of being accepted as arbitrary non-empty
45
+ strings. Applications that persisted quoted ETags must refresh them from
46
+ provider metadata before conditional mutation; this prevents wildcard/list and
47
+ header-ambiguity inputs from widening an exact-match precondition.
48
+
49
+ Specify and test provider list cursors as non-consuming replayable tokens for
50
+ unchanged provider state, so higher-level replayable pagination can fail closed
51
+ when a provider cannot meet that contract.
52
+
53
+ Exercise the complete advertised source-condition by destination-condition
54
+ promotion matrix, including stale-state preservation and competing stale/valid
55
+ requests. Provider documentation or audited implementation evidence remains
56
+ required for the internal one-linearization-point claim.
57
+
58
+ Ordinary and conditional provider failures now expose stable public messages
59
+ and preserve only normalized codes and retry flags; raw provider bodies,
60
+ request metadata, and nested causes are not retained in loggable error shapes.
61
+
62
+ Raise the `@aws-sdk/client-s3` peer floor to 3.919.0, the first release that
63
+ serializes destination conditions for `CopyObject`, and verify the real wire
64
+ headers in the packed minimum-peer consumer. Native AWS construction now
65
+ disables environment and shared-config endpoint URL overrides, while the public
66
+ capability helper derives custom-endpoint provenance from the actual SDK client
67
+ instead of a duplicated caller hint. Capability decoration is now single-use
68
+ per raw S3 adapter, preventing broader operations from surviving a later
69
+ narrower profile application.
70
+
71
+ Bind S3 provider authority to package-private raw-client and adapter-method
72
+ identity plus the exact surface snapshot installed by capability decoration.
73
+ Structurally S3-backed raw adapters are rejected until they pass through the
74
+ package helper, regardless of adapter name, proxying, or forged global symbols;
75
+ same-client aliases cannot replace their raw client, ordinary methods, policies,
76
+ or conditional operations. Unverified custom endpoints and noncanonical
77
+ S3-backed provider slugs are forced read-only, while an explicit branded profile
78
+ unlocks only its declared conditional operations. Endpoint and public-URL
79
+ provenance now follows the adapter actually produced by the provider loader,
80
+ including `configJson`.
81
+
82
+ Retain `publicBaseUrl` construction policy in the package-owned `s3()` helper
83
+ so omitted decorator hints cannot re-enable an expiring-download claim. Unknown
84
+ foreign S3 construction conservatively disables that claim. Validate the exact
85
+ physical adapter key without stripping leading slashes, and include configured
86
+ separators plus list/search-derived prefixes in the provider byte budget before
87
+ dispatch. Search uses files-sdk's own inferred glob-prefix and zero-result
88
+ semantics instead of duplicating its matcher logic. The innermost dispatch guard
89
+ repeats these checks after supported in-process plugins have transformed an
90
+ operation; adapters and plugins remain trusted code rather than a sandbox
91
+ boundary.
92
+
93
+ Derive signed-upload policy claims from the branded provider profile instead
94
+ of granting them to every S3-compatible endpoint. Native AWS proves content
95
+ type and POST size-range enforcement; Cloudflare R2 proves content type but not
96
+ POST form size ranges; omitted custom declarations normalize to false/false so
97
+ the gateway fails closed. Profile authority is backed by a package-private
98
+ WeakSet after deep freezing, so reflecting and copying the nominal brand symbol
99
+ cannot forge conformance evidence.
100
+
101
+ Treat the built-in AWS profile as an immutable ceiling for every SDK client
102
+ with native endpoint provenance, independent of mutable adapter display names.
103
+ Explicit profiles may narrow its operations, policy bits, and key limit but
104
+ cannot raise the 1,024-byte physical-key budget or add unsupported claims.
105
+
106
+ Enforce every requested signed-upload constraint at URL creation time.
107
+ Content-type-constrained PUT URLs now sign the `content-type` header; bounded
108
+ AWS uploads use exact POST MIME and byte-range conditions; unsupported profile
109
+ constraints and lower-only S3 ranges fail before signing. Bounded POST uploads
110
+ also reject physical keys ending in AWS's `${filename}` template so an exact
111
+ authorized key cannot be widened into a prefix policy.
112
+
3
113
  ## 0.1.0-alpha.7
4
114
 
5
115
  ### Minor Changes
package/README.md CHANGED
@@ -36,6 +36,14 @@ such as `files-sdk/gcs`:
36
36
  pnpm add files-sdk@2.2.3
37
37
  ```
38
38
 
39
+ Pass AWS-SDK-backed S3 adapters through the package-owned `s3()` and
40
+ `withS3Capabilities()` helpers (or use `createS3StorageDriver()`).
41
+ `createFilesSdkDriver()` rejects a structurally S3-backed raw adapter that has
42
+ not crossed this provenance boundary, even when a wrapper renames or proxies
43
+ the adapter. This prevents callers from bypassing endpoint and provider-profile
44
+ checks by omitting the capability decorator or replacing the public `raw`
45
+ property while retaining methods closed over the original client.
46
+
39
47
  Install only the native SDKs required by the chosen provider. For example:
40
48
 
41
49
  ```sh
@@ -50,6 +58,11 @@ pnpm add @google-cloud/storage google-auth-library
50
58
  pnpm add @azure/storage-blob @azure/core-auth @azure/identity
51
59
  ```
52
60
 
61
+ `@aws-sdk/client-s3` 3.919.0 or newer is required. Earlier releases omit
62
+ destination `If-Match` and `If-None-Match` from the serialized `CopyObject`
63
+ request even when those fields appear in a newer compile-time command shape;
64
+ the package peer range and packed minimum-peer smoke test enforce this floor.
65
+
53
66
  `files-sdk` currently declares its optional Nest peer for Nest 10 and 11. This
54
67
  library does not import `files-sdk/nestjs`; the Nest 12 integration is entirely
55
68
  owned here. A package manager may nevertheless report that temporary optional
@@ -110,12 +123,46 @@ control characters, repeated separators, and `.` or `..` segments are rejected
110
123
  rather than normalized. Keys and provider cursors returned by a driver are also
111
124
  checked before they are converted back to logical paths.
112
125
 
126
+ Pagination requires a server-owned cursor configuration. The built-in
127
+ `Aes256GcmStorageWorkspaceCursorCodec` produces versioned, authenticated,
128
+ encrypted tokens that can resume on another request, process, or replica when
129
+ every replica constructs an equivalent codec from the same key ring and uses
130
+ the same stable store identity, physical prefix, mount ID, trusted scope, and
131
+ effective limits. Use one codec instance per process, use a dedicated 32-byte
132
+ key, retain rotated decryption keys for at least one cursor TTL, and derive
133
+ `mountId` and `scope` only from authenticated server context.
134
+
135
+ The underlying driver must also implement the universal replayable list-cursor
136
+ contract against the same logical backend namespace: its cursor cannot be
137
+ consumed or tied to one driver instance. While the provider cursor remains
138
+ valid and available, an outer cursor can be retried until its authenticated
139
+ expiry. That expiry is only an authorization ceiling: it does not extend a
140
+ provider token's lifetime or promise snapshot isolation, provider availability,
141
+ network access, or valid credentials. Provider invalidation is an operational
142
+ list failure, and concurrent object changes remain subject to provider
143
+ continuation semantics. Without a codec, single-page operations still work but
144
+ a continuation fails closed.
145
+
113
146
  ```ts
114
- import { mountStorageWorkspace } from '@nestm/storage/workspace';
147
+ import {
148
+ Aes256GcmStorageWorkspaceCursorCodec,
149
+ mountStorageWorkspace,
150
+ } from '@nestm/storage/workspace';
151
+
152
+ // cursorKey is a separately validated 32-byte secret from deployment config.
153
+ const cursorCodec = new Aes256GcmStorageWorkspaceCursorCodec({
154
+ activeKeyId: 'v1',
155
+ keys: { v1: cursorKey },
156
+ });
115
157
 
116
158
  const workspace = mountStorageWorkspace(agentFiles, {
117
159
  // Use an opaque server-derived run id, never a value selected by the model.
118
160
  prefix: `workspaces/${runId}`,
161
+ cursor: {
162
+ codec: cursorCodec,
163
+ mountId: `agent-workspace:${runId}`,
164
+ scope: `organization:${organizationId}/workspace:${workspaceId}`,
165
+ },
119
166
  permissions: [
120
167
  'list',
121
168
  'read',
@@ -127,6 +174,8 @@ const workspace = mountStorageWorkspace(agentFiles, {
127
174
  'delete',
128
175
  ],
129
176
  limits: {
177
+ cursorTtlMs: 5 * 60 * 1000,
178
+ maxCursorBytes: 4096,
130
179
  maxReadBytes: 1024 * 1024,
131
180
  maxWriteBytes: 1024 * 1024,
132
181
  maxPageSize: 100,
@@ -220,6 +269,8 @@ import type { ToolSet } from 'ai';
220
269
  // A validated, opaque coordinate from trusted auth/run state.
221
270
  // It is never accepted from a prompt or tool input.
222
271
  prefix: context.storagePrefix,
272
+ // Includes the singleton codec plus stable mountId and scope.
273
+ cursor: context.cursorConfiguration,
223
274
  permissions: [
224
275
  'list',
225
276
  'read',
@@ -261,6 +312,24 @@ workspace capability remains the authorization boundary even when approval is
261
312
  disabled. The module's `AiSdkService.files()` API is the model provider's file
262
313
  upload facility and is unrelated to storage workspaces.
263
314
 
315
+ Atomic create collisions remain sanitized tool errors by default. Applications
316
+ that model an existing destination as a normal tool result can map that one
317
+ case while preserving replace/ETag conflicts as failures:
318
+
319
+ ```ts
320
+ const tools = createAiSdkWorkspaceTools({
321
+ workspace,
322
+ mapCreateConflict: ({ path }) => ({
323
+ kind: 'artifact-conflict' as const,
324
+ path,
325
+ status: 'already-exists' as const,
326
+ }),
327
+ });
328
+ ```
329
+
330
+ The mapper receives only the logical workspace path; provider errors, object
331
+ keys, and mount coordinates are never exposed.
332
+
264
333
  This logical confinement is sufficient for a `ToolLoopAgent` whose only file
265
334
  capabilities are these tools. It cannot constrain a coding harness that already
266
335
  has shell, `node:fs`, or subprocess access. For Codex/Claude-style harnesses,
@@ -463,19 +532,36 @@ listStorageProviderSecretEnvVars('s3').map((variable) => variable.key);
463
532
  The catalog is pure data and pulls in no adapter, so it is safe in config UIs,
464
533
  health checks, and startup validation.
465
534
 
466
- The `s3` slug additionally carries the conditional-promotion and signed-policy
467
- capabilities described under
468
- [Race-free staged-object promotion](#race-free-staged-object-promotion); every
469
- other provider exposes exactly what its adapter declares. When the provider _is_
470
- known at build time, import `@nestm/storage/files-sdk/s3` or
471
- `@nestm/storage/files-sdk/fs` directly and skip the indirection.
535
+ The `s3` slug additionally carries the verified per-operation profile and
536
+ signed-policy capabilities described under
537
+ [Exact provider conditions and staged-object promotion](#exact-provider-conditions-and-staged-object-promotion);
538
+ every provider not backed by the AWS S3 SDK exposes what its adapter declares.
539
+ When the provider _is_ known at build time, import
540
+ `@nestm/storage/files-sdk/s3` or `@nestm/storage/files-sdk/fs` directly and skip
541
+ the indirection.
542
+
543
+ S3 endpoint and public-URL provenance is resolved from the adapter that
544
+ `files-sdk` actually constructs, including values merged from `configJson`.
545
+ An unaudited endpoint forces the driver read-only, and a `publicBaseUrl` removes
546
+ the signed-download TTL guarantee because the resulting public URL does not
547
+ expire. AWS-SDK-backed noncanonical provider slugs (for example an S3-compatible
548
+ provider wrapper) also default to unverified/read-only; only the canonical
549
+ `s3` provider may infer the native AWS profile, and a custom endpoint becomes
550
+ writable only with an explicit branded `S3ProviderProfile`.
551
+
552
+ Before enabling conditional operations for a custom S3-compatible endpoint,
553
+ run the reusable
554
+ [provider conformance contract](https://github.com/nestm-dev/storage/blob/main/docs/provider-conformance.md)
555
+ against dedicated test credentials. Unknown endpoints are forced read-only and
556
+ receive no inferred conditional capabilities.
472
557
 
473
558
  ## Storage API
474
559
 
475
560
  `StorageClient` exposes:
476
561
 
477
562
  - `upload`, `downloadStream`, `head`, `exists`, `delete`, `copy`, and `move`;
478
- - conditional staged-object `promote` when the driver advertises it;
563
+ - exact `uploadConditional`, `downloadConditional`, `deleteConditional`, and
564
+ staged-object `promote` operations when the driver advertises each primitive;
479
565
  - `list`, cursor-aware `listAll`, and lazy `search`;
480
566
  - `signDownload` and discriminated PUT/POST `signUpload`;
481
567
  - `uploadMany`, `downloadMany`, `headMany`, `existsMany`, and `deleteMany`;
@@ -483,6 +569,24 @@ known at build time, import `@nestm/storage/files-sdk/s3` or
483
569
  - provider capability inspection; and
484
570
  - pause/resume/abort through `StorageUploadControl`.
485
571
 
572
+ Provider list cursors are opaque, non-consuming continuation tokens. Replaying
573
+ the same cursor and page limit against unchanged provider-visible state must
574
+ return an equivalent page and continuation position, even after a descendant
575
+ cursor has been used. A cursor is bound to the logical store, `prefix`, and
576
+ `delimiter`, but not to `limit`, retries, timeout, or abort signal; callers may
577
+ change those transport/page-size options while resuming the same position.
578
+
579
+ The cursor must work through a newly constructed compatible driver targeting
580
+ the same backend namespace while the provider token remains valid and
581
+ available; it cannot depend on process-, client-, or session-local state. An
582
+ adapter for a consuming or instance-bound provider token must materialize a
583
+ stable continuation before it can provide conforming paginated
584
+ `StorageDriver.list` results. This contract lets a caller safely retry, replay,
585
+ or resume pagination on another replica. It does not promise a provider-token
586
+ lifetime, snapshot isolation across concurrent mutations, or provider,
587
+ network, credential, or authorization availability. Provider invalidation is
588
+ an ordinary list-operation failure.
589
+
486
590
  Downloads are streaming by default:
487
591
 
488
592
  ```ts
@@ -505,11 +609,54 @@ Node `Readable` uploads are accepted and converted to Web streams without
505
609
  buffering. Provider capability gaps fail closed with `StorageError` rather than
506
610
  silently discarding a range, metadata, or cache-control request.
507
611
 
508
- ### Race-free staged-object promotion
509
-
510
- The S3 bridge advertises ETag- and version-conditional server-side copy. This
511
- lets an application validate a staged object and copy that exact source to its
512
- final key instead of re-reading whichever bytes occupy the staging key later:
612
+ ### Exact provider conditions and staged-object promotion
613
+
614
+ Capabilities distinguish conditional create, replace, delete, read, source
615
+ copy, destination copy, atomic source-and-destination promotion, and multipart
616
+ completion. They also declare the complete physical-key byte budget. Callers
617
+ must check the exact primitive they need; a missing field is unsupported and is
618
+ never widened from another operation.
619
+
620
+ The physical-key budget applies to the exact key sent to the adapter. It
621
+ therefore counts leading slashes for unprefixed drivers, the separator added to
622
+ a configured driver prefix, and provider prefixes derived for `list` or
623
+ `search`. Over-budget object keys and explicit list/search prefixes fail before
624
+ provider dispatch rather than being normalized into a shorter key. A glob's
625
+ provider prefix is the exact prefix inferred by files-sdk itself, and that
626
+ derived list operation passes through the same final guard. A non-positive
627
+ `maxResults` performs no provider walk and therefore dispatches no prefix.
628
+
629
+ Adapters and plugins execute as trusted in-process code; this package does not
630
+ attempt to sandbox a plugin that performs its own network or filesystem I/O.
631
+ For supported plugin pipelines that forward operations through `next`, the
632
+ physical-key guard is the innermost wrapper, after caller plugins and before
633
+ the adapter call. A plugin therefore cannot widen an upload key or list/search
634
+ prefix past the declared byte ceiling while still using the normal dispatch
635
+ pipeline.
636
+
637
+ Storage-facing ETags have one canonical representation: a bare, case-sensitive
638
+ opaque token with no HTTP quotes. Canonical values contain 1–1024 visible
639
+ ASCII bytes and exclude commas, backslashes, whitespace, control characters,
640
+ `DEL`, non-ASCII text, the `*` wildcard, and case-insensitive `W/` weak-tag
641
+ prefixes. Treat the value as opaque: preserve the exact string returned by
642
+ `head`, reads, or writes and pass it back unchanged to a conditional operation.
643
+ Do not add or remove quotes in application code. S3-compatible drivers remove
644
+ exactly one valid provider-owned quote pair on ingress and add exactly one pair
645
+ when serializing the HTTP header.
646
+
647
+ This tightens the precondition boundary. Quoted or otherwise non-canonical
648
+ ETags that older versions happened to accept now fail with `INVALID_ARGUMENT`,
649
+ and an unsafe provider result fails with a sanitized `PROVIDER` error instead
650
+ of being exposed as a usable validator. Applications that persisted quoted
651
+ ETags must refresh them with `head` rather than trimming them heuristically.
652
+ Strict normalization prevents a caller-controlled wildcard, entity-tag list,
653
+ weak validator, or malformed header value from widening an operation that
654
+ promises one exact strong match.
655
+
656
+ The native AWS S3 profile advertises ETag- and version-conditioned server-side
657
+ copy. This lets an application validate a staged object and copy that exact
658
+ source to its final key instead of re-reading whichever bytes occupy the
659
+ staging key later:
513
660
 
514
661
  ```ts
515
662
  import { StorageError, StorageErrorCode } from '@nestm/storage';
@@ -532,9 +679,58 @@ await media.delete(stagingKey);
532
679
  ```
533
680
 
534
681
  `sourceVersion` can select an immutable S3 version and may be combined with
535
- `sourceEtag`. A promotion without either identity is rejected. Drivers that do
536
- not publish `capabilities.conditionalCopy` fail with `NOT_SUPPORTED` rather
537
- than falling back to an unsafe ordinary copy.
682
+ `sourceEtag`. A destination condition can independently require create-only or
683
+ replacement of an exact ETag. Combining source and destination predicates also
684
+ requires `capabilities.conditionalCopyDestination.atomicWithSource`; otherwise
685
+ the request fails with `NOT_SUPPORTED`. A promotion must contain at least one
686
+ source or destination predicate.
687
+
688
+ Cloudflare R2 has a separate stable profile: create, replace, ETag-conditioned
689
+ read, and ETag-conditioned source copy are enabled, while conditional delete,
690
+ destination copy, atomic promotion, version predicates, and conditional
691
+ multipart completion remain absent. R2 proves content-type binding for
692
+ presigned PUT requests but not POST-form size ranges, so its signed-upload
693
+ policy is `{ contentType: true, sizeRange: false }` and the gateway refuses to
694
+ mint its POST upload form. Cloudflare documents that presigned `POST` form
695
+ uploads are not supported in its
696
+ [presigned URL contract](https://developers.cloudflare.com/r2/api/s3/presigned-urls/).
697
+ Direct R2 signed-upload calls that request a size bound likewise fail with
698
+ `NOT_SUPPORTED` before signing.
699
+ Custom S3-compatible endpoints start with no conditional operations and the
700
+ entire driver is forced read-only until an explicit conformance-verified
701
+ `S3ProviderProfile` is supplied. Omitting `signedUploadPolicy` while defining a
702
+ custom profile normalizes both policy claims to `false`; providers may opt in
703
+ only to constraints their conformance evidence proves.
704
+
705
+ Successful S3 signed uploads enforce every requested constraint. A request
706
+ with `contentType` and no `maxSize` uses a presigned PUT whose signature
707
+ includes the `content-type` header. A request with `maxSize` uses a POST policy
708
+ with `content-length-range` and, when present, an exact `Content-Type`
709
+ condition. S3 cannot express a lower-only `minSize` through this contract, so
710
+ that shape fails with `NOT_SUPPORTED`; an unclaimed profile constraint also
711
+ fails before credentials are resolved or a URL is minted. Literal physical
712
+ keys ending in AWS's `${filename}` POST template are rejected for bounded
713
+ uploads because the SDK otherwise widens the exact key condition to a prefix.
714
+
715
+ `withS3Capabilities()` decorates a raw S3 adapter in place and may be applied
716
+ only once. Construct a fresh raw adapter when selecting a different profile;
717
+ reapplying the helper is rejected so a previous broader profile cannot survive
718
+ a later narrower declaration. The selected profile is bound to the exact
719
+ reserved capability and operation members installed by that decoration;
720
+ same-client aliases may change display metadata but cannot add or replace those
721
+ members to widen the profile.
722
+
723
+ An explicit profile applied to a native AWS SDK endpoint may only narrow the
724
+ immutable `AWS_S3_PROVIDER_PROFILE`. It cannot raise the complete-key budget
725
+ above 1,024 bytes or claim an operation/policy bit absent from the built-in
726
+ profile. This containment follows actual SDK endpoint provenance even when an
727
+ adapter alias changes its display name.
728
+
729
+ The package-owned `s3()` factory also retains whether `publicBaseUrl` was
730
+ configured even when the second `withS3Capabilities()` options object is
731
+ omitted. In that case `signedDownloadPolicy.expiresIn` is false. Foreign S3
732
+ adapters whose construction metadata is unavailable receive the same
733
+ conservative false value instead of claiming an enforceable TTL.
538
734
 
539
735
  ### Resumable uploads
540
736
 
@@ -676,8 +872,11 @@ by `maxSignedUploadBytes`, and require an exact lowercase MIME type from
676
872
  `attachment` or `inline` response disposition; arbitrary response-header text
677
873
  and filenames are rejected. A driver must also advertise
678
874
  `signedUploadPolicy.contentType` and `signedUploadPolicy.sizeRange`; otherwise
679
- the gateway refuses to mint the URL. `createS3StorageDriver()` advertises both
680
- and uses S3 POST policy conditions. Signed downloads similarly require
875
+ the gateway refuses to mint the URL. Native AWS advertises both and uses S3
876
+ POST policy conditions. R2 advertises content-type enforcement but not a POST
877
+ size range, while omitted custom declarations normalize both claims to false;
878
+ the gateway therefore fails closed for those profiles. Signed downloads
879
+ similarly require
681
880
  `signedDownloadPolicy.expiresIn`. The S3 factory advertises it only when no
682
881
  permanent `publicBaseUrl` was configured, preventing a configured TTL from
683
882
  silently returning a non-expiring public link.
package/SECURITY.md CHANGED
@@ -56,11 +56,40 @@ untrusted agent code. A tenant id, run id, prefix, provider cursor, snapshot id,
56
56
  or fork id supplied by a model is not a safe mount coordinate.
57
57
 
58
58
  The workspace accepts only canonical mount-relative POSIX paths and rechecks
59
- every key returned by a driver before unscoping it. Its cursors are opaque and
60
- bound to the mount and query. Permissions, byte limits, result limits, and
61
- conditional mutation preconditions are enforced inside the capability; tool
62
- omission and user approval are additional workflow controls, not the
63
- authorization boundary.
59
+ every key returned by a driver before unscoping it. Pagination cursors are bound
60
+ to the store, normalized physical prefix, stable mount identity, trusted
61
+ tenant/workspace scope, operation, complete effective limits, normalized query,
62
+ and expiry. Provider continuations and prefixes stay inside the encrypted or
63
+ server-side payload. Both layers are non-consuming: a durable opaque token
64
+ store must read rather than consume a record, and an embedded provider cursor
65
+ must remain replayable through a fresh compatible driver against the same
66
+ backend namespace while that provider cursor remains valid and available.
67
+ Provider cursors cannot depend on process-local state; an adapter for a
68
+ consuming or instance-bound backend token must materialize a stable continuation
69
+ before exposing paginated `StorageDriver.list` results.
70
+
71
+ Under unchanged provider-visible state, cursors are reusable while their
72
+ provider continuation remains valid and available. The authenticated expiry is
73
+ an authorization ceiling, not a guarantee of provider-token lifetime, snapshot
74
+ isolation, provider/network availability, or valid credentials. Provider
75
+ invalidation remains an operational failure, and concurrent mutations remain
76
+ subject to provider ordering, duplicate, and omission semantics.
77
+
78
+ Production pagination must configure either the built-in AES-256-GCM codec with
79
+ a dedicated shared 32-byte key ring or an authenticated shared durable token
80
+ store. Do not reuse an authentication, session, storage-provider, or encryption
81
+ key from another purpose. All replicas must use the same stable identities and
82
+ key ring, compatible driver configuration, and logical backend namespace. New
83
+ cursors use the active key id; retain prior keys for at least the maximum cursor
84
+ TTL during rotation. Dropping a key, changing a binding field, or restarting
85
+ with a different ephemeral key intentionally invalidates outstanding cursors.
86
+ Tokens and decoded payloads are bounded, and malformed, altered, expired,
87
+ cross-query, cross-operation, cross-workspace, and cross-store cursors fail
88
+ closed.
89
+
90
+ Permissions, byte limits, result limits, and conditional mutation preconditions
91
+ are enforced inside the capability; tool omission and user approval are
92
+ additional workflow controls, not the authorization boundary.
64
93
 
65
94
  Conditional mutations can still have an ambiguous outcome when a remote
66
95
  provider commits and then loses or violates its response, or when a configured
@@ -1,4 +1,4 @@
1
- import { type ToolSet } from 'ai';
1
+ import { type JSONValue, type ToolSet } from 'ai';
2
2
  import { type StorageWorkspace } from '../workspace/index.js';
3
3
  import { type StorageErrorCode as StorageErrorCodeValue } from '../storage.error.js';
4
4
  export declare const AI_SDK_WORKSPACE_TOOL_NAMES: readonly ['workspace_list', 'workspace_stat', 'workspace_read_file', 'workspace_search', 'workspace_write_file', 'workspace_copy_file', 'workspace_move_file', 'workspace_delete_file'];
@@ -9,7 +9,12 @@ export type AiSdkWorkspaceMutationToolName = Extract<AiSdkWorkspaceToolName, 'wo
9
9
  * approval, matching an omitted policy.
10
10
  */
11
11
  export type AiSdkWorkspaceApprovalConfig = boolean | Partial<Record<AiSdkWorkspaceMutationToolName, boolean>>;
12
- export interface CreateAiSdkWorkspaceToolsOptions {
12
+ export interface AiSdkWorkspaceCreateConflict {
13
+ /** The logical destination inside the mounted workspace. */
14
+ readonly path: string;
15
+ }
16
+ export type AiSdkWorkspaceCreateConflictMapper<Result extends JSONValue> = (conflict: AiSdkWorkspaceCreateConflict) => PromiseLike<Result> | Result;
17
+ export interface CreateAiSdkWorkspaceToolsOptions<CreateConflictResult extends JSONValue = never> {
13
18
  /** The already-mounted, policy-enforcing workspace exposed to the tools. */
14
19
  workspace: StorageWorkspace;
15
20
  /**
@@ -19,6 +24,12 @@ export interface CreateAiSdkWorkspaceToolsOptions {
19
24
  maxReadBytes?: number;
20
25
  /** Mutation tools require approval by default. */
21
26
  requireApproval?: AiSdkWorkspaceApprovalConfig;
27
+ /**
28
+ * Maps an atomic create collision to an application result. When omitted,
29
+ * the collision remains an AiSdkWorkspaceToolError like every other storage
30
+ * failure. Replace conflicts are never mapped by this hook.
31
+ */
32
+ mapCreateConflict?: AiSdkWorkspaceCreateConflictMapper<CreateConflictResult>;
22
33
  }
23
34
  export type AiSdkWorkspaceToolErrorCode = StorageErrorCodeValue;
24
35
  /**
@@ -58,6 +69,6 @@ export interface AiSdkWorkspacePageResult {
58
69
  * The workspace remains the enforcing boundary if a retained tool reference
59
70
  * is invoked after further narrowing.
60
71
  */
61
- export declare function createAiSdkWorkspaceTools({ workspace, maxReadBytes: requestedMaxReadBytes, requireApproval, }: CreateAiSdkWorkspaceToolsOptions): ToolSet;
72
+ export declare function createAiSdkWorkspaceTools<CreateConflictResult extends JSONValue = never>({ workspace, maxReadBytes: requestedMaxReadBytes, requireApproval, mapCreateConflict, }: CreateAiSdkWorkspaceToolsOptions<CreateConflictResult>): ToolSet;
62
73
  export declare function isAiSdkWorkspaceMutationToolName(value: string): value is AiSdkWorkspaceMutationToolName;
63
74
  //# sourceMappingURL=ai-sdk-workspace-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ai-sdk-workspace-tools.d.ts","sourceRoot":"","sources":["../../src/ai-sdk/ai-sdk-workspace-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAGxC,OAAO,EAEL,KAAK,gBAAgB,EAItB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAGL,KAAK,gBAAgB,IAAI,qBAAqB,EAC/C,MAAM,qBAAqB,CAAC;AAE7B,eAAO,MAAM,2BAA2B,YACtC,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,CACf,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/C,MAAM,MAAM,8BAA8B,GAAG,OAAO,CAClD,sBAAsB,EACpB,sBAAsB,GACtB,qBAAqB,GACrB,qBAAqB,GACrB,uBAAuB,CAC1B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,4BAA4B,GACtC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,8BAA8B,EAAE,OAAO,CAAC,CAAC,CAAC;AAErE,MAAM,WAAW,gCAAgC;IAC/C,4EAA4E;IAC5E,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,eAAe,CAAC,EAAE,4BAA4B,CAAC;CAChD;AAED,MAAM,MAAM,2BAA2B,GAAG,qBAAqB,CAAC;AAsBhE;;;GAGG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAE3C,YAAY,IAAI,EAAE,2BAA2B,EAI5C;CACF;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,yBAAyB,GACnC,wBAAwB,GAAG,6BAA6B,CAAC;AAE3D,MAAM,WAAW,4BAA6B,SAAQ,wBAAwB;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,yBAAyB,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA2MD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,SAAS,EACT,YAAY,EAAE,qBAAqB,EACnC,eAAsB,GACvB,EAAE,gCAAgC,GAAG,OAAO,CA2R5C;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,8BAA8B,CAEzC"}
1
+ {"version":3,"file":"ai-sdk-workspace-tools.d.ts","sourceRoot":"","sources":["../../src/ai-sdk/ai-sdk-workspace-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAGxD,OAAO,EAGL,KAAK,gBAAgB,EAItB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAGL,KAAK,gBAAgB,IAAI,qBAAqB,EAC/C,MAAM,qBAAqB,CAAC;AAG7B,eAAO,MAAM,2BAA2B,YACtC,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,CACf,CAAC;AAEX,MAAM,MAAM,sBAAsB,GAChC,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/C,MAAM,MAAM,8BAA8B,GAAG,OAAO,CAClD,sBAAsB,EACpB,sBAAsB,GACtB,qBAAqB,GACrB,qBAAqB,GACrB,uBAAuB,CAC1B,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,4BAA4B,GACtC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,8BAA8B,EAAE,OAAO,CAAC,CAAC,CAAC;AAErE,MAAM,WAAW,4BAA4B;IAC3C,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,kCAAkC,CAAC,MAAM,SAAS,SAAS,IAAI,CACzE,QAAQ,EAAE,4BAA4B,KACnC,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;AAElC,MAAM,WAAW,gCAAgC,CAC/C,oBAAoB,SAAS,SAAS,GAAG,KAAK;IAE9C,4EAA4E;IAC5E,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,eAAe,CAAC,EAAE,4BAA4B,CAAC;IAC/C;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,kCAAkC,CAAC,oBAAoB,CAAC,CAAC;CAC9E;AAED,MAAM,MAAM,2BAA2B,GAAG,qBAAqB,CAAC;AAsBhE;;;GAGG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAE3C,YAAY,IAAI,EAAE,2BAA2B,EAI5C;CACF;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,yBAAyB,GACnC,wBAAwB,GAAG,6BAA6B,CAAC;AAE3D,MAAM,WAAW,4BAA6B,SAAQ,wBAAwB;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,yBAAyB,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA2OD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,oBAAoB,SAAS,SAAS,GAAG,KAAK,EAC9C,EACA,SAAS,EACT,YAAY,EAAE,qBAAqB,EACnC,eAAsB,EACtB,iBAAiB,GAClB,EAAE,gCAAgC,CAAC,oBAAoB,CAAC,GAAG,OAAO,CA6SlE;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,8BAA8B,CAEzC"}