@noy-db/as-blob 0.2.0-pre.9 → 0.3.0-pre.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noy-db/as-blob",
3
- "version": "0.2.0-pre.9",
3
+ "version": "0.3.0-pre.1",
4
4
  "description": "Single-attachment plaintext export for noy-db — extracts one blob from BlobSet as native MIME bytes. Gated by the RFC #249 `canExportPlaintext` capability bit; writes an audit-ledger entry on every call. Part of the @noy-db/as-* portable-artefact family (plaintext tier, document sub-family).",
5
5
  "license": "MIT",
6
6
  "author": "vLannaAi <vicio@lanna.ai>",
@@ -17,17 +17,10 @@
17
17
  "sideEffects": false,
18
18
  "exports": {
19
19
  ".": {
20
- "import": {
21
- "types": "./dist/index.d.ts",
22
- "default": "./dist/index.js"
23
- },
24
- "require": {
25
- "types": "./dist/index.d.cts",
26
- "default": "./dist/index.cjs"
27
- }
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
28
22
  }
29
23
  },
30
- "main": "./dist/index.cjs",
31
24
  "module": "./dist/index.js",
32
25
  "types": "./dist/index.d.ts",
33
26
  "files": [
@@ -36,15 +29,15 @@
36
29
  "LICENSE"
37
30
  ],
38
31
  "engines": {
39
- "node": ">=18.0.0"
32
+ "node": ">=22.0.0"
40
33
  },
41
34
  "peerDependencies": {
42
- "@noy-db/hub": "0.2.0-pre.9"
35
+ "@noy-db/hub": "0.3.0-pre.1"
43
36
  },
44
37
  "devDependencies": {
45
38
  "@types/node": "^22.0.0",
46
- "@noy-db/hub": "0.2.0-pre.9",
47
- "@noy-db/to-memory": "0.2.0-pre.9"
39
+ "@noy-db/hub": "0.3.0-pre.1",
40
+ "@noy-db/to-memory": "0.3.0-pre.1"
48
41
  },
49
42
  "keywords": [
50
43
  "noy-db",
package/dist/index.cjs DELETED
@@ -1,140 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- AsBlobNotFoundError: () => AsBlobNotFoundError,
34
- download: () => download,
35
- fromBytes: () => fromBytes,
36
- toBytes: () => toBytes,
37
- write: () => write
38
- });
39
- module.exports = __toCommonJS(index_exports);
40
- var AsBlobNotFoundError = class extends Error {
41
- collection;
42
- id;
43
- slot;
44
- constructor(collection, id, slot) {
45
- super(
46
- `as-blob: no blob at ${collection}/${id}[slot=${slot}]. Record may not exist, slot may be unset, or the caller's ACL may not include this collection.`
47
- );
48
- this.name = "AsBlobNotFoundError";
49
- this.collection = collection;
50
- this.id = id;
51
- this.slot = slot;
52
- }
53
- };
54
- var DEFAULT_SLOT = "raw";
55
- async function toBytes(vault, options) {
56
- vault.assertCanExport("plaintext", "blob");
57
- const slotName = options.slot ?? DEFAULT_SLOT;
58
- const collection = vault.collection(options.collection);
59
- const blobSet = collection.blob(options.id);
60
- const slots = await blobSet.list();
61
- const slot = slots.find((s) => s.name === slotName);
62
- if (!slot) {
63
- throw new AsBlobNotFoundError(options.collection, options.id, slotName);
64
- }
65
- const bytes = await blobSet.get(slotName);
66
- if (!bytes) {
67
- throw new AsBlobNotFoundError(options.collection, options.id, slotName);
68
- }
69
- return {
70
- bytes,
71
- mime: slot.mimeType ?? "application/octet-stream",
72
- filename: slot.filename ?? `${slotName}.bin`
73
- };
74
- }
75
- async function download(vault, options) {
76
- const { bytes, mime, filename } = await toBytes(vault, options);
77
- const finalName = options.filename ?? filename;
78
- const blob = new Blob([bytes], { type: mime });
79
- const url = URL.createObjectURL(blob);
80
- const a = document.createElement("a");
81
- a.href = url;
82
- a.download = finalName;
83
- a.click();
84
- URL.revokeObjectURL(url);
85
- }
86
- async function write(vault, path, options) {
87
- if (options.acknowledgeRisks !== true) {
88
- throw new Error(
89
- `as-blob.write: acknowledgeRisks: true is required for on-disk plaintext output. This call creates a persistent plaintext copy of the blob outside noy-db's encrypted storage \u2014 see docs/patterns/as-exports.md \xA7"The three tiers of \\"plaintext out\\""`
90
- );
91
- }
92
- const { bytes } = await toBytes(vault, options);
93
- const { writeFile } = await import("fs/promises");
94
- await writeFile(path, bytes);
95
- }
96
- async function fromBytes(vault, bytes, options) {
97
- vault.assertCanImport("plaintext", "blob");
98
- const slotName = options.slot ?? DEFAULT_SLOT;
99
- const policy = options.policy ?? "replace";
100
- const collection = vault.collection(options.collection);
101
- const record = await collection.get(options.id);
102
- if (record === null) {
103
- throw new Error(
104
- `as-blob.fromBytes: record ${options.collection}/${options.id} not found. The target record must exist before attaching a blob.`
105
- );
106
- }
107
- const blobSet = collection.blob(options.id);
108
- const slots = await blobSet.list();
109
- const existing = slots.find((s) => s.name === slotName);
110
- const status = existing ? "modified" : "added";
111
- if (policy === "insert-only" && existing) {
112
- throw new Error(
113
- `as-blob.fromBytes: insert-only refused \u2014 slot ${slotName} on ${options.collection}/${options.id} is already populated. Pass \`policy: 'replace'\` to overwrite.`
114
- );
115
- }
116
- return {
117
- status,
118
- ...existing?.eTag !== void 0 ? { priorETag: existing.eTag } : {},
119
- collection: options.collection,
120
- id: options.id,
121
- slot: slotName,
122
- bytes: bytes.byteLength,
123
- policy,
124
- async apply() {
125
- const writeOpts = {};
126
- if (options.mimeType !== void 0) writeOpts.mimeType = options.mimeType;
127
- if (options.filename !== void 0) writeOpts.filename = options.filename;
128
- await blobSet.put(slotName, bytes, writeOpts);
129
- }
130
- };
131
- }
132
- // Annotate the CommonJS export names for ESM import in node:
133
- 0 && (module.exports = {
134
- AsBlobNotFoundError,
135
- download,
136
- fromBytes,
137
- toBytes,
138
- write
139
- });
140
- //# sourceMappingURL=index.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/as-blob** — single-attachment plaintext export for noy-db.\n *\n * Extracts one blob from a record's `BlobSet` as its native MIME\n * bytes. Part of the `@noy-db/as-*` portable-artefact family —\n * plaintext tier, document sub-family. Handles the \"download this\n * PDF\" / \"export this scan\" primitive that a record-formatter\n * (`as-csv`, `as-xlsx`) can't cover — structured data vs binary\n * bytes are different shapes with the same egress gate.\n *\n * **Authorization.** Every call is gated by the invoking keyring's\n * `canExportPlaintext` capability with format `'blob'`.\n * Decrypted bytes crossing the plaintext boundary require an\n * explicit grant from the vault owner; the package calls\n * `vault.assertCanExport('plaintext', 'blob')` before loading any\n * chunks from the store.\n *\n * **Scope.** One blob per call. Multi-record / multi-blob archive →\n * use `@noy-db/as-zip`.\n *\n * See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).\n *\n * @packageDocumentation\n */\n\nimport type { Vault } from '@noy-db/hub'\n\n/** Inputs shared by every `as-blob` entry point. */\nexport interface AsBlobOptions {\n /** Collection the record lives in. Must be in the caller's read ACL. */\n readonly collection: string\n /** Record id. */\n readonly id: string\n /**\n * Slot name within the record's `BlobSet`. Defaults to `'raw'` —\n * the convention used by scan/attachment workflows where each\n * record has a single primary blob.\n */\n readonly slot?: string\n}\n\n/** Options for `download()` — adds an optional filename override. */\nexport interface AsBlobDownloadOptions extends AsBlobOptions {\n /**\n * Filename offered to the browser download prompt. When omitted,\n * falls back to the `SlotRecord.filename` stored at upload time,\n * then to `<slotName>.bin`.\n */\n readonly filename?: string\n}\n\n/** Options for `write()` — requires explicit risk acknowledgement. */\nexport interface AsBlobWriteOptions extends AsBlobOptions {\n /**\n * Required for Node file-write calls — consumer acknowledgement\n * that plaintext bytes will persist on disk past the current\n * process lifetime (Tier 3 risk per `docs/patterns/as-exports.md`).\n */\n readonly acknowledgeRisks: true\n}\n\n/** Return shape from `toBytes()` — raw bytes plus the inferred MIME + filename. */\nexport interface AsBlobResult {\n /** Decrypted blob bytes in their native MIME form. */\n readonly bytes: Uint8Array\n /** MIME type, or `'application/octet-stream'` if none was stored. */\n readonly mime: string\n /** Filename stored at upload time, or `<slotName>.bin`. */\n readonly filename: string\n}\n\n/**\n * Thrown when the requested record or slot isn't found in the vault.\n * Distinct from the hub's `NotFoundError` so callers can match the\n * export-path specifically.\n */\nexport class AsBlobNotFoundError extends Error {\n readonly collection: string\n readonly id: string\n readonly slot: string\n\n constructor(collection: string, id: string, slot: string) {\n super(\n `as-blob: no blob at ${collection}/${id}[slot=${slot}]. Record may not ` +\n `exist, slot may be unset, or the caller's ACL may not include this collection.`,\n )\n this.name = 'AsBlobNotFoundError'\n this.collection = collection\n this.id = id\n this.slot = slot\n }\n}\n\nconst DEFAULT_SLOT = 'raw'\n\n/**\n * Decrypt and return the blob bytes plus MIME + filename metadata.\n * Pure — no I/O beyond the authorization check, slot lookup, and\n * chunk fetch. The caller decides where the bytes go.\n */\nexport async function toBytes(\n vault: Vault,\n options: AsBlobOptions,\n): Promise<AsBlobResult> {\n vault.assertCanExport('plaintext', 'blob')\n\n const slotName = options.slot ?? DEFAULT_SLOT\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const collection = vault.collection<any>(options.collection)\n const blobSet = collection.blob(options.id)\n\n // Look up slot metadata FIRST so we can surface a precise error\n // without triggering an extra chunk fetch for the missing case.\n const slots = await blobSet.list()\n const slot = slots.find((s) => s.name === slotName)\n if (!slot) {\n throw new AsBlobNotFoundError(options.collection, options.id, slotName)\n }\n\n const bytes = await blobSet.get(slotName)\n if (!bytes) {\n throw new AsBlobNotFoundError(options.collection, options.id, slotName)\n }\n\n return {\n bytes,\n mime: slot.mimeType ?? 'application/octet-stream',\n filename: slot.filename ?? `${slotName}.bin`,\n }\n}\n\n/**\n * Browser download — wraps `toBytes()` in a `Blob` and triggers the\n * browser's download prompt. Tier 2 egress per the pattern doc:\n * plaintext bytes leave memory, but the lifetime is scoped to the\n * user's chosen destination (usually the OS download folder).\n *\n * Requires a browser-like environment with `URL.createObjectURL`\n * and `document.createElement`. Throws on headless runtimes — use\n * `toBytes()` there and pipe the bytes wherever you need.\n */\nexport async function download(\n vault: Vault,\n options: AsBlobDownloadOptions,\n): Promise<void> {\n const { bytes, mime, filename } = await toBytes(vault, options)\n const finalName = options.filename ?? filename\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const blob = new Blob([bytes as any], { type: mime })\n const url = URL.createObjectURL(blob)\n const a = document.createElement('a')\n a.href = url\n a.download = finalName\n a.click()\n URL.revokeObjectURL(url)\n}\n\n/**\n * Node file-write — persists the blob bytes to the filesystem.\n * Requires explicit `acknowledgeRisks: true` because the plaintext\n * file outlives the current process (Tier 3 egress). The\n * authorization check still runs even when the consumer\n * acknowledges — capability bits aren't bypassable.\n */\nexport async function write(\n vault: Vault,\n path: string,\n options: AsBlobWriteOptions,\n): Promise<void> {\n if (options.acknowledgeRisks !== true) {\n throw new Error(\n 'as-blob.write: acknowledgeRisks: true is required for on-disk plaintext output. ' +\n 'This call creates a persistent plaintext copy of the blob outside noy-db\\'s ' +\n 'encrypted storage — see docs/patterns/as-exports.md §\"The three tiers of \\\\\"plaintext out\\\\\"\"',\n )\n }\n const { bytes } = await toBytes(vault, options)\n // Defer the node:fs import so this package remains browser-safe.\n const { writeFile } = await import('node:fs/promises')\n await writeFile(path, bytes)\n}\n\n// ─── Reader ──────────────────────────────────\n\n/**\n * `merge` has no meaning for a single-slot write — only `replace`\n * (overwrite if present) and `insert-only` (refuse if present) make\n * sense. Default is `replace`.\n */\nexport type AsBlobImportPolicy = 'replace' | 'insert-only'\n\nexport interface AsBlobImportOptions {\n /** Collection the target record lives in. */\n readonly collection: string\n /** Record id. The record must already exist. */\n readonly id: string\n /** Slot name. Defaults to `'raw'` (matches `toBytes`). */\n readonly slot?: string\n /** Default `'replace'`. */\n readonly policy?: AsBlobImportPolicy\n /**\n * Optional MIME hint stored on the slot. When omitted the blob layer\n * sniffs the magic bytes — same fallback as a direct `blob.put()`.\n */\n readonly mimeType?: string\n /** Optional filename stored on the slot. */\n readonly filename?: string\n}\n\nexport interface AsBlobImportPlan {\n /** Whether the slot was empty (`'added'`) or already populated (`'modified'`). */\n readonly status: 'added' | 'modified'\n /** Pre-existing eTag when `status === 'modified'`, otherwise undefined. */\n readonly priorETag?: string\n readonly collection: string\n readonly id: string\n readonly slot: string\n /** Plaintext byte count of the incoming blob. */\n readonly bytes: number\n readonly policy: AsBlobImportPolicy\n apply(): Promise<void>\n}\n\n/**\n * Build an import plan for a single attachment. Returns a plan that\n * surfaces whether the target slot exists today (`status: 'added' |\n * 'modified'`); calling `apply()` performs the write.\n *\n * Authorization: gated by `assertCanImport('plaintext', 'blob')`.\n *\n * Errors:\n * - `ImportCapabilityError` — keyring lacks the import capability.\n * - `Error('as-blob.fromBytes: record ... not found')` — the target\n * record id does not exist in the collection. We refuse to attach\n * to a phantom record because the slot index would orphan.\n * - `Error('as-blob.fromBytes: insert-only ...')` — `policy:\n * 'insert-only'` and the slot is already populated.\n */\nexport async function fromBytes(\n vault: Vault,\n bytes: Uint8Array,\n options: AsBlobImportOptions,\n): Promise<AsBlobImportPlan> {\n vault.assertCanImport('plaintext', 'blob')\n\n const slotName = options.slot ?? DEFAULT_SLOT\n const policy: AsBlobImportPolicy = options.policy ?? 'replace'\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const collection = vault.collection<any>(options.collection)\n\n // Refuse to attach to a non-existent record — would leave an orphan\n // slot entry pointing at no record.\n const record = await collection.get(options.id)\n if (record === null) {\n throw new Error(\n `as-blob.fromBytes: record ${options.collection}/${options.id} not found. ` +\n 'The target record must exist before attaching a blob.',\n )\n }\n\n const blobSet = collection.blob(options.id)\n const slots = await blobSet.list()\n const existing = slots.find((s) => s.name === slotName)\n const status: 'added' | 'modified' = existing ? 'modified' : 'added'\n\n if (policy === 'insert-only' && existing) {\n throw new Error(\n `as-blob.fromBytes: insert-only refused — slot ${slotName} on ` +\n `${options.collection}/${options.id} is already populated. ` +\n \"Pass `policy: 'replace'` to overwrite.\",\n )\n }\n\n return {\n status,\n ...(existing?.eTag !== undefined ? { priorETag: existing.eTag } : {}),\n collection: options.collection,\n id: options.id,\n slot: slotName,\n bytes: bytes.byteLength,\n policy,\n async apply(): Promise<void> {\n const writeOpts: { mimeType?: string; filename?: string } = {}\n if (options.mimeType !== undefined) writeOpts.mimeType = options.mimeType\n if (options.filename !== undefined) writeOpts.filename = options.filename\n await blobSet.put(slotName, bytes, writeOpts)\n },\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts DELETED
@@ -1,157 +0,0 @@
1
- import { Vault } from '@noy-db/hub';
2
-
3
- /**
4
- * **@noy-db/as-blob** — single-attachment plaintext export for noy-db.
5
- *
6
- * Extracts one blob from a record's `BlobSet` as its native MIME
7
- * bytes. Part of the `@noy-db/as-*` portable-artefact family —
8
- * plaintext tier, document sub-family. Handles the "download this
9
- * PDF" / "export this scan" primitive that a record-formatter
10
- * (`as-csv`, `as-xlsx`) can't cover — structured data vs binary
11
- * bytes are different shapes with the same egress gate.
12
- *
13
- * **Authorization.** Every call is gated by the invoking keyring's
14
- * `canExportPlaintext` capability with format `'blob'`.
15
- * Decrypted bytes crossing the plaintext boundary require an
16
- * explicit grant from the vault owner; the package calls
17
- * `vault.assertCanExport('plaintext', 'blob')` before loading any
18
- * chunks from the store.
19
- *
20
- * **Scope.** One blob per call. Multi-record / multi-blob archive →
21
- * use `@noy-db/as-zip`.
22
- *
23
- * See [`docs/patterns/as-exports.md`](https://github.com/vLannaAi/noy-db/blob/main/docs/patterns/as-exports.md).
24
- *
25
- * @packageDocumentation
26
- */
27
-
28
- /** Inputs shared by every `as-blob` entry point. */
29
- interface AsBlobOptions {
30
- /** Collection the record lives in. Must be in the caller's read ACL. */
31
- readonly collection: string;
32
- /** Record id. */
33
- readonly id: string;
34
- /**
35
- * Slot name within the record's `BlobSet`. Defaults to `'raw'` —
36
- * the convention used by scan/attachment workflows where each
37
- * record has a single primary blob.
38
- */
39
- readonly slot?: string;
40
- }
41
- /** Options for `download()` — adds an optional filename override. */
42
- interface AsBlobDownloadOptions extends AsBlobOptions {
43
- /**
44
- * Filename offered to the browser download prompt. When omitted,
45
- * falls back to the `SlotRecord.filename` stored at upload time,
46
- * then to `<slotName>.bin`.
47
- */
48
- readonly filename?: string;
49
- }
50
- /** Options for `write()` — requires explicit risk acknowledgement. */
51
- interface AsBlobWriteOptions extends AsBlobOptions {
52
- /**
53
- * Required for Node file-write calls — consumer acknowledgement
54
- * that plaintext bytes will persist on disk past the current
55
- * process lifetime (Tier 3 risk per `docs/patterns/as-exports.md`).
56
- */
57
- readonly acknowledgeRisks: true;
58
- }
59
- /** Return shape from `toBytes()` — raw bytes plus the inferred MIME + filename. */
60
- interface AsBlobResult {
61
- /** Decrypted blob bytes in their native MIME form. */
62
- readonly bytes: Uint8Array;
63
- /** MIME type, or `'application/octet-stream'` if none was stored. */
64
- readonly mime: string;
65
- /** Filename stored at upload time, or `<slotName>.bin`. */
66
- readonly filename: string;
67
- }
68
- /**
69
- * Thrown when the requested record or slot isn't found in the vault.
70
- * Distinct from the hub's `NotFoundError` so callers can match the
71
- * export-path specifically.
72
- */
73
- declare class AsBlobNotFoundError extends Error {
74
- readonly collection: string;
75
- readonly id: string;
76
- readonly slot: string;
77
- constructor(collection: string, id: string, slot: string);
78
- }
79
- /**
80
- * Decrypt and return the blob bytes plus MIME + filename metadata.
81
- * Pure — no I/O beyond the authorization check, slot lookup, and
82
- * chunk fetch. The caller decides where the bytes go.
83
- */
84
- declare function toBytes(vault: Vault, options: AsBlobOptions): Promise<AsBlobResult>;
85
- /**
86
- * Browser download — wraps `toBytes()` in a `Blob` and triggers the
87
- * browser's download prompt. Tier 2 egress per the pattern doc:
88
- * plaintext bytes leave memory, but the lifetime is scoped to the
89
- * user's chosen destination (usually the OS download folder).
90
- *
91
- * Requires a browser-like environment with `URL.createObjectURL`
92
- * and `document.createElement`. Throws on headless runtimes — use
93
- * `toBytes()` there and pipe the bytes wherever you need.
94
- */
95
- declare function download(vault: Vault, options: AsBlobDownloadOptions): Promise<void>;
96
- /**
97
- * Node file-write — persists the blob bytes to the filesystem.
98
- * Requires explicit `acknowledgeRisks: true` because the plaintext
99
- * file outlives the current process (Tier 3 egress). The
100
- * authorization check still runs even when the consumer
101
- * acknowledges — capability bits aren't bypassable.
102
- */
103
- declare function write(vault: Vault, path: string, options: AsBlobWriteOptions): Promise<void>;
104
- /**
105
- * `merge` has no meaning for a single-slot write — only `replace`
106
- * (overwrite if present) and `insert-only` (refuse if present) make
107
- * sense. Default is `replace`.
108
- */
109
- type AsBlobImportPolicy = 'replace' | 'insert-only';
110
- interface AsBlobImportOptions {
111
- /** Collection the target record lives in. */
112
- readonly collection: string;
113
- /** Record id. The record must already exist. */
114
- readonly id: string;
115
- /** Slot name. Defaults to `'raw'` (matches `toBytes`). */
116
- readonly slot?: string;
117
- /** Default `'replace'`. */
118
- readonly policy?: AsBlobImportPolicy;
119
- /**
120
- * Optional MIME hint stored on the slot. When omitted the blob layer
121
- * sniffs the magic bytes — same fallback as a direct `blob.put()`.
122
- */
123
- readonly mimeType?: string;
124
- /** Optional filename stored on the slot. */
125
- readonly filename?: string;
126
- }
127
- interface AsBlobImportPlan {
128
- /** Whether the slot was empty (`'added'`) or already populated (`'modified'`). */
129
- readonly status: 'added' | 'modified';
130
- /** Pre-existing eTag when `status === 'modified'`, otherwise undefined. */
131
- readonly priorETag?: string;
132
- readonly collection: string;
133
- readonly id: string;
134
- readonly slot: string;
135
- /** Plaintext byte count of the incoming blob. */
136
- readonly bytes: number;
137
- readonly policy: AsBlobImportPolicy;
138
- apply(): Promise<void>;
139
- }
140
- /**
141
- * Build an import plan for a single attachment. Returns a plan that
142
- * surfaces whether the target slot exists today (`status: 'added' |
143
- * 'modified'`); calling `apply()` performs the write.
144
- *
145
- * Authorization: gated by `assertCanImport('plaintext', 'blob')`.
146
- *
147
- * Errors:
148
- * - `ImportCapabilityError` — keyring lacks the import capability.
149
- * - `Error('as-blob.fromBytes: record ... not found')` — the target
150
- * record id does not exist in the collection. We refuse to attach
151
- * to a phantom record because the slot index would orphan.
152
- * - `Error('as-blob.fromBytes: insert-only ...')` — `policy:
153
- * 'insert-only'` and the slot is already populated.
154
- */
155
- declare function fromBytes(vault: Vault, bytes: Uint8Array, options: AsBlobImportOptions): Promise<AsBlobImportPlan>;
156
-
157
- export { type AsBlobDownloadOptions, type AsBlobImportOptions, type AsBlobImportPlan, type AsBlobImportPolicy, AsBlobNotFoundError, type AsBlobOptions, type AsBlobResult, type AsBlobWriteOptions, download, fromBytes, toBytes, write };