arkiv-images 0.1.0 → 0.1.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/README.md +224 -224
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/testnet-evidence.json +2 -1
- package/docs/verification.md +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,224 +1,224 @@
|
|
|
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.
|
|
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.
|
|
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://hub.arkiv.network/networks), [test GLM faucet](https://hub.arkiv.network/faucet) and [access keys](https://hub.arkiv.network/api-keys) are provided by the Hub. 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 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.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ChunkingPublicClient, type ChunkingWalletClient, type Progress } from 'arkiv-chunking';
|
|
2
2
|
import type { Hex } from 'viem';
|
|
3
|
-
export declare const VERSION = "0.1.
|
|
3
|
+
export declare const VERSION = "0.1.1";
|
|
4
4
|
export declare const IMAGE_TYPE = "arkiv-images/v1";
|
|
5
5
|
export declare const INLINE_MAX_BYTES = 120000;
|
|
6
6
|
export declare const MAX_IMAGE_BYTES: number;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ExpirationTime } from '@arkiv-network/sdk';
|
|
|
2
2
|
import { bytes32, key, str, u64 } from '@arkiv-network/sdk/attr';
|
|
3
3
|
import { eq } from '@arkiv-network/sdk/query';
|
|
4
4
|
import { uploadFile, downloadFile, hashFile, ChunkingError, DEFAULT_CHUNK_BYTES } from 'arkiv-chunking';
|
|
5
|
-
export const VERSION = '0.1.
|
|
5
|
+
export const VERSION = '0.1.1';
|
|
6
6
|
export const IMAGE_TYPE = 'arkiv-images/v1';
|
|
7
7
|
export const INLINE_MAX_BYTES = 120_000;
|
|
8
8
|
export const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
|
package/docs/verification.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Verification — arkiv-images 0.1.0
|
|
2
2
|
|
|
3
|
-
Verification date: 2026-09-08. This report distinguishes real Tiramisu transactions, controlled RPC tests and release gates. The npm archive contains the verification snapshot taken before publication; the [source report](https://github.com/SantiagoDevRel/arkiv-images/blob/feat/image-storage/docs/verification.md) records subsequent registry installation evidence.
|
|
3
|
+
Verification date: 2026-09-08. Version 0.1.1 corrects the Tiramisu setup links from the production Hub (still serving retired Braga) to the verified Hub staging pages. The storage implementation is unchanged from 0.1.0; only its exported version changes. Real writes below were made with 0.1.0, and the same entities were read again with 0.1.1. This report distinguishes real Tiramisu transactions, controlled RPC tests and release gates. The npm archive contains the verification snapshot taken before publication; the [source report](https://github.com/SantiagoDevRel/arkiv-images/blob/feat/image-storage/docs/verification.md) records subsequent registry installation evidence.
|
|
4
4
|
|
|
5
5
|
## Tested combination
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ Verification date: 2026-09-08. This report distinguishes real Tiramisu transacti
|
|
|
13
13
|
| Sample tooling | Vite 8.2.2 |
|
|
14
14
|
| Network | Tiramisu, RPC-confirmed chain ID 7738577 |
|
|
15
15
|
| Live signer | Rabby, explicitly authorized Arkiv Wallet; address in the receipt evidence |
|
|
16
|
-
| Browser automation | Playwright 1.62.1,
|
|
16
|
+
| Browser automation | Playwright 1.62.1, Google Chrome 152.0.7977.77; native pixel decoding |
|
|
17
17
|
|
|
18
18
|
Other SDK versions, networks, Firefox/Safari, mobile wallet browsers and a real MetaMask extension signing flow are not claimed tested. MetaMask-compatible EIP-1193 requests and EIP-6963 discovery were exercised with an injected test wallet; Rabby performed the real writes.
|
|
19
19
|
|
|
@@ -43,7 +43,11 @@ Rendered widths: 390, 599, 600, 601, 768 and 1440 CSS pixels. DOM checks found n
|
|
|
43
43
|
|
|
44
44
|
A fresh agent with only the package/sample READMEs, consumer guides and an integration objective installed the packed candidate in a new directory. It ran the exact minimal example, compiled every public type against actual SDK clients, and read the real 120,000-byte image from Tiramisu without a wallet. Invalid format, excessive size, invalid/missing key, unexpected digest and wrong network failed as documented. No blocking documentation gaps were found; its suggestion of a complete anonymous-read example was incorporated.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Version 0.1.0 was published at 2026-09-08T22:09:43Z with SHA-1 78aa4b0593dd4c9a03f0aebe350fb12e37a9b08d. A second clean directory installed it from npm, verified the registry lockfile, compiled every public type with strict TypeScript, executed both documented examples and retrieved both real inline/chunked PNGs without a wallet. The sample also passed npm ci and build from that registry version. All consumer guides were present in the installed package.
|
|
47
|
+
|
|
48
|
+
Claude completed its source/security/logic/browser review after resuming a timed-out session. It independently retrieved all three live images and confirmed the recorded byte equality. It identified stale sample dependency contents and missing publication as release blockers. Both closed: a follow-up independently hashed the published tarball and served JavaScript, confirmed the 128-byte filename gate, and reported no code blockers. Full resolution table: docs/claude-review.md in the source repository.
|
|
49
|
+
|
|
50
|
+
The 0.1.1 archive contains this pre-publication snapshot. Its registry install and final checkout evidence will be added to the source report after publication. Setup destinations were inspected as rendered pages: staging /networks shows Tiramisu 7738577 and the correct RPC, /api-keys offers Tiramisu keys, and /faucet requires connecting/signing in with a wallet. No new key or faucet claim was performed. Production /api-keys returned 404 and /networks still showed Braga; this is why 0.1.1 changes those links.
|
|
47
51
|
|
|
48
52
|
## Preview and scope
|
|
49
53
|
|