@noy-db/to-aws-s3 0.2.0-pre.12 → 0.2.0-pre.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -111,8 +111,15 @@ function s3Bundle(options) {
111
111
  }
112
112
 
113
113
  // src/index.ts
114
+ function isPreconditionFailed(err) {
115
+ if (!(err instanceof Error)) return false;
116
+ if (err.name === "PreconditionFailed") return true;
117
+ const meta = err.$metadata;
118
+ return meta?.httpStatusCode === 412;
119
+ }
114
120
  function s3(options) {
115
121
  const { bucket, prefix = "" } = options;
122
+ const clockUncertaintyMs = options.clockUncertaintyMs ?? 5e3;
116
123
  const client = options.client ?? new import_client_s32.S3Client({
117
124
  ...options.region ? { region: options.region } : {}
118
125
  });
@@ -127,8 +134,28 @@ function s3(options) {
127
134
  function compPrefix(vault) {
128
135
  return prefix ? `${prefix}/${vault}/` : `${vault}/`;
129
136
  }
137
+ const clockKey = prefix ? `${prefix}/_noydb-clock` : "_noydb-clock";
130
138
  return {
131
139
  name: "s3",
140
+ capabilities: {
141
+ casAtomic: true,
142
+ serverWriteTime: true,
143
+ auth: { kind: "iam", required: true, flow: "static" }
144
+ },
145
+ async getStoreTime() {
146
+ await client.send(new import_client_s32.PutObjectCommand({
147
+ Bucket: bucket,
148
+ Key: clockKey,
149
+ Body: "",
150
+ ContentType: "text/plain"
151
+ }));
152
+ const head = await client.send(new import_client_s32.HeadObjectCommand({
153
+ Bucket: bucket,
154
+ Key: clockKey
155
+ }));
156
+ const serverMs = head.LastModified.getTime();
157
+ return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs };
158
+ },
132
159
  async get(vault, collection, id) {
133
160
  try {
134
161
  const result = await client.send(new import_client_s32.GetObjectCommand({
@@ -146,15 +173,76 @@ function s3(options) {
146
173
  }
147
174
  },
148
175
  async put(vault, collection, id, envelope, expectedVersion) {
176
+ const key = objectKey(vault, collection, id);
149
177
  if (expectedVersion !== void 0) {
150
- const existing = await this.get(vault, collection, id);
151
- if (existing && existing._v !== expectedVersion) {
152
- throw new import_hub2.ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`);
178
+ if (expectedVersion === 0) {
179
+ try {
180
+ await client.send(new import_client_s32.PutObjectCommand({
181
+ Bucket: bucket,
182
+ Key: key,
183
+ Body: JSON.stringify(envelope),
184
+ ContentType: "application/json",
185
+ IfNoneMatch: "*"
186
+ }));
187
+ } catch (err) {
188
+ if (isPreconditionFailed(err)) {
189
+ try {
190
+ const r = await client.send(new import_client_s32.GetObjectCommand({ Bucket: bucket, Key: key }));
191
+ const b = await r.Body.transformToString();
192
+ const cur = JSON.parse(b);
193
+ throw new import_hub2.ConflictError(cur._v, "Concurrent create: object already exists");
194
+ } catch (inner) {
195
+ if (inner instanceof import_hub2.ConflictError) throw inner;
196
+ }
197
+ throw new import_hub2.ConflictError(0, "Concurrent create: object already exists");
198
+ }
199
+ throw err;
200
+ }
201
+ return;
202
+ }
203
+ let currentETag;
204
+ try {
205
+ const result = await client.send(new import_client_s32.GetObjectCommand({ Bucket: bucket, Key: key }));
206
+ const body = await result.Body.transformToString();
207
+ const current = JSON.parse(body);
208
+ if (current._v !== expectedVersion) {
209
+ throw new import_hub2.ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`);
210
+ }
211
+ currentETag = result.ETag ?? "";
212
+ } catch (err) {
213
+ if (err instanceof import_hub2.ConflictError) throw err;
214
+ if (err instanceof Error && (err.name === "NoSuchKey" || err.name === "NotFound")) {
215
+ throw new import_hub2.ConflictError(0, `Object not found, expected version ${expectedVersion}`);
216
+ }
217
+ throw err;
218
+ }
219
+ try {
220
+ await client.send(new import_client_s32.PutObjectCommand({
221
+ Bucket: bucket,
222
+ Key: key,
223
+ Body: JSON.stringify(envelope),
224
+ ContentType: "application/json",
225
+ IfMatch: currentETag
226
+ }));
227
+ } catch (err) {
228
+ if (isPreconditionFailed(err)) {
229
+ try {
230
+ const r = await client.send(new import_client_s32.GetObjectCommand({ Bucket: bucket, Key: key }));
231
+ const b = await r.Body.transformToString();
232
+ const latest = JSON.parse(b);
233
+ throw new import_hub2.ConflictError(latest._v, "Concurrent write detected");
234
+ } catch (inner) {
235
+ if (inner instanceof import_hub2.ConflictError) throw inner;
236
+ }
237
+ throw new import_hub2.ConflictError(0, "Concurrent write detected");
238
+ }
239
+ throw err;
153
240
  }
241
+ return;
154
242
  }
155
243
  await client.send(new import_client_s32.PutObjectCommand({
156
244
  Bucket: bucket,
157
- Key: objectKey(vault, collection, id),
245
+ Key: key,
158
246
  Body: JSON.stringify(envelope),
159
247
  ContentType: "application/json"
160
248
  }));
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["import_hub","import_client_s3"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n HeadObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),\n * enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel\n * object's `LastModified` timestamp — store-authoritative, not client wall clock.\n * ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n /** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */\n clockUncertaintyMs?: number\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nfunction isPreconditionFailed(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n if (err.name === 'PreconditionFailed') return true\n const meta = (err as { $metadata?: { httpStatusCode?: number } }).$metadata\n return meta?.httpStatusCode === 412\n}\n\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n const clockUncertaintyMs = options.clockUncertaintyMs ?? 5_000\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n // Sentinel key used solely to sample the store's server clock.\n const clockKey = prefix ? `${prefix}/_noydb-clock` : '_noydb-clock'\n\n return {\n name: 's3',\n capabilities: {\n casAtomic: true,\n serverWriteTime: true,\n auth: { kind: 'iam', required: true, flow: 'static' },\n },\n\n async getStoreTime() {\n // Write a sentinel object so S3 assigns a server-authoritative LastModified.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n Body: '',\n ContentType: 'text/plain',\n }))\n const head = await client.send(new HeadObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n }))\n const serverMs = head.LastModified!.getTime()\n return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs }\n },\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const key = objectKey(vault, collection, id)\n\n if (expectedVersion !== undefined) {\n if (expectedVersion === 0) {\n // Create-only — IfNoneMatch: '*' is atomic at S3's write layer.\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfNoneMatch: '*',\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const cur = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(cur._v, 'Concurrent create: object already exists')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent create: object already exists')\n }\n throw err\n }\n return\n }\n\n // Update — GetObject captures ETag + verifies _v, then PutObject with\n // IfMatch: etag ensures no concurrent writer slipped in between.\n let currentETag: string\n try {\n const result = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const body = await result.Body!.transformToString()\n const current = JSON.parse(body) as EncryptedEnvelope\n if (current._v !== expectedVersion) {\n throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`)\n }\n currentETag = result.ETag ?? ''\n } catch (err: unknown) {\n if (err instanceof ConflictError) throw err\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n throw new ConflictError(0, `Object not found, expected version ${expectedVersion}`)\n }\n throw err\n }\n\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfMatch: currentETag,\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const latest = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(latest._v, 'Concurrent write detected')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent write detected')\n }\n throw err\n }\n return\n }\n\n // Unconditional PUT.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["import_hub","import_client_s3"]}
package/dist/index.d.cts CHANGED
@@ -50,9 +50,6 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
50
50
  *
51
51
  * ## Limitations
52
52
  *
53
- * - **`casAtomic: false`** — S3 has no server-side conditional write on
54
- * arbitrary metadata. Concurrent puts may result in last-write-wins.
55
- * Use DynamoDB for records that need conflict-safe writes.
56
53
  * - **`loadAll()` is O(N) requests** — listing + fetching every object in a
57
54
  * vault. Suitable for vaults up to ~10K records; beyond that, prefer
58
55
  * DynamoDB for indexed stores and S3 only for append-heavy blob storage.
@@ -76,8 +73,10 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
76
73
  * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only
77
74
  * holds archived records.
78
75
  *
79
- * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins
80
- * on concurrent puts.
76
+ * S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),
77
+ * enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel
78
+ * object's `LastModified` timestamp — store-authoritative, not client wall clock.
79
+ * ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).
81
80
  */
82
81
  interface S3Options {
83
82
  /** S3 bucket name. */
@@ -92,11 +91,9 @@ interface S3Options {
92
91
  * to share a client across adapters or supply custom middleware.
93
92
  */
94
93
  client?: S3Client;
94
+ /** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */
95
+ clockUncertaintyMs?: number;
95
96
  }
96
- /**
97
- * Create an S3 adapter.
98
- * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`
99
- */
100
97
  declare function s3(options: S3Options): NoydbStore;
101
98
 
102
99
  export { type S3BundleOptions, type S3Options, s3, s3Bundle };
package/dist/index.d.ts CHANGED
@@ -50,9 +50,6 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
50
50
  *
51
51
  * ## Limitations
52
52
  *
53
- * - **`casAtomic: false`** — S3 has no server-side conditional write on
54
- * arbitrary metadata. Concurrent puts may result in last-write-wins.
55
- * Use DynamoDB for records that need conflict-safe writes.
56
53
  * - **`loadAll()` is O(N) requests** — listing + fetching every object in a
57
54
  * vault. Suitable for vaults up to ~10K records; beyond that, prefer
58
55
  * DynamoDB for indexed stores and S3 only for append-heavy blob storage.
@@ -76,8 +73,10 @@ declare function s3Bundle(options: S3BundleOptions): NoydbBundleStore;
76
73
  * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only
77
74
  * holds archived records.
78
75
  *
79
- * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins
80
- * on concurrent puts.
76
+ * S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),
77
+ * enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel
78
+ * object's `LastModified` timestamp — store-authoritative, not client wall clock.
79
+ * ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).
81
80
  */
82
81
  interface S3Options {
83
82
  /** S3 bucket name. */
@@ -92,11 +91,9 @@ interface S3Options {
92
91
  * to share a client across adapters or supply custom middleware.
93
92
  */
94
93
  client?: S3Client;
94
+ /** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */
95
+ clockUncertaintyMs?: number;
95
96
  }
96
- /**
97
- * Create an S3 adapter.
98
- * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`
99
- */
100
97
  declare function s3(options: S3Options): NoydbStore;
101
98
 
102
99
  export { type S3BundleOptions, type S3Options, s3, s3Bundle };
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  S3Client as S3Client2,
5
5
  GetObjectCommand as GetObjectCommand2,
6
6
  PutObjectCommand as PutObjectCommand2,
7
+ HeadObjectCommand as HeadObjectCommand2,
7
8
  DeleteObjectCommand as DeleteObjectCommand2,
8
9
  ListObjectsV2Command as ListObjectsV2Command2,
9
10
  HeadBucketCommand
@@ -100,8 +101,15 @@ function s3Bundle(options) {
100
101
  }
101
102
 
102
103
  // src/index.ts
104
+ function isPreconditionFailed(err) {
105
+ if (!(err instanceof Error)) return false;
106
+ if (err.name === "PreconditionFailed") return true;
107
+ const meta = err.$metadata;
108
+ return meta?.httpStatusCode === 412;
109
+ }
103
110
  function s3(options) {
104
111
  const { bucket, prefix = "" } = options;
112
+ const clockUncertaintyMs = options.clockUncertaintyMs ?? 5e3;
105
113
  const client = options.client ?? new S3Client2({
106
114
  ...options.region ? { region: options.region } : {}
107
115
  });
@@ -116,8 +124,28 @@ function s3(options) {
116
124
  function compPrefix(vault) {
117
125
  return prefix ? `${prefix}/${vault}/` : `${vault}/`;
118
126
  }
127
+ const clockKey = prefix ? `${prefix}/_noydb-clock` : "_noydb-clock";
119
128
  return {
120
129
  name: "s3",
130
+ capabilities: {
131
+ casAtomic: true,
132
+ serverWriteTime: true,
133
+ auth: { kind: "iam", required: true, flow: "static" }
134
+ },
135
+ async getStoreTime() {
136
+ await client.send(new PutObjectCommand2({
137
+ Bucket: bucket,
138
+ Key: clockKey,
139
+ Body: "",
140
+ ContentType: "text/plain"
141
+ }));
142
+ const head = await client.send(new HeadObjectCommand2({
143
+ Bucket: bucket,
144
+ Key: clockKey
145
+ }));
146
+ const serverMs = head.LastModified.getTime();
147
+ return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs };
148
+ },
121
149
  async get(vault, collection, id) {
122
150
  try {
123
151
  const result = await client.send(new GetObjectCommand2({
@@ -135,15 +163,76 @@ function s3(options) {
135
163
  }
136
164
  },
137
165
  async put(vault, collection, id, envelope, expectedVersion) {
166
+ const key = objectKey(vault, collection, id);
138
167
  if (expectedVersion !== void 0) {
139
- const existing = await this.get(vault, collection, id);
140
- if (existing && existing._v !== expectedVersion) {
141
- throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`);
168
+ if (expectedVersion === 0) {
169
+ try {
170
+ await client.send(new PutObjectCommand2({
171
+ Bucket: bucket,
172
+ Key: key,
173
+ Body: JSON.stringify(envelope),
174
+ ContentType: "application/json",
175
+ IfNoneMatch: "*"
176
+ }));
177
+ } catch (err) {
178
+ if (isPreconditionFailed(err)) {
179
+ try {
180
+ const r = await client.send(new GetObjectCommand2({ Bucket: bucket, Key: key }));
181
+ const b = await r.Body.transformToString();
182
+ const cur = JSON.parse(b);
183
+ throw new ConflictError(cur._v, "Concurrent create: object already exists");
184
+ } catch (inner) {
185
+ if (inner instanceof ConflictError) throw inner;
186
+ }
187
+ throw new ConflictError(0, "Concurrent create: object already exists");
188
+ }
189
+ throw err;
190
+ }
191
+ return;
192
+ }
193
+ let currentETag;
194
+ try {
195
+ const result = await client.send(new GetObjectCommand2({ Bucket: bucket, Key: key }));
196
+ const body = await result.Body.transformToString();
197
+ const current = JSON.parse(body);
198
+ if (current._v !== expectedVersion) {
199
+ throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`);
200
+ }
201
+ currentETag = result.ETag ?? "";
202
+ } catch (err) {
203
+ if (err instanceof ConflictError) throw err;
204
+ if (err instanceof Error && (err.name === "NoSuchKey" || err.name === "NotFound")) {
205
+ throw new ConflictError(0, `Object not found, expected version ${expectedVersion}`);
206
+ }
207
+ throw err;
208
+ }
209
+ try {
210
+ await client.send(new PutObjectCommand2({
211
+ Bucket: bucket,
212
+ Key: key,
213
+ Body: JSON.stringify(envelope),
214
+ ContentType: "application/json",
215
+ IfMatch: currentETag
216
+ }));
217
+ } catch (err) {
218
+ if (isPreconditionFailed(err)) {
219
+ try {
220
+ const r = await client.send(new GetObjectCommand2({ Bucket: bucket, Key: key }));
221
+ const b = await r.Body.transformToString();
222
+ const latest = JSON.parse(b);
223
+ throw new ConflictError(latest._v, "Concurrent write detected");
224
+ } catch (inner) {
225
+ if (inner instanceof ConflictError) throw inner;
226
+ }
227
+ throw new ConflictError(0, "Concurrent write detected");
228
+ }
229
+ throw err;
142
230
  }
231
+ return;
143
232
  }
144
233
  await client.send(new PutObjectCommand2({
145
234
  Bucket: bucket,
146
- Key: objectKey(vault, collection, id),
235
+ Key: key,
147
236
  Body: JSON.stringify(envelope),
148
237
  ContentType: "application/json"
149
238
  }));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`casAtomic: false`** — S3 has no server-side conditional write on\n * arbitrary metadata. Concurrent puts may result in last-write-wins.\n * Use DynamoDB for records that need conflict-safe writes.\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * Note: S3 does not support atomic CAS (`casAtomic: false`). Last-write-wins\n * on concurrent puts.\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n return {\n name: 's3',\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n if (expectedVersion !== undefined) {\n const existing = await this.get(vault, collection, id)\n if (existing && existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["S3Client","GetObjectCommand","PutObjectCommand","DeleteObjectCommand","ListObjectsV2Command","S3Client","GetObjectCommand","PutObjectCommand","DeleteObjectCommand","ListObjectsV2Command"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/bundle.ts"],"sourcesContent":["/**\n * **@noy-db/to-aws-s3** — S3 object store for NOYDB.\n *\n * Each record is stored as a JSON object at\n * `{prefix}/{vault}/{collection}/{id}.json`. The `loadAll()` method uses\n * `ListObjectsV2` to enumerate keys then fetches them in parallel.\n *\n * ## When to use\n *\n * - **Blob / attachment storage** — pair with `@noy-db/to-aws-dynamo` via\n * `routeStore({ default: dynamo(...), blobs: s3(...) })` to route\n * encrypted binary chunks to S3.\n * - **Archive tier** — configure `routeStore` age-based tiering so old\n * records migrate to S3 while hot records stay in DynamoDB.\n * - **Large vaults** — S3 has no item size limit, unlike DynamoDB's 400 KB cap.\n *\n * ## Limitations\n *\n * - **`loadAll()` is O(N) requests** — listing + fetching every object in a\n * vault. Suitable for vaults up to ~10K records; beyond that, prefer\n * DynamoDB for indexed stores and S3 only for append-heavy blob storage.\n *\n * ## IAM minimum permissions\n *\n * ```json\n * { \"Action\": [\"s3:GetObject\", \"s3:PutObject\", \"s3:DeleteObject\",\n * \"s3:ListBucket\"] }\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbStore, EncryptedEnvelope, VaultSnapshot } from '@noy-db/hub'\nimport { ConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n HeadObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadBucketCommand,\n} from '@aws-sdk/client-s3'\n\n/**\n * Options for `s3()`.\n *\n * Objects are stored at `{prefix}/{vault}/{collection}/{id}.json`.\n * `loadAll()` uses `ListObjectsV2` over the vault prefix followed by parallel\n * `GetObject` calls — suitable for vaults with up to ~10K records. For larger\n * vaults, use DynamoDB or pair with `routeStore` age-tiering so S3 only\n * holds archived records.\n *\n * S3 supports conditional writes (`IfMatch` / `IfNoneMatch` on `PutObject`),\n * enabling atomic CAS (`casAtomic: true`). Server clock is read via a sentinel\n * object's `LastModified` timestamp — store-authoritative, not client wall clock.\n * ε defaults to 5 000 ms (S3 is NTP-synced; observed skew bound).\n */\nexport interface S3Options {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default: ''. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default: 'us-east-1'. */\n region?: string\n /**\n * Pre-built S3Client from `@aws-sdk/client-s3`. If provided, the adapter\n * uses this client directly and ignores `region`. Useful for apps that want\n * to share a client across adapters or supply custom middleware.\n */\n client?: S3Client\n /** Clock uncertainty bound for serverWriteTime (ms). Default: 5000. */\n clockUncertaintyMs?: number\n}\n\n/**\n * Create an S3 adapter.\n * Key scheme: `{prefix}/{vault}/{collection}/{id}.json`\n */\nfunction isPreconditionFailed(err: unknown): boolean {\n if (!(err instanceof Error)) return false\n if (err.name === 'PreconditionFailed') return true\n const meta = (err as { $metadata?: { httpStatusCode?: number } }).$metadata\n return meta?.httpStatusCode === 412\n}\n\nexport function s3(options: S3Options): NoydbStore {\n const { bucket, prefix = '' } = options\n const clockUncertaintyMs = options.clockUncertaintyMs ?? 5_000\n\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n function objectKey(vault: string, collection: string, id: string): string {\n const parts = [vault, collection, `${id}.json`]\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function collPrefix(vault: string, collection: string): string {\n const parts = [vault, collection, '']\n return prefix ? `${prefix}/${parts.join('/')}` : parts.join('/')\n }\n\n function compPrefix(vault: string): string {\n return prefix ? `${prefix}/${vault}/` : `${vault}/`\n }\n\n // Sentinel key used solely to sample the store's server clock.\n const clockKey = prefix ? `${prefix}/_noydb-clock` : '_noydb-clock'\n\n return {\n name: 's3',\n capabilities: {\n casAtomic: true,\n serverWriteTime: true,\n auth: { kind: 'iam', required: true, flow: 'static' },\n },\n\n async getStoreTime() {\n // Write a sentinel object so S3 assigns a server-authoritative LastModified.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n Body: '',\n ContentType: 'text/plain',\n }))\n const head = await client.send(new HeadObjectCommand({\n Bucket: bucket,\n Key: clockKey,\n }))\n const serverMs = head.LastModified!.getTime()\n return { earliest: serverMs - clockUncertaintyMs, latest: serverMs + clockUncertaintyMs }\n },\n\n async get(vault, collection, id) {\n try {\n const result = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n\n if (!result.Body) return null\n const body = await result.Body.transformToString()\n return JSON.parse(body) as EncryptedEnvelope\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n return null\n }\n throw err\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const key = objectKey(vault, collection, id)\n\n if (expectedVersion !== undefined) {\n if (expectedVersion === 0) {\n // Create-only — IfNoneMatch: '*' is atomic at S3's write layer.\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfNoneMatch: '*',\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const cur = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(cur._v, 'Concurrent create: object already exists')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent create: object already exists')\n }\n throw err\n }\n return\n }\n\n // Update — GetObject captures ETag + verifies _v, then PutObject with\n // IfMatch: etag ensures no concurrent writer slipped in between.\n let currentETag: string\n try {\n const result = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const body = await result.Body!.transformToString()\n const current = JSON.parse(body) as EncryptedEnvelope\n if (current._v !== expectedVersion) {\n throw new ConflictError(current._v, `Version conflict: expected ${expectedVersion}, found ${current._v}`)\n }\n currentETag = result.ETag ?? ''\n } catch (err: unknown) {\n if (err instanceof ConflictError) throw err\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) {\n throw new ConflictError(0, `Object not found, expected version ${expectedVersion}`)\n }\n throw err\n }\n\n try {\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n IfMatch: currentETag,\n }))\n } catch (err: unknown) {\n if (isPreconditionFailed(err)) {\n try {\n const r = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }))\n const b = await r.Body!.transformToString()\n const latest = JSON.parse(b) as EncryptedEnvelope\n throw new ConflictError(latest._v, 'Concurrent write detected')\n } catch (inner: unknown) {\n if (inner instanceof ConflictError) throw inner\n }\n throw new ConflictError(0, 'Concurrent write detected')\n }\n throw err\n }\n return\n }\n\n // Unconditional PUT.\n await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: JSON.stringify(envelope),\n ContentType: 'application/json',\n }))\n },\n\n async delete(vault, collection, id) {\n await client.send(new DeleteObjectCommand({\n Bucket: bucket,\n Key: objectKey(vault, collection, id),\n }))\n },\n\n async list(vault, collection) {\n const pfx = collPrefix(vault, collection)\n const result = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n return (result.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n .map(k => k.slice(pfx.length, -5))\n },\n\n async loadAll(vault) {\n const pfx = compPrefix(vault)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n }))\n\n const snapshot: VaultSnapshot = {}\n\n for (const obj of listResult.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith('.json')) continue\n\n const relativePath = key.slice(pfx.length)\n const parts = relativePath.split('/')\n if (parts.length !== 2) continue\n\n const collection = parts[0]!\n const id = parts[1]!.slice(0, -5)\n if (collection.startsWith('_')) continue\n\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n\n if (!getResult.Body) continue\n const body = await getResult.Body.transformToString()\n\n if (!snapshot[collection]) snapshot[collection] = {}\n snapshot[collection][id] = JSON.parse(body) as EncryptedEnvelope\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collection, records] of Object.entries(data)) {\n for (const [id, envelope] of Object.entries(records)) {\n await this.put(vault, collection, id, envelope)\n }\n }\n },\n\n async ping() {\n try {\n await client.send(new HeadBucketCommand({ Bucket: bucket }))\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Paginate over a collection using S3's native `ContinuationToken`.\n *\n * Each page does:\n * 1. ListObjectsV2 with MaxKeys = limit and the previous token\n * 2. GetObject for every key on the page (in parallel)\n *\n * The 2-step pattern is necessary because S3 list responses don't\n * include object bodies. For very large collections this is N+1 — but\n * the parallel GETs amortize well, and consumers willing to pay for\n * stronger pagination should use a different adapter (Dynamo).\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const pfx = collPrefix(vault, collection)\n const listResult = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: pfx,\n MaxKeys: limit,\n ...(cursor ? { ContinuationToken: cursor } : {}),\n }))\n\n const keys = (listResult.Contents ?? [])\n .map(obj => obj.Key ?? '')\n .filter(k => k.endsWith('.json'))\n\n // Fetch every body in parallel — bounded by `limit` so we never\n // fan out beyond the page size.\n const items = await Promise.all(keys.map(async (key) => {\n const id = key.slice(pfx.length, -5)\n const getResult = await client.send(new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n }))\n if (!getResult.Body) return null\n const body = await getResult.Body.transformToString()\n return { id, envelope: JSON.parse(body) as EncryptedEnvelope }\n }))\n\n return {\n items: items.filter((x): x is { id: string; envelope: EncryptedEnvelope } => x !== null),\n nextCursor: listResult.IsTruncated && listResult.NextContinuationToken\n ? listResult.NextContinuationToken\n : null,\n }\n },\n }\n}\n\nexport { s3Bundle } from './bundle.js'\nexport type { S3BundleOptions } from './bundle.js'\n","/**\n * **s3Bundle** — whole-vault bundle store for noy-db over Amazon S3.\n *\n * Implements the `NoydbBundleStore` contract (read/write/delete/list of whole\n * `.noydb` blobs) with optimistic concurrency via S3 conditional writes. Pairs\n * with `@noy-db/hub` snapshots (`withSnapshots({ store: s3Bundle(...) })`) and\n * with bundle-mode sync.\n *\n * Key scheme: `{prefix}/{vaultId}.noydb`. The version token is the object ETag.\n *\n * **OCC:** `writeBundle(id, bytes, expectedVersion)` —\n * - `expectedVersion === null` → unconditional `PutObject` (first write / rolling overwrite).\n * - `expectedVersion = <etag>` → `PutObject` with `IfMatch`; a 412 becomes `BundleVersionConflictError`.\n *\n * Requires `@aws-sdk/client-s3` ≥ 3.696 (conditional-write `IfMatch` on PutObject, GA Nov 2024).\n *\n * @packageDocumentation\n */\nimport type { NoydbBundleStore } from '@noy-db/hub'\nimport { BundleVersionConflictError } from '@noy-db/hub'\nimport {\n S3Client,\n GetObjectCommand,\n PutObjectCommand,\n DeleteObjectCommand,\n ListObjectsV2Command,\n HeadObjectCommand,\n} from '@aws-sdk/client-s3'\n\nexport interface S3BundleOptions {\n /** S3 bucket name. */\n bucket: string\n /** Key prefix within the bucket. Default ''. Keys are `{prefix}/{vaultId}.noydb`. */\n prefix?: string\n /** AWS region. Used only when `client` is not provided. Default 'us-east-1'. */\n region?: string\n /** Pre-built S3Client. If provided, `region` is ignored. */\n client?: S3Client\n}\n\nconst SUFFIX = '.noydb'\n\nfunction stripQuotes(etag: string | undefined): string {\n return (etag ?? '').replace(/^\"|\"$/g, '')\n}\n\nexport function s3Bundle(options: S3BundleOptions): NoydbBundleStore {\n const { bucket, prefix = '' } = options\n const client = options.client ?? new S3Client({\n ...(options.region ? { region: options.region } : {}),\n })\n\n const listPrefix = prefix ? `${prefix}/` : ''\n function objectKey(vaultId: string): string {\n return `${listPrefix}${vaultId}${SUFFIX}`\n }\n\n return {\n kind: 'bundle',\n name: 's3',\n\n async readBundle(vaultId) {\n try {\n const res = await client.send(new GetObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n if (!res.Body) return null\n const bytes = await (res.Body as { transformToByteArray: () => Promise<Uint8Array> }).transformToByteArray()\n return { bytes, version: stripQuotes(res.ETag) }\n } catch (err: unknown) {\n if (err instanceof Error && (err.name === 'NoSuchKey' || err.name === 'NotFound')) return null\n throw err\n }\n },\n\n async writeBundle(vaultId, bytes, expectedVersion) {\n try {\n const res = await client.send(new PutObjectCommand({\n Bucket: bucket,\n Key: objectKey(vaultId),\n Body: bytes,\n ContentType: 'application/octet-stream',\n ...(expectedVersion !== null ? { IfMatch: expectedVersion } : {}),\n }))\n let version = stripQuotes(res.ETag)\n if (!version) {\n const head = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n version = stripQuotes(head.ETag)\n }\n return { version }\n } catch (err: unknown) {\n const status = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode\n if (err instanceof Error && (err.name === 'PreconditionFailed' || status === 412)) {\n throw new BundleVersionConflictError(\n `S3 bundle \"${vaultId}\" changed since expectedVersion=\"${expectedVersion}\".`,\n )\n }\n throw err\n }\n },\n\n async deleteBundle(vaultId) {\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey(vaultId) }))\n },\n\n async listBundles() {\n const out: Array<{ vaultId: string; version: string; size: number }> = []\n let token: string | undefined\n do {\n const res = await client.send(new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: listPrefix,\n ...(token ? { ContinuationToken: token } : {}),\n }))\n for (const obj of res.Contents ?? []) {\n const key = obj.Key ?? ''\n if (!key.endsWith(SUFFIX)) continue\n out.push({\n vaultId: key.slice(listPrefix.length, -SUFFIX.length),\n version: stripQuotes(obj.ETag),\n size: obj.Size ?? 0,\n })\n }\n token = res.IsTruncated ? res.NextContinuationToken : undefined\n } while (token)\n return out\n },\n }\n}\n"],"mappings":"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","names":["S3Client","GetObjectCommand","PutObjectCommand","HeadObjectCommand","DeleteObjectCommand","ListObjectsV2Command","S3Client","PutObjectCommand","HeadObjectCommand","GetObjectCommand","DeleteObjectCommand","ListObjectsV2Command"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/to-aws-s3",
3
- "version": "0.2.0-pre.12",
3
+ "version": "0.2.0-pre.14",
4
4
  "description": "AWS S3 adapter for noy-db — encrypted object storage with zero-knowledge cloud sync",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -40,11 +40,11 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@aws-sdk/client-s3": "^3.0.0",
43
- "@noy-db/hub": "0.2.0-pre.12"
43
+ "@noy-db/hub": "0.2.0-pre.14"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@aws-sdk/client-s3": "^3.0.0",
47
- "@noy-db/hub": "0.2.0-pre.12"
47
+ "@noy-db/hub": "0.2.0-pre.14"
48
48
  },
49
49
  "keywords": [
50
50
  "noy-db",