arkiv-images 0.1.1 → 1.0.0

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/AGENTS.md CHANGED
@@ -1,29 +1,17 @@
1
- # Integrating arkiv-images
2
-
3
- This is the consumer guide; it is included in npm. Read [README.md](README.md) explicitly for the public API, runnable examples, dependency versions, limits, data model and recovery behavior. This guide does not replace it.
4
-
5
- ## Ask the developer
6
-
7
- - Local inspection, existing-image retrieval, or a new public upload? The first needs no network; retrieval needs the image key and explicit testnet; only writes need a signing wallet and test funds.
8
- - Which testnet/RPC and Entity Expiration in blocks? Use the actually tested combination in README. Never infer production or mainnet compatibility.
9
- - Are the **original image bytes, filename and metadata** safe to make public? The tool keeps EXIF/GPS unchanged. If privacy is required, stop: encryption is a different tool, and this one does not solve it.
10
- - For retrieval, ask for an image key and optionally an independently trusted SHA-256. Do not confuse an image entity key with its file-manifest key.
11
- - For browser writes, ask the developer to connect their own MetaMask/Rabby account and confirm transactions. For Node, ask them to configure their key locally as described in README; never request a private key in chat. Provider access keys come from the provider/Hub links there.
12
-
13
- Local `inspectImage` and `planImage` can be integrated immediately while legitimate network/product questions remain open.
1
+ Edit instructions only in AGENTS.md; CLAUDE.md only imports this file.
14
2
 
15
- ## Preserve these invariants
16
-
17
- - Import only the public package API; do not copy its storage, image validation or chunking implementation into the app. Use the released npm version in consumers.
18
- - Binary image bytes belong in the payload; queryable metadata belongs in typed attributes. The package schema and dependency own these decisions. Do not search inside payloads or substitute SQL for SDK query syntax.
19
- - Keep the testnet checks, size limits and integrity validation. No partial image, no silent lossy conversion, no pretending expiration erases history.
20
- - A browser consumer must fully decode before upload and after retrieval, then render only the allowed raster MIME as an image. Manage/revoke Blob URLs. Filenames are text, not markup.
21
- - Confirm completion only after `storeImage` resolves. Preserve confirmed hashes/manifest keys on failure, serialize writes, and do not automatically retry ambiguous writes. Check wallet account/network before every signature, including the final root write.
22
- - Keep secrets out of source, logs, packages and browser bundles. Do not display raw SDK/provider errors. No server signer for the sample.
23
- - No independent mutation/extension/ownership transfer of the parts. Root existence alone does not prove its referenced chunks are still available.
24
-
25
- ## Verify the integration
26
-
27
- Follow the README in a new consumer. Import the public TypeScript types, run the exact local example, then test the requested network flow. Compare original and retrieved bytes and digest. Test unsupported/oversized files, wrong network, absent key, rejected writes, missing/corrupt chunks and browser decode errors. Distinguish mocked RPC, packed candidate and installed npm release. For UI, render small/medium/large widths and inspect loading, empty and error states. Record versions, network, actual results and unverified items.
3
+ # Integrating arkiv-images
28
4
 
29
- Source maintainers: keep this guide, CLAUDE.md and verification in the publication allowlist. Sample instructions and agent guide live in `sample/`, linked from README; explicitly give them to consumer agents. No new skills, MCP, generator, encryption or media-management features belong to this tool.
5
+ Read [README.md](README.md) explicitly for the public API, runnable local example, data model, limits, signing and recovery. Version 1.0.0 is a candidate until verification/publication evidence says otherwise.
6
+
7
+ - Ask whether the task needs local inspection, retrieval or a new public upload. Inspection requires no network. Retrieval needs an image key, Rabdi and optionally an independently trusted SHA-256. Writes additionally require a locally configured signing wallet and test funds.
8
+ - Ask whether original bytes, filename and metadata are safe to publish. EXIF/GPS is preserved. Stop if confidentiality is required; encryption is a different tool.
9
+ - Use Rabdi chain 7794095 with SDK 1.0.0. Ask for an explicit Entity Expiration within the admitted block range. Access keys and test funds come from the Hub links in README. Never request a private key in chat.
10
+ - Import only public package APIs. Reuse arkiv-chunking's publicly exported write policy; no private dependency imports or copied storage/chunking/gas logic. Bytes use non-indexed bytes columns; query only indexed typed metadata with SDK predicates.
11
+ - Preserve MIME/size/network/integrity/decode checks and public TypeScript types. A browser must decode before upload and after retrieval, render only PNG/JPEG, treat filenames as text and revoke Blob URLs.
12
+ - Every write uses unsigned latest-state estimation, explicit gas/price/latest nonce and at least twice estimate, below half the head block gas limit. Do not use unlimited lifetime or historical state reads. Never automatically retry successful or uncertain writes.
13
+ - Serialize account writes. Recheck browser wallet identity/network before each signature, including the final root. Confirm completion only after storeImage resolves. Preserve error imageKey/manifestKey/transactionHashes, including CONFIRMATION_PENDING. Never display raw provider errors or causes.
14
+ - No independent part mutation, transfer or extension. Root existence does not prove chunk availability; expiration does not erase historical copies.
15
+ - Sample reads keep the access key server-side in its own endpoint. Browser writes use the visitor's configured wallet RPC; no shared server signer.
16
+ - Verify the exact local example, public types, meaningful controlled failure cases, requested live network flow and original/retrieved byte equality. Distinguish controlled RPC, packed candidate and registry release. Inspect sample empty/loading/error/recovered states at narrow/medium/wide widths.
17
+ - Keep AGENTS.md, CLAUDE.md and verification in the npm publication allowlist. Explicitly provide consumers the sample guides. No new skill, MCP, generator, encryption or media-management features belong here.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 — candidate
4
+
5
+ - Breaking: SDK 1.0.0 peer requirement and Rabdi testnet, chain ID 7794095.
6
+ - Breaking: named `arkiv-images/v2` schema with non-indexed binary data and typed columns. Older image formats are not read by this release.
7
+ - Breaking: arkiv-chunking 1.1.0 with its public write policy and latest-state keyset pagination; no historical snapshot guarantee.
8
+ - Breaking: maximum admitted lifetime 7,883,970 blocks with an inclusion margin; no unlimited lifetime.
9
+ - Public per-write transaction policy, explicit latest estimates/gas/price/nonce, half-block admission ceiling, and recovery keys/hashes when confirmation is pending.
10
+ - Persistent createImageWriteOperation state across UI renders/clicks. Submitted hashes are recorded before receipt rechecks; uncertain, successful and terminal results cannot trigger another send for the same operation.
11
+ - Sample read proxy keeps its access key server-side; the visitor signs through their own configured wallet RPC. Idempotent synthetic PNG/JPEG seed with a persistent unresolved-write journal.
12
+
13
+ This candidate is not published. Validation status is documented in docs/verification.md.
package/CLAUDE.md CHANGED
@@ -1 +1 @@
1
- Read [AGENTS.md](AGENTS.md) for the consumer integration guide and invariants.
1
+ @AGENTS.md
package/README.md CHANGED
@@ -1,224 +1,126 @@
1
- # arkiv-images
2
-
3
- Store a public PNG or JPEG as queryable Arkiv entities, then retrieve and display its original bytes. Small images use one entity. Larger images use the existing `arkiv-chunking` package: a discoverable image entity points to a file manifest, which links the ordered chunks. Retrieval checks size, format, dimensions and SHA-256 before returning the bytes.
4
-
5
- **These packages are intended for testnet use.**
6
-
7
- Version **0.1.1**. See [verification](docs/verification.md) for release evidence and [the npm registry](https://www.npmjs.com/package/arkiv-images/v/0.1.1) for availability. A locally packed artifact is not a published release.
8
-
9
- ## Install and inspect an image
10
-
11
- ESM, Node **22.10+** with Web Crypto, or a current browser on HTTPS/localhost. The tested dependency combination is SDK **0.8.0**, viem **2.56.3**, `arkiv-chunking` **0.1.0**, Node **22.22.3**. SDK 0.7 and older are incompatible. Other SDK versions, runtimes and networks are not claimed tested; see the evidence matrix below.
12
-
13
- ```sh
14
- mkdir image-consumer
15
- cd image-consumer
16
- npm init -y
17
- npm pkg set type=module
18
- npm install --save-exact arkiv-images@0.1.1 @arkiv-network/sdk@0.8.0 viem@2.56.3
19
- ```
20
-
21
- Create `inspect.mjs`:
22
-
23
- ```js
24
- import { inspectImage, planImage } from 'arkiv-images';
25
-
26
- // Synthetic 1x1 PNG, no personal image or credentials.
27
- const bytes = new Uint8Array(Buffer.from(
28
- 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4V+L2HwAG5AK4X5zdSAAAAABJRU5ErkJggg==', 'base64'));
29
- console.log(inspectImage(bytes));
30
- console.log(planImage(bytes.length));
31
- ```
32
-
33
- ```sh
34
- node inspect.mjs
35
- ```
36
-
37
- Expected: PNG, width `1`, height `1`, byte length `70`; mode `inline`, `entityCount: 1`, `transactionCount: 1`. This inspection is local: no wallet, RPC, funds or access key. Base64 is used only to embed this tiny example; the package stores binary bytes without base64 overhead.
38
-
39
- ## Store and retrieve on Tiramisu
40
-
41
- Use **Tiramisu testnet, chain ID 7738577**, exported by `@arkiv-network/sdk/chains`. [Network connection details](https://stage.hub.arkiv.network/networks), [test GLM faucet](https://stage.hub.arkiv.network/faucet) and [access keys](https://stage.hub.arkiv.network/api-keys) are provided by the Hub staging environment, verified on 2026-09-08. The production Hub still showed retired Braga at verification time. The faucet and access-key pages require connecting/signing in with your own wallet; follow their on-screen verification and cooldown requirements. No faucet claim or new access-key issuance was needed or tested in this run. The RPC used below allows anonymous reads; write access and quotas depend on the provider. A provider access key does not replace a signing wallet. Never put an administrative provider key in a browser.
42
-
43
- The [sample](https://github.com/SantiagoDevRel/arkiv-images/blob/feat/image-storage/sample/README.md) uses **MetaMask or Rabby** to sign with the visitor's own wallet. No shared server signer or private key is needed. For a Node integration, configure a dedicated test wallet key **locally**, outside the checkout. Do not send private keys to an agent or put them in chat.
44
-
45
- In the consumer above, save `roundtrip.mjs`:
46
-
47
- ```js
48
- import { readFile } from 'node:fs/promises';
49
- import { createPublicClient, createWalletClient } from '@arkiv-network/sdk';
50
- import { tiramisu } from '@arkiv-network/sdk/chains';
51
- import { http } from 'viem';
52
- import { privateKeyToAccount } from 'viem/accounts';
53
- import { inspectImage, planImage, storeImage, retrieveImage } from 'arkiv-images';
54
-
55
- if (!process.env.ARKIV_PRIVATE_KEY) throw new Error('Configure ARKIV_PRIVATE_KEY locally.');
56
- const bytes = new Uint8Array(await readFile('example.png'));
57
- console.log(inspectImage(bytes), planImage(bytes.length));
58
- const transport = () => http(process.env.ARKIV_RPC_URL ?? tiramisu.rpcUrls.default.http[0], {
59
- retryCount: 0, timeout: 30_000, fetchOptions: { cache: 'no-store' },
60
- });
61
- const publicClient = createPublicClient({ chain: tiramisu, transport: transport() });
62
- const walletClient = createWalletClient({ chain: tiramisu, transport: transport(),
63
- account: privateKeyToAccount(process.env.ARKIV_PRIVATE_KEY) });
64
- const saved = await storeImage({ publicClient, walletClient, bytes, filename: 'example.png',
65
- expirationBlocks: 43_200 });
66
- const image = await retrieveImage({ publicClient, imageKey: saved.imageKey,
67
- expectedSha256: saved.sha256 });
68
- const identical = image.bytes.length === bytes.length && bytes.every((b,i) => b === image.bytes[i]);
69
- if (!identical) throw new Error('Image bytes differ');
70
- console.log({ imageKey: saved.imageKey, sha256: image.sha256, mode: image.mode,
71
- bytes: image.bytes.length, identical, expiresAt: String(image.expiresAt) });
72
- ```
73
-
74
- Place a **non-sensitive PNG** at `example.png`. Then run:
75
-
76
- ```sh
77
- node --env-file=/absolute/path/to/your-local-secret.env roundtrip.mjs
78
- ```
79
-
80
- The local secret file uses ordinary `NAME=value` entries for `ARKIV_PRIVATE_KEY` and optionally `ARKIV_RPC_URL`. Supply your own values; no secrets are included here. If those variables already exist in the process environment, use `node roundtrip.mjs`.
81
-
82
- Expected: a new 32-byte `imageKey`, SHA-256, the selected storage mode, byte count, `identical: true` and a queried expiration block. Writes spend **test GLM**. Keep the returned image key for retrieval after restarting. A read needs only `publicClient` and `imageKey`, not the wallet. To display the recovered bytes in a browser:
83
-
84
- ```js
85
- const image = await retrieveImage({ publicClient, imageKey });
86
- const blob = new Blob([new Uint8Array(image.bytes)], { type: image.contentType });
87
- const bitmap = await createImageBitmap(blob); // full browser decode; may reject corrupt pixels
88
- bitmap.close();
89
- const url = URL.createObjectURL(blob);
90
- document.querySelector('img').src = url;
91
- // When replacing the image or unmounting: URL.revokeObjectURL(url).
92
- ```
93
-
94
- ## Read an existing image without a wallet
95
-
96
- In the same clean consumer, save `read.mjs`:
97
-
98
- ```js
99
- import { writeFile } from 'node:fs/promises';
100
- import { createPublicClient } from '@arkiv-network/sdk';
101
- import { tiramisu } from '@arkiv-network/sdk/chains';
102
- import { http } from 'viem';
103
- import { retrieveImage } from 'arkiv-images';
104
-
105
- const publicClient = createPublicClient({ chain: tiramisu,
106
- transport: http(tiramisu.rpcUrls.default.http[0], { retryCount: 0 }) });
107
- const image = await retrieveImage({ publicClient, imageKey: process.argv[2] });
108
- // Use a fixed local output path, not the untrusted stored filename.
109
- const output = image.contentType === 'image/png' ? 'recovered.png' : 'recovered.jpg';
110
- await writeFile(output, image.bytes, { flag: 'wx' });
111
- console.log({ output, bytes: image.byteLength, sha256: image.sha256 });
112
- ```
113
-
114
- Run `node read.mjs YOUR_IMAGE_ENTITY_KEY` with a key returned by a completed upload. Expected: a recovered image file and its byte count/digest. No signing wallet, funds or private key are needed. The output uses exclusive creation: if it already exists, choose another filename rather than silently overwriting it. Entities in the verification evidence can expire, so they are evidence, not permanent fixtures.
115
-
116
- ## Data model and queries
117
-
118
- | Location | Contents |
119
- |---|---|
120
- | Image entity attributes | `type: str('arkiv-images/v1')`, `mode: str('inline'|'chunked')`, `mime: str(...)`, `filename: str(...)`, `sha256: bytes32(...)`, `size/width/height: u64(...)`; chunked mode adds `manifest: key(...)` |
121
- | Inline payload | Original PNG/JPEG binary bytes; content type `image/png` or `image/jpeg` |
122
- | Chunked image payload | Empty; the `manifest` attribute links to the file, rather than pretending the image is contained in this one payload |
123
- | File manifest and chunks | The versioned `arkiv-chunking@0.1.0` format; the dependency owns splitting, sequencing, snapshot pagination, completion and integrity checks |
124
-
125
- The image entity is created read-only. Filenames are public, indexed attributes; the package does not infer author/version from the image. Queries operate on attributes, never image pixels or payload contents. To discover PNG images:
126
-
127
- ```js
128
- import { str } from '@arkiv-network/sdk/attr';
129
- import { eq } from '@arkiv-network/sdk/query';
130
- import { IMAGE_TYPE, retrieveImage } from 'arkiv-images';
131
-
132
- const query = publicClient.select({ key: true })
133
- .where(eq('type', str(IMAGE_TYPE)), eq('mime', str('image/png')))
134
- .limit(200);
135
- for await (const entity of query) {
136
- // Narrow by owner or another indexed attribute for a real application.
137
- // A single image's retrieval does not enumerate the global catalogue.
138
- const image = await retrieveImage({ publicClient, imageKey: entity.key });
139
- console.log(entity.key, image.byteLength);
140
- }
141
- ```
142
-
143
- SDK types matter: `u64(2)` is different from `i32(2)`. The actual query expression for this example is `type = str('arkiv-images/v1') AND mime = str('image/png')`, not SQL `SELECT *`.
144
-
145
- ## Public API
146
-
147
- All functions and TypeScript types are exported from `arkiv-images`. Do not import internal build paths.
148
-
149
- | Export | Contract |
150
- |---|---|
151
- | `inspectImage(bytes, expectedContentType?)` | Returns `ImageInfo`: detected MIME, width, height, byteLength. Validates size, signature and basic container structure; not a full pixel decoder. No network. |
152
- | `planImage(byteLength)` | Returns `ImagePlan`: inline/chunked mode, bytes, chunk/entity/transaction counts. No network. |
153
- | `storeImage(options)` | Resolves `StoredImage` after observed confirmations: imageKey, optional manifestKey, digest, dimensions, size, mode, transactionHashes. Required clients, bytes, filename, expirationBlocks; optional contentType/onProgress. |
154
- | `retrieveImage(options)` | Resolves `RetrievedImage`: original bytes, filename, MIME, dimensions, digest, mode, queried expiresAt. Required publicClient/imageKey; optional expectedSha256/onProgress. Never returns a partial image. |
155
- | `ImageStorageError`, `ChunkingError` | Typed error codes. Upload errors preserve available manifestKey and confirmed transactionHashes. Raw `cause` is for local diagnostics and may contain provider secrets: never render or log it in the UI. |
156
- | Types | `ImageInfo`, `ImagePlan`, `StoreImageOptions`, `StoredImage`, `RetrieveImageOptions`, `RetrievedImage`, `ImageContentType`, `ImageErrorCode`, `Progress` |
157
- | Constants | `VERSION`, `IMAGE_TYPE`, `INLINE_MAX_BYTES`, `MAX_IMAGE_BYTES`, `MAX_IMAGE_PIXELS` |
158
-
159
- `onProgress` is the chunk dependency's observational callback: phase `manifest`, `chunks` or `finalize`, completed/total and optional manifestKey. Its total counts **file chunks**, not the final image entity transaction. Inline writes do not emit chunk progress. Only a resolved `storeImage` means the whole upload finished; callback exceptions cannot change a confirmed upload into a failure.
160
-
161
- ## Limits, visibility and failure semantics
162
-
163
- | Policy | Value |
164
- |---|---|
165
- | Formats | Static PNG; baseline/progressive JPEG. No SVG, GIF, APNG, WebP, HEIC, PDF or video |
166
- | Maximum file | **26,214,400 bytes (25 MiB)**, measured before upload; in memory, no streaming |
167
- | Maximum dimensions | Positive width/height, product at most **40,000,000 pixels** |
168
- | Inline image | **1–120,000 bytes**, one entity and one transaction |
169
- | Chunked image | **120,001–26,214,400 bytes**, 100,000-byte chunks; N+2 entities, N+3 confirmed transactions |
170
- | At 120,001 bytes | 2 chunks + manifest + image entity: 4 entities, 5 transactions |
171
- | At 1,000,000 bytes | 10 chunks + manifest + image entity: 12 entities, 13 transactions |
172
- | At 25 MiB | 263 chunks, 265 entities, 266 transactions; this is a policy ceiling, not a claim that uploading this size is quick or economical |
173
- | Filename | Basename only, 1–128 UTF-8 bytes (SDK `str` attribute limit), no path separators or control characters |
174
- | Expiration | Explicit whole blocks: `max(64, transactionCount + 32)` through 15,768,000 |
175
-
176
- The inline threshold leaves room for attributes and transaction framing. The commonly cited 131,072-byte payload ceiling is **not** an image budget: the complete transaction also has a size limit. [Verification](docs/verification.md) records actual encoded sizes and which boundaries were submitted on Tiramisu. JPEG/PNG are already compressed image formats; converting to PDF or base64 does not remove the storage constraint. A photo's size varies with dimensions, detail and encoding; the sample measures the selected file rather than guessing from megapixels.
177
-
178
- **No encryption, metadata stripping, resizing or recompression.** The tool preserves the exact original bytes, including EXIF/GPS, embedded thumbnails and names. Everyone with read access to the public network can retrieve them. The sample requires acknowledgement before uploading. Use synthetic/non-sensitive images. Expiration removes entities from current queries; it does **not** erase historical bytes or copies. This tool offers no read authorization and does not claim confidentiality.
179
-
180
- The package performs bounded container checks, not a complete PNG CRC/JPEG entropy decode, antivirus scan or content moderation. A file can pass header checks and still fail full decoding. JPEGs must end at their end-of-image marker; files with trailing padding are rejected even if some decoders accept them. Export such files as a standard JPEG before uploading. The sample calls `createImageBitmap` both before writes and after retrieval. Consumers rendering images must also handle decode failures. Never serve uploaded bytes as HTML or SVG, and never trust a filename as markup.
181
-
182
- Chunking is required only for images above the inline threshold and is **reused, not reimplemented**. The dependency is installed with the package. An inline read uses one entity query plus the chain check. A chunked read first loads the image entity, then the file manifest and every required chunk page; it is one JavaScript API call, **not one RPC query**. File pages share the manifest's snapshot block. The initial image query and file query are separate snapshots. A trusted `expectedSha256` pins bytes independently of mutable manifests; a digest proves integrity, not the publisher's identity.
183
-
184
- Parts receive relative expiration at their own creation. The manifest is older than the final image entity and can expire first. The root may therefore still exist when the file can no longer be retrieved; retrieval fails clearly. `RetrievedImage.expiresAt` is the earlier queried root/manifest expiration, not a fixed-time availability promise. Do not independently extend, transfer or change parts; these are not supported workflows.
185
-
186
- No multi-transaction atomicity, automatic write retries/resume, deduplication, deletion or rollback. If the final image transaction fails, confirmed file entities remain: preserve `manifestKey` and receipts, inspect them, then decide whether to start a new upload. A timeout can follow a successful transaction; the returned list includes only confirmations observed by this call. Serialize writes from the same account. In browser integrations recheck account/network before **every** signing request; the sample provides the implementation.
187
-
188
- ## Troubleshooting
189
-
190
- | Symptom | Resolution |
191
- |---|---|
192
- | `INVALID_IMAGE` / browser decode error | Use a valid static PNG or baseline/progressive JPEG within the pixel limit. Changing an extension does not convert a file. |
193
- | `TOO_LARGE` | Choose a smaller file. The tool does not silently resize it. |
194
- | `INVALID_INPUT` | Check 32-byte key/hash, filename rules and whole-block expiration. |
195
- | `NETWORK_MISMATCH` | Configure matching explicit testnet clients; the sample supports only Tiramisu. Check RPC `eth_chainId` too. |
196
- | Wallet missing, rejected or changed | Enable/connect MetaMask or Rabby; resolve pending requests. Reconnect after changing account and inspect confirmed writes before retrying. |
197
- | Not enough GLM | Obtain test GLM for the actual signing account on Tiramisu; plan the number of transaction approvals before uploading a large photo. |
198
- | RPC 401/403/429, `READ_FAILED` | Check your network-specific provider access key/quota/connection. Never retry ambiguous writes automatically. |
199
- | `NOT_FOUND` | Verify the **image** key and network; entity may have expired. A chunk manifest key is a different type. |
200
- | `INCOMPLETE_UPLOAD`, `MISSING_CHUNK` | Inspect upload receipts and expiration. Partial images are not returned. |
201
- | `INVALID_ENTITY`, `INVALID_MANIFEST`, `INVALID_CHUNK`, `HASH_MISMATCH` | Treat the result as invalid; do not bypass checks. A manifest may have changed or content may be damaged. |
202
- | npm E404 | Check the publication status in verification; do not call a local tarball published. |
203
- | CommonJS import failure | Use ESM `import`; there is no CommonJS export. |
204
-
205
- ## Sample, agent guides and development
206
-
207
- - [Sample README](https://github.com/SantiagoDevRel/arkiv-images/blob/feat/image-storage/sample/README.md): source checkout, localhost commands and visitor-wallet flow.
208
- - [Consumer AGENTS.md](AGENTS.md) and [sample AGENTS.md](https://github.com/SantiagoDevRel/arkiv-images/blob/feat/image-storage/sample/AGENTS.md). Give these guides to your agent explicitly. It will not necessarily read files under `node_modules` automatically. Each CLAUDE.md points to the corresponding AGENTS.md.
209
- - [Verification](docs/verification.md): exact runtimes, dependency versions, simulated vs real checks, publication and unresolved gates.
210
-
211
- ```sh
212
- git clone --branch feat/image-storage https://github.com/SantiagoDevRel/arkiv-images.git
213
- cd arkiv-images
214
- npm ci
215
- npm test
216
- npm run typecheck
217
- npm pack --pack-destination /absolute/path/to/output
218
- ```
219
-
220
- For an unpublished candidate only, replace the npm package argument in a clean consumer with the absolute `.tgz` path. This tests the packed artifact, not registry publication. There are no runtime aliases to sibling repositories.
221
-
222
- Browser regression checks use Playwright with a locally installed Chrome. First start the sample dev server in another terminal, then run `npm run test:browser` from the package root. They exercise the actual SDK/package with a controlled RPC and injected wallet, not live funds. Screenshots and results default to `~/Downloads/arkiv-image-storage-98/browser`; set `ARTIFACTS_DIR` to another output directory or `SAMPLE_URL` to another localhost port. These checks are separate from `npm test`.
223
-
224
- Provenance: image workflow and PNG/JPEG/25 MiB policy from [Arkiv-Network/usecases-imagedb](https://github.com/Arkiv-Network/usecases-imagedb); public-byte/metadata cautions and transaction budget from the existing Arkiv Cookbook; chunk implementation is the separately published `arkiv-chunking` dependency. The old service's quotas, shared signer and RPC protocol are not added to this package. Test RPC harness and sample token treatments are adapted from arkiv-chunking, with Arkiv UI's documented open font fallbacks.
1
+ # arkiv-images
2
+
3
+ Store a public PNG or JPEG as queryable Arkiv entities, then retrieve its original bytes with size, MIME, dimensions and SHA-256 checks. Small images use one entity with a non-indexed `bytes()` column. Larger images use `arkiv-chunking`: an image entity references a file manifest and ordered immutable chunks.
4
+
5
+ **1.0.0 is a candidate, pending verification and publication. Intended for Rabdi testnet only.** The published release remains separate from this source candidate. See [verification](docs/verification.md) for actual results and blockers.
6
+
7
+ ## Install and inspect
8
+
9
+ ESM, Node 22.10+ with Web Crypto, or a browser on HTTPS/localhost. Candidate dependencies: SDK 1.0.0, viem 2.56.3, arkiv-chunking 1.1.0. Install the packed candidate until 1.0.0 is published:
10
+
11
+ ```sh
12
+ mkdir image-consumer
13
+ cd image-consumer
14
+ npm init -y
15
+ npm pkg set type=module
16
+ npm install /absolute/path/arkiv-images-1.0.0.tgz @arkiv-network/sdk@1.0.0 viem@2.56.3
17
+ ```
18
+
19
+ Save `inspect.mjs`:
20
+
21
+ ```js
22
+ import { inspectImage, planImage } from 'arkiv-images';
23
+ const bytes = new Uint8Array(Buffer.from(
24
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4V+L2HwAG5AK4X5zdSAAAAABJRU5ErkJggg==', 'base64'));
25
+ console.log(inspectImage(bytes));
26
+ console.log(planImage(bytes.length));
27
+ ```
28
+
29
+ `node inspect.mjs` returns PNG, 1 × 1, 70 bytes, inline mode, one entity and one transaction. Inspection is local and needs no wallet, access key or network. Base64 embeds this example only; stored content is binary.
30
+
31
+ ## Rabdi round trip
32
+
33
+ Rabdi is a DB-Chain testnet, chain ID **7794095**. Obtain your own [access key](https://hub.arkiv.network/access-keys) and [test funds](https://hub.arkiv.network/faucet). Configure a dedicated test wallet locally, outside source control; never send its private key in chat. Missing access keys fail closed with `MissingAccessKeyError`.
34
+
35
+ Save `roundtrip.mjs` in the consumer:
36
+
37
+ ```js
38
+ import assert from 'node:assert/strict';
39
+ import { readFile } from 'node:fs/promises';
40
+ import { createPublicClient, createWalletClient, MissingAccessKeyError } from '@arkiv-network/sdk';
41
+ import { rabdi } from '@arkiv-network/sdk/chains';
42
+ import { http } from 'viem';
43
+ import { privateKeyToAccount } from 'viem/accounts';
44
+ import { inspectImage, planImage, storeImage, retrieveImage } from 'arkiv-images';
45
+
46
+ if (!process.env.ARKIV_ACCESS_KEY) throw new MissingAccessKeyError('Rabdi');
47
+ if (!process.env.ARKIV_PRIVATE_KEY) throw Error('Configure a dedicated test wallet locally.');
48
+ const chain = rabdi({ accessKey: process.env.ARKIV_ACCESS_KEY });
49
+ const transport = () => http(chain.rpcUrls.default.http[0], {
50
+ retryCount: 0, timeout: 30_000, fetchOptions: { cache: 'no-store' },
51
+ });
52
+ const publicClient = createPublicClient({ chain, transport: transport() });
53
+ const walletClient = createWalletClient({ chain, transport: transport(),
54
+ account: privateKeyToAccount(process.env.ARKIV_PRIVATE_KEY) });
55
+ const bytes = new Uint8Array(await readFile('example.png'));
56
+ console.log(inspectImage(bytes), planImage(bytes.length));
57
+ const stored = await storeImage({ publicClient, walletClient, bytes,
58
+ filename: 'example.png', expirationBlocks: 3600 });
59
+ const recovered = await retrieveImage({ publicClient, imageKey: stored.imageKey,
60
+ expectedSha256: stored.sha256 });
61
+ assert.deepEqual(recovered.bytes, bytes);
62
+ console.log({ imageKey: stored.imageKey, byteExact: true });
63
+ ```
64
+
65
+ Run `node roundtrip.mjs` after configuring those environment variables locally. All original bytes, filename, metadata and wallet addresses are public, including EXIF/GPS. Use synthetic or non-sensitive images. There is no encryption, metadata stripping, resizing, recompression or read authorization. Entity Expiration removes data from current queries; it does not erase history or copies.
66
+
67
+ ## Public API
68
+
69
+ | Export | Contract |
70
+ |---|---|
71
+ | `inspectImage(bytes, expectedContentType?)` | Size, static PNG/JPEG container, MIME and dimensions; no network or complete pixel decode. |
72
+ | `planImage(byteLength)` | Inline/chunked mode and measured entity/transaction count. No network. |
73
+ | `storeImage(options)` | Requires matching Rabdi public/wallet clients, bytes, basename, explicit `expirationBlocks`; optional content type, progress and per-write `txParams` policy. Returns imageKey, optional manifestKey, sha256, mode, dimensions and observed transaction hashes. |
74
+ | `createImageWriteOperation(optionsOrAsyncSetup)` | Create once per intentional upload and keep across UI renders/clicks. `run()` sets sending before setup/network calls and returns the same promise forever, including failed, uncertain or pending outcomes. Read-only state, imageKey and transactionHashes support recovery. |
75
+ | `retrieveImage(options)` | Public client, **image** key, optional independently trusted SHA-256 and progress. Returns original bytes, metadata, mode and earliest observed expiration. |
76
+ | `ImageSchema`, `IMAGE_TYPE` | SDK 1.0 named schema `arkiv-images/v2`. Binary `data` is stored without an index. Indexed type/mode/mime/sha256/manifest support discovery. Filename, size and dimensions are stored typed columns. |
77
+ | `ImageStorageError`, `ChunkingError` | Safe code/message; preserve imageKey/manifestKey and transactionHashes on failure. Raw causes can include provider details: never display or log them. |
78
+ | `TransactionParameterPolicy`, `Progress` | Public TypeScript integration types re-exported from chunking. |
79
+ | `VERSION`, `INLINE_MAX_BYTES`, `MAX_IMAGE_BYTES`, `MAX_IMAGE_PIXELS`, `MAX_EXPIRATION_BLOCKS`, `RABDI_MAX_LIFETIME_BLOCKS`, `EXPIRATION_INCLUSION_MARGIN_BLOCKS` | Package version and enforced limits. |
80
+
81
+ Import types such as `ImageInfo`, `ImagePlan`, `StoreImageOptions`, `StoredImage`, `RetrieveImageOptions`, `RetrievedImage`, `ImageContentType` and `ImageErrorCode` directly from `arkiv-images`.
82
+
83
+ Limits: **120,000 bytes** inline; **25 MiB** per image; **40,000,000 pixels**. Chunking uses 100,000-byte slices. Larger files require N+3 transactions for N chunks. `planImage` computes this count. These are package admission limits, not a promise of every node's transaction budget.
84
+
85
+ Entity Expiration is whole blocks, at least `max(64, transactionCount + 32)` and at most **7,883,970**, leaving 30 blocks below Rabdi's 7,884,000-block maximum. There is no unlimited lifetime. Longer storage requires coordinated Lifetime Extension outside this API. The image root and manifest use the same absolute deadline; retrieval rejects independent deadline changes. Root existence alone does not guarantee availability of every chunk.
86
+
87
+ ## Signing and recovery
88
+
89
+ Every part and the final root reuse the public `arkiv-chunking` write policy: an unsigned SDK request is estimated at `latest`; gas is at least `max(1_000_000, 2 × estimate)`, with explicit network gasPrice and a fresh latest nonce. The image API additionally stops before signing if that budget exceeds half the latest block gas limit. Longer expiration can substantially increase gas, so shorten Entity Expiration or choose a smaller image if the estimate exceeds that budget. Admitted byte/lifetime limits do not guarantee every combination fits the network's gas budget. `txParams` can increase gas or change fees; it cannot remove required headroom or override the latest nonce. Wallet account/network are checked before every signature.
90
+
91
+ Serialize writes from the same account. A retry is permitted only once after an authoritative gas-exhausted reverted receipt and only within the half-block gas ceiling. Successful or uncertain writes are never automatically resent. `CONFIRMATION_PENDING` preserves SDK-success hashes and root/manifest keys if a later receipt lookup is unresolved or disagrees. The operation records submitted before that lookup. Browser consumers must keep a `createImageWriteOperation` object across renders and later clicks; `storeImage` is a one-call convenience for an intentional new upload. Inspect preserved receipts before explicitly creating another operation. The sample resets the operation only when you choose a file again. Other failures can also leave confirmed entities. There is no multi-transaction atomicity, rollback, resumable upload, independent part mutation/transfer/extension or automatic cleanup.
92
+
93
+ Retrieval uses latest-state keyset pages and verifies immutable chunks, owner/creator, sequence, lengths, metadata and digest; the manifest is rechecked before return. It does not claim a historical snapshot. An independently trusted digest pins bytes, not the publisher's identity. Missing/corrupt chunks never return a partial image.
94
+
95
+ The package checks containers, not full PNG CRC/JPEG entropy decoding or malware. Browser consumers must fully decode before upload and after retrieval, render only PNG/JPEG as images, treat filenames as text, handle decode failure and revoke Blob URLs. JPEG must end at its end-of-image marker; trailing padding is rejected.
96
+
97
+ ## Troubleshooting
98
+
99
+ | Symptom | Action |
100
+ |---|---|
101
+ | `MissingAccessKeyError`, HTTP 401/403/429 | Configure your own network access key; check provider quota. The sample keeps its read key server-side. |
102
+ | `arkiv serves state at the chain head only` | Use `latest` for state reads, nonce and unsigned estimates; historical block hashes, safe/finalized state are unsupported. |
103
+ | Revert with gasUsed equal to gas limit | Use explicit gas with at least twice the latest estimate. The public policy owns the bounded retry; do not resend an uncertain write. |
104
+ | `INVALID_INPUT`, `NETWORK_MISMATCH` | Check Rabdi, whole-block lifetime, filename and 32-byte key/digest. |
105
+ | `INVALID_IMAGE`, `TOO_LARGE`, browser decode error | Choose a valid static PNG/JPEG within limits. Changing an extension does not convert it. |
106
+ | `NOT_FOUND`, `MISSING_CHUNK`, `INCOMPLETE_UPLOAD` | Check the image key, network, referenced parts and expiration; a manifest key is a different key type. |
107
+ | `INVALID_ENTITY`, `INVALID_MANIFEST`, `INVALID_CHUNK`, `HASH_MISMATCH` | Do not bypass integrity checks or display partial content. |
108
+ | `CONFIRMATION_PENDING`, `UPLOAD_FAILED` | Preserve recovery keys/hashes. Inspect receipts and the wallet before a new upload. |
109
+
110
+ ## Sample and development
111
+
112
+ The [sample README](sample/README.md) explains the existing `arkiv-images-example` project, preview status and visitor-wallet setup. Reads use its own bounded `/api/rpc` endpoint and a server-side `ARKIV_ACCESS_KEY`; the visitor configures their own wallet RPC. No server signer. Candidate previews consume the packed candidate; registry releases must be verified separately.
113
+
114
+ Give consumer agents [AGENTS.md](AGENTS.md) and [sample/AGENTS.md](sample/AGENTS.md) explicitly. The corresponding CLAUDE.md files only import those guides.
115
+
116
+ ```sh
117
+ npm ci
118
+ npm run typecheck
119
+ npm run lint
120
+ npm test
121
+ npm pack
122
+ ```
123
+
124
+ Tests distinguish controlled RPC, live Rabdi, packed installs and npm publication in [verification](docs/verification.md). Seed synthetic data with `node scripts/seed-rabdi.mjs` after build and local key configuration; it discovers matching live entities and never retries unresolved journal entries. Keep `.seed-rabdi-state.json` private and persistent between runs.
125
+
126
+ Breaking format changes are in [CHANGELOG.md](CHANGELOG.md). Historical evidence under docs stays dated; it is not validation of this candidate.
package/dist/index.d.ts CHANGED
@@ -1,24 +1,42 @@
1
- import { type ChunkingPublicClient, type ChunkingWalletClient, type Progress } from 'arkiv-chunking';
2
- import type { Hex } from 'viem';
3
- export declare const VERSION = "0.1.1";
4
- export declare const IMAGE_TYPE = "arkiv-images/v1";
1
+ import { type Column, type Schema, type TypeTag } from '@arkiv-network/sdk/schema';
2
+ import { type TransactionParameterPolicy, type ChunkingPublicClient, type ChunkingWalletClient, type Progress } from 'arkiv-chunking';
3
+ import { type Hex } from 'viem';
4
+ export declare const VERSION = "1.0.0";
5
+ export declare const IMAGE_TYPE = "arkiv-images/v2";
6
+ type Stored<T extends TypeTag> = Column<T, false, false, false>;
7
+ type Indexed<T extends TypeTag> = Column<T, true, false, false>;
8
+ export declare const ImageSchema: Schema<{
9
+ type: Indexed<'str'>;
10
+ mode: Indexed<'str'>;
11
+ mime: Indexed<'str'>;
12
+ sha256: Indexed<'str'>;
13
+ filename: Stored<'str'>;
14
+ size: Stored<'u64'>;
15
+ width: Stored<'u64'>;
16
+ height: Stored<'u64'>;
17
+ manifest: Column<'key', true, true, false>;
18
+ data: Stored<'bytes'>;
19
+ }>;
20
+ export { MAX_EXPIRATION_BLOCKS, RABDI_MAX_LIFETIME_BLOCKS, EXPIRATION_INCLUSION_MARGIN_BLOCKS } from 'arkiv-chunking';
5
21
  export declare const INLINE_MAX_BYTES = 120000;
6
22
  export declare const MAX_IMAGE_BYTES: number;
7
23
  export declare const MAX_IMAGE_PIXELS = 40000000;
8
24
  export type ImageContentType = 'image/png' | 'image/jpeg';
9
- export type ImageErrorCode = 'INVALID_IMAGE' | 'TOO_LARGE' | 'INVALID_INPUT' | 'NETWORK_MISMATCH' | 'NOT_FOUND' | 'INVALID_ENTITY' | 'HASH_MISMATCH' | 'UPLOAD_FAILED' | 'READ_FAILED';
25
+ export type ImageErrorCode = 'INVALID_IMAGE' | 'TOO_LARGE' | 'INVALID_INPUT' | 'NETWORK_MISMATCH' | 'NOT_FOUND' | 'INVALID_ENTITY' | 'HASH_MISMATCH' | 'UPLOAD_FAILED' | 'CONFIRMATION_PENDING' | 'READ_FAILED';
10
26
  export declare class ImageStorageError extends Error {
11
27
  readonly code: ImageErrorCode;
12
28
  readonly manifestKey?: Hex;
29
+ readonly imageKey?: Hex;
13
30
  readonly transactionHashes: readonly Hex[];
14
31
  constructor(code: ImageErrorCode, message: string, options?: {
15
32
  cause?: unknown;
16
33
  manifestKey?: Hex;
34
+ imageKey?: Hex;
17
35
  transactionHashes?: readonly Hex[];
18
36
  });
19
37
  }
20
38
  export { ChunkingError } from 'arkiv-chunking';
21
- export type { Progress } from 'arkiv-chunking';
39
+ export type { Progress, TransactionParameterPolicy } from 'arkiv-chunking';
22
40
  export interface ImageInfo {
23
41
  contentType: ImageContentType;
24
42
  width: number;
@@ -40,6 +58,8 @@ export interface StoreImageOptions {
40
58
  contentType?: ImageContentType;
41
59
  expirationBlocks: number;
42
60
  onProgress?: (progress: Progress) => void | Promise<void>;
61
+ /** Reuses the chunking write policy for every part and the image root. */
62
+ txParams?: TransactionParameterPolicy;
43
63
  }
44
64
  export interface StoredImage extends ImageInfo {
45
65
  imageKey: Hex;
@@ -48,6 +68,16 @@ export interface StoredImage extends ImageInfo {
48
68
  mode: 'inline' | 'chunked';
49
69
  transactionHashes: Hex[];
50
70
  }
71
+ export type ImageWriteState = 'ready' | 'sending' | 'submitted' | 'complete' | 'uncertain' | 'failed';
72
+ export interface ImageWriteOperation {
73
+ readonly state: ImageWriteState;
74
+ readonly imageKey?: Hex;
75
+ readonly transactionHashes: readonly Hex[];
76
+ /** Repeated calls share one operation, including a rejected or pending result. */
77
+ run(): Promise<StoredImage>;
78
+ }
79
+ /** Keep this object across UI renders/clicks. A new operation is an explicit new upload. */
80
+ export declare function createImageWriteOperation(options: StoreImageOptions | (() => Promise<StoreImageOptions>)): ImageWriteOperation;
51
81
  export interface RetrieveImageOptions {
52
82
  publicClient: ChunkingPublicClient;
53
83
  imageKey: Hex;
@@ -64,7 +94,7 @@ export interface RetrievedImage extends ImageInfo {
64
94
  /** Validate size, signature, container dimensions. This is not a full pixel decoder or malware scanner. */
65
95
  export declare function inspectImage(bytes: Uint8Array, expectedContentType?: ImageContentType): ImageInfo;
66
96
  export declare function planImage(byteLength: number): ImagePlan;
67
- /** Preserve original file bytes. All bytes, metadata and wallet addresses are public. Never retries writes. */
97
+ /** Preserve original public file bytes. Only the shared policy's confirmed-revert retry is allowed. */
68
98
  export declare function storeImage(options: StoreImageOptions): Promise<StoredImage>;
69
99
  /** One SDK call for an inline image; chunked images additionally load and verify the referenced file. */
70
100
  export declare function retrieveImage({ publicClient, imageKey, expectedSha256, onProgress }: RetrieveImageOptions): Promise<RetrievedImage>;