@ariestools/aries-dapp-core 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +326 -32
  2. package/dist/bin/dappServer.mjs +685 -104
  3. package/dist/node/DappActor.d.ts +28 -7
  4. package/dist/node/DappActor.d.ts.map +1 -1
  5. package/dist/node/actor/AbstractActor.d.ts +13 -8
  6. package/dist/node/actor/AbstractActor.d.ts.map +1 -1
  7. package/dist/node/actor/types.d.ts +7 -0
  8. package/dist/node/actor/types.d.ts.map +1 -1
  9. package/dist/node/bin/dappServer.d.ts +11 -4
  10. package/dist/node/bin/dappServer.d.ts.map +1 -1
  11. package/dist/node/bin/loadReducer.d.ts +17 -0
  12. package/dist/node/bin/loadReducer.d.ts.map +1 -0
  13. package/dist/node/boot/bootDappActors.d.ts +12 -1
  14. package/dist/node/boot/bootDappActors.d.ts.map +1 -1
  15. package/dist/node/consumer/api.d.ts +56 -0
  16. package/dist/node/consumer/api.d.ts.map +1 -0
  17. package/dist/node/consumer/hash.d.ts +6 -0
  18. package/dist/node/consumer/hash.d.ts.map +1 -0
  19. package/dist/node/consumer/parse.d.ts +18 -0
  20. package/dist/node/consumer/parse.d.ts.map +1 -0
  21. package/dist/node/consumer.d.ts +9 -0
  22. package/dist/node/consumer.d.ts.map +1 -0
  23. package/dist/node/consumer.mjs +330 -0
  24. package/dist/node/consumer.mjs.map +7 -0
  25. package/dist/node/examples/productionComposition.d.ts +118 -0
  26. package/dist/node/examples/productionComposition.d.ts.map +1 -0
  27. package/dist/node/index.d.ts +25 -3
  28. package/dist/node/index.d.ts.map +1 -1
  29. package/dist/node/index.mjs +1730 -68
  30. package/dist/node/index.mjs.map +4 -4
  31. package/dist/node/locator/createDappLocator.d.ts +32 -5
  32. package/dist/node/locator/createDappLocator.d.ts.map +1 -1
  33. package/dist/node/providers/DappBucketStore.d.ts +26 -21
  34. package/dist/node/providers/DappBucketStore.d.ts.map +1 -1
  35. package/dist/node/providers/capabilities.d.ts +49 -0
  36. package/dist/node/providers/capabilities.d.ts.map +1 -0
  37. package/dist/node/providers/errors.d.ts +11 -0
  38. package/dist/node/providers/errors.d.ts.map +1 -0
  39. package/dist/node/providers/hashBytes.d.ts +3 -0
  40. package/dist/node/providers/hashBytes.d.ts.map +1 -0
  41. package/dist/node/providers/joinKey.d.ts +11 -0
  42. package/dist/node/providers/joinKey.d.ts.map +1 -0
  43. package/dist/node/providers/memoryBucketStore.d.ts +14 -0
  44. package/dist/node/providers/memoryBucketStore.d.ts.map +1 -0
  45. package/dist/node/providers/types.d.ts +99 -0
  46. package/dist/node/providers/types.d.ts.map +1 -0
  47. package/dist/node/publication/canonicalHead.d.ts +7 -0
  48. package/dist/node/publication/canonicalHead.d.ts.map +1 -0
  49. package/dist/node/publication/commitHead.d.ts +9 -0
  50. package/dist/node/publication/commitHead.d.ts.map +1 -0
  51. package/dist/node/publication/constants.d.ts +27 -0
  52. package/dist/node/publication/constants.d.ts.map +1 -0
  53. package/dist/node/publication/gc.d.ts +42 -0
  54. package/dist/node/publication/gc.d.ts.map +1 -0
  55. package/dist/node/publication/incremental.d.ts +83 -0
  56. package/dist/node/publication/incremental.d.ts.map +1 -0
  57. package/dist/node/publication/publishGeneration.d.ts +18 -0
  58. package/dist/node/publication/publishGeneration.d.ts.map +1 -0
  59. package/dist/node/publication/putCreateOnly.d.ts +11 -0
  60. package/dist/node/publication/putCreateOnly.d.ts.map +1 -0
  61. package/dist/node/publication/releases.d.ts +49 -0
  62. package/dist/node/publication/releases.d.ts.map +1 -0
  63. package/dist/node/publication/safety.d.ts +55 -0
  64. package/dist/node/publication/safety.d.ts.map +1 -0
  65. package/dist/node/publication/shard.d.ts +7 -0
  66. package/dist/node/publication/shard.d.ts.map +1 -0
  67. package/dist/node/publication/status.d.ts +27 -0
  68. package/dist/node/publication/status.d.ts.map +1 -0
  69. package/dist/node/publication/statusPatch.d.ts +10 -0
  70. package/dist/node/publication/statusPatch.d.ts.map +1 -0
  71. package/dist/node/publication/types.d.ts +138 -0
  72. package/dist/node/publication/types.d.ts.map +1 -0
  73. package/dist/node/publication/writeGenerationObjects.d.ts +5 -0
  74. package/dist/node/publication/writeGenerationObjects.d.ts.map +1 -0
  75. package/dist/node/reducer/DappReducer.d.ts +37 -14
  76. package/dist/node/reducer/DappReducer.d.ts.map +1 -1
  77. package/examples/countFactsReducer.mjs +96 -0
  78. package/package.json +16 -8
package/README.md CHANGED
@@ -1,38 +1,332 @@
1
1
  # @ariestools/aries-dapp-core
2
2
 
3
- > **Dev only.** This is the compute half of the local `aries dapp` backend, a
4
- > development fixture with no auth or metrics and memory-only storage. Not for
5
- > production at this time.
3
+ Dapp backend core: a process-wide provider locator, pluggable reducer, coherent
4
+ generation publication for S3/R2-backed indexes, and the stock `DappActor`
5
+ timer. Designed for **outbound-only** production indexers (no public inbound
6
+ API on the compute process) as well as the local `aries dapp` development
7
+ fixture.
6
8
 
7
- The brains of a local dapp backend. It pairs with the storage server in
8
- [`@ariestools/aries-dapp-serve`](../dapp-serve): the server holds three buckets
9
- (`data`, `state`, `index`), and this package derives the mutable buckets from
10
- the immutable one on an interval.
9
+ ## Development vs production
10
+
11
+ | Surface | Purpose |
12
+ | --- | --- |
13
+ | `aries-dapp-server` bin + `noopReducer` | **Local development only** — embedded s3rver via `@ariestools/aries-dapp-serve`, memory backing, no real derivation |
14
+ | Project-owned entrypoint + real reducer | **Production** — inject an S3/R2 client, bind logical roles to buckets/prefixes, publish immutable generations |
15
+
16
+ `dapp-serve` and the noop daemon remain explicit local tools. Production usage
17
+ is a **project-owned** executable that composes this package; dapp-core does
18
+ not dynamically load arbitrary reducer modules and does not store application
19
+ authority (signing seeds, Webble controller roots, etc.).
20
+
21
+ Published state and indexes are **rebuildable projections**, not authority.
22
+ Public buckets must not be assumed safe for plaintext private material.
11
23
 
12
24
  ## Pieces
13
25
 
14
- - **`AbstractActor`** — a lean, dapp-owned fork of the xl1 `ActorV3` pattern:
15
- the `create`/`start`/`stop` lifecycle plus non-overlapping interval timers
16
- (`registerTimer`) and a readiness contract. It swaps the chain's
17
- `ProviderFactoryLocator`/`LocatorConfig` machinery for the small
18
- `DappProviderLocator` seam, so an actor resolves providers by moniker without
19
- the chain protocol config model. Intended to graduate into a shared cli/actor
20
- toolkit and, later, xl1-protocol where it can re-adopt the real locator.
21
- - **`DappProviderLocator` / `createDappLocator`** — the **process-wide** service
22
- locator, built once and shared across every actor (xl1-cli style). It
23
- pre-registers an S3-backed `DappBucketStore` per bucket keyed by moniker.
24
- - **`DappActor`** — resolves the three stores from the locator and drives its
25
- `DappReducer` on an interval to keep `state`/`index` derived from `data`.
26
- - **`DappReducer`** the pluggable unit of dapp logic (`reduce({data, state,
27
- index, logger})`). `noopReducer` is the default; supply a real one to derive.
28
- - **`bootDappActors`** — creates and starts N actors against one shared locator.
29
-
30
- ## Daemon
31
-
32
- The composed daemon bin (`dist/bin/dappServer.mjs`) starts the storage server
33
- and, in the same process, the locator and actors. `aries dapp up` spawns it.
34
- Env config: `DAPP_PORT` (default 8801), `HOST` (default 127.0.0.1),
35
- `DAPP_BACKING` (default `memory`), `DAPP_PUBLIC_HOST`, `DAPP_TLS_CERT`,
36
- `DAPP_TLS_KEY`, and `DAPP_REDUCE_INTERVAL_MS` (default 5000). Under
37
- `--ssl auto` the daemon exports the local CA via `NODE_EXTRA_CA_CERTS` so the
38
- in-process S3 client trusts the HTTPS origin.
26
+ - **`AbstractActor`** — create/start/stop lifecycle, non-overlapping timers
27
+ (`firstRunDelay` is the real delay before the first pass), abort signal on
28
+ stop, optional shutdown bound for in-flight work, readiness contract.
29
+ - **`DappProviderLocator` / `createDappLocator`** process-wide locator. One
30
+ shared S3 client across roles, or **per-role clients** for least-privilege
31
+ principals; logical role physical bucket + prefix + optional public CDN base.
32
+ The **data** role is registered read-only by default (`put` throws).
33
+ - **`DappObjectReader` / `DappObjectWriter` / `DappBucketStore`** — capability
34
+ split: reducers see `data` as read-only; state/index remain writers.
35
+ get/getWithMeta/stat/put (metadata + conditional writes)/list*/destroy.
36
+ - **`publishGeneration` / `readPublishedHead`** — fenced publication: create-only
37
+ generation objects, head-last with `conditional-head` CAS or injected lease
38
+ (or `unfenced` for local s3rver only). Returns `{ ok: true, … }` or
39
+ `{ ok: false, conflict }`.
40
+ - **`writeIndexerStatus` / `readIndexerStatus`** — durable floor/cursor,
41
+ completed position, observed source head, generation, success/error
42
+ timestamps, consecutive failures (CDN-revalidate).
43
+ - **`DappActor`** — drives a `DappReducer` on an interval; optional status
44
+ publication; optional `maxConsecutiveFailures` for supervisor restarts.
45
+ - **`bootDappActors`** starts N actors, runs readiness, stops earlier actors
46
+ if a later one fails.
47
+
48
+ ## Production composition (outbound-only)
49
+
50
+ ```ts
51
+ import { S3Client } from '@aws-sdk/client-s3'
52
+ import {
53
+ bootDappActors,
54
+ createDappLocator,
55
+ publishGeneration,
56
+ type DappReducer,
57
+ } from '@ariestools/aries-dapp-core'
58
+
59
+ const client = new S3Client({
60
+ region: 'auto',
61
+ endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
62
+ forcePathStyle: true,
63
+ credentials: {
64
+ accessKeyId: process.env.R2_ACCESS_KEY_ID!,
65
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
66
+ },
67
+ })
68
+
69
+ const locator = createDappLocator({
70
+ client,
71
+ ownClient: true,
72
+ bindings: {
73
+ data: {
74
+ bucket: 'my-dapp',
75
+ prefix: 'data',
76
+ publicBaseUrl: 'https://cdn.example.com/data',
77
+ },
78
+ state: {
79
+ bucket: 'my-dapp',
80
+ prefix: 'state',
81
+ publicBaseUrl: 'https://cdn.example.com/state',
82
+ },
83
+ index: {
84
+ bucket: 'my-dapp',
85
+ prefix: 'index',
86
+ publicBaseUrl: 'https://cdn.example.com/index',
87
+ },
88
+ },
89
+ })
90
+
91
+ const reducer: DappReducer = {
92
+ name: 'my-app-indexer',
93
+ version: '1',
94
+ async reduce({ state, index, signal, logger }) {
95
+ const published = await publishGeneration({
96
+ state,
97
+ index,
98
+ safety: { mode: 'conditional-head' }, // fail closed if provider lacks CAS
99
+ // expectedHeadEtag: prior?.headEtag,
100
+ reducer: { name: 'my-app-indexer', version: '1' },
101
+ source: { cursor: '…', completedPosition: '…', observedHead: '…' },
102
+ stateObjects: [{ key: 'view.json', body: '{}', contentType: 'application/json' }],
103
+ indexObjects: [{ key: 'by-id.json', body: '{}', contentType: 'application/json' }],
104
+ signal,
105
+ })
106
+ if (!published.ok) {
107
+ logger.warn(`publish conflict: ${published.conflict.kind} ${published.conflict.message}`)
108
+ return // do not advance checkpoint on conflict
109
+ }
110
+ logger.info(`published ${published.generation}`)
111
+ return {
112
+ cursor: '…',
113
+ lastCompletedPosition: '…',
114
+ observedSourceHead: '…',
115
+ generation: published.generation,
116
+ generationRoot: published.manifestKey,
117
+ }
118
+ },
119
+ }
120
+
121
+ const actors = await bootDappActors(locator, [{
122
+ name: 'MyIndexer',
123
+ reducer,
124
+ firstRunDelayMs: 1_000,
125
+ reduceIntervalMs: 15_000,
126
+ maxConsecutiveFailures: 10,
127
+ publishStatus: true,
128
+ }])
129
+ ```
130
+
131
+ Clients never contact this process. They read `head.json`, generation objects,
132
+ and `status.json` from the public CDN/base URLs. Credentials stay out of
133
+ published config and logs.
134
+
135
+ See also `src/examples/productionComposition.ts`.
136
+
137
+ ### Publication layout
138
+
139
+ ```
140
+ state/head.json # mutable head (revalidate CDN)
141
+ state/status.json # indexer status (revalidate CDN)
142
+ state/generations/<id>/manifest.json # immutable
143
+ state/generations/<id>/… # immutable state objects
144
+ index/generations/<id>/… # immutable index objects
145
+ ```
146
+
147
+ Head is written **last**. A crash before head leaves the previous complete
148
+ generation visible. Checkpoints must not advance past a fully published
149
+ generation (the actor only records generation after a successful reduce that
150
+ published head).
151
+
152
+ ## Local daemon (dev only)
153
+
154
+ The composed bin (`dist/bin/dappServer.mjs`) starts the storage server and a
155
+ DappActor. `aries dapp up` spawns it.
156
+
157
+ ### CLI
158
+
159
+ ```sh
160
+ # Empty spine (noop reducer)
161
+ aries dapp up
162
+
163
+ # Real project reducer (ESM .mjs or package export — TypeScript not supported)
164
+ aries dapp up --reducer ./dist/indexer.mjs
165
+ aries dapp up --reducer ./dist/indexer.mjs --reducer-export myIndex
166
+ aries dapp up --reducer @myorg/my-dapp/indexer
167
+
168
+ # Persist buckets across restarts
169
+ aries dapp up --backing disk --data-dir ~/.aries/dapp/data
170
+ ```
171
+
172
+ Example reducer shipped with this package:
173
+
174
+ ```sh
175
+ aries dapp up --reducer ./node_modules/@ariestools/aries-dapp-core/examples/countFactsReducer.mjs
176
+ # monorepo:
177
+ aries dapp up --reducer ./packages/dapp-core/examples/countFactsReducer.mjs
178
+ ```
179
+
180
+ ### Reducer module contract
181
+
182
+ The module must export a `DappReducer`:
183
+
184
+ - **default export**, or
185
+ - named export `reducer`, or
186
+ - any name via `--reducer-export` / `DAPP_REDUCER_EXPORT`
187
+
188
+ ```js
189
+ // dist/indexer.mjs
190
+ export default {
191
+ name: 'my-index',
192
+ version: '1',
193
+ async reduce({ data, state, index, signal, logger }) {
194
+ // read data, publishGeneration / publishIncremental, return progress
195
+ },
196
+ }
197
+ ```
198
+
199
+ Local publication should use `safety: { mode: 'unfenced' }` (or honor
200
+ `DAPP_PUBLICATION_SAFETY`, which the daemon sets to `unfenced` by default).
201
+ s3rver does not support reliable head CAS.
202
+
203
+ ### Env (daemon)
204
+
205
+ | Variable | Default | Notes |
206
+ | --- | --- | --- |
207
+ | `DAPP_PORT` | `8801` | |
208
+ | `HOST` | `127.0.0.1` | |
209
+ | `DAPP_BACKING` | `memory` | `memory` \| `disk` |
210
+ | `DAPP_DATA_DIR` | `~/.aries/dapp/data` | disk root |
211
+ | `DAPP_REDUCER` | _(noop)_ | path to `.mjs`/`.js` or package export |
212
+ | `DAPP_REDUCER_EXPORT` | default / `reducer` | named export |
213
+ | `DAPP_REDUCE_INTERVAL_MS` | `5000` | |
214
+ | `DAPP_FIRST_RUN_DELAY_MS` | `0` | |
215
+ | `DAPP_PUBLICATION_SAFETY` | `unfenced` | set by daemon for project reducers |
216
+ | `DAPP_PUBLIC_HOST` / `DAPP_TLS_*` | | `--ssl auto` |
217
+
218
+ Under `--ssl auto` the daemon exports the local CA via `NODE_EXTRA_CA_CERTS`.
219
+
220
+ ## Production entrypoint (not the CLI bin)
221
+
222
+ Do **not** run `aries-dapp-server` or `aries dapp up` in production. Write a
223
+ project-owned process that:
224
+
225
+ 1. Builds an `S3Client` for R2/S3 (credentials from env/IAM — never logged)
226
+ 2. Calls `createDappLocator({ client, bindings, ownClient: true })`
227
+ 3. Implements a `DappReducer` (import statically — no dynamic CLI load required)
228
+ 4. Calls `bootDappActors` with `safety: { mode: 'conditional-head' }` inside
229
+ `publishGeneration` / `publishIncremental`
230
+ 5. Listens for SIGTERM and stops actors + `locator.destroy()`
231
+
232
+ See `examples/productionComposition.ts` and the composition section above.
233
+ There is no inbound public listener; clients read head/status/objects from CDN.
234
+
235
+ ## Least-privilege IAM / R2 notes
236
+
237
+ - **Writer (private API):** state/index need `s3:GetObject`, `s3:PutObject`, and
238
+ usually `s3:ListBucket` on the role prefix. Data role: `GetObject`/`ListBucket`
239
+ only (no `PutObject`). Prefer distinct access keys via `createDappLocator({ clients: { data, state, index } })`.
240
+ - **Public CDN:** allow anonymous `GET`/`HEAD` only. **Deny** `s3:ListBucket` and
241
+ all `Put*`/`Delete*` for the public principal. dapp-core never requires public
242
+ LIST or PUT.
243
+ - **Credentials** must not appear in published head/manifest/status or logs.
244
+
245
+ ## Publication safety
246
+
247
+ | Mode | Use |
248
+ | --- | --- |
249
+ | `conditional-head` | Production when the provider supports If-Match / If-None-Match |
250
+ | `lease` | Production with an application-injected lease/fencing adapter |
251
+ | `unfenced` | **Local s3rver / tests only** — no concurrent-writer safety |
252
+
253
+ ## Releases, pins, rollback, GC
254
+
255
+ ```ts
256
+ import {
257
+ listReleases, pinRelease, rollbackHead,
258
+ planGarbageCollection, runGarbageCollection,
259
+ } from '@ariestools/aries-dapp-core'
260
+
261
+ await pinRelease(state, 'gen-a', { reason: 'hotfix' })
262
+ await rollbackHead(state, {
263
+ targetGeneration: 'gen-a',
264
+ safety: { mode: 'conditional-head' },
265
+ expectedHeadEtag: currentEtag,
266
+ })
267
+
268
+ const plan = await planGarbageCollection({
269
+ state,
270
+ index,
271
+ policy: { mode: 'bounded', retainGenerations: 5, cdnGraceMs: 86_400_000 },
272
+ })
273
+ await runGarbageCollection(plan, { state, index }, { dryRun: true })
274
+ ```
275
+
276
+ Source **data** is never GC'd. Active head, pins, and in-flight generations are protected.
277
+
278
+ ## Incremental high-cardinality publication
279
+
280
+ For large sparsely-changing indexes use `publishIncremental` (CAS bodies + 256-way
281
+ sharded map). Unchanged keys keep stable `cas/sha256/<hash>` URLs; each pass
282
+ writes only changed objects + touched shards + root + receipt.
283
+
284
+ ```ts
285
+ import { publishIncremental } from '@ariestools/aries-dapp-core'
286
+
287
+ const result = await publishIncremental({
288
+ state,
289
+ index,
290
+ safety: { mode: 'conditional-head' },
291
+ expectedHeadEtag: prior?.headEtag,
292
+ reducer: { name: 'addr-index', version: '1' },
293
+ indexChanges: [{ key: 'by-source/0xabc', body: frameJson, contentType: 'application/json' }],
294
+ })
295
+ ```
296
+
297
+ ## Browser-neutral consumer
298
+
299
+ ```ts
300
+ import {
301
+ verifyPublishedView, resolveObject,
302
+ } from '@ariestools/aries-dapp-core/consumer'
303
+
304
+ const view = await verifyPublishedView({
305
+ fetch,
306
+ bases: { state: 'https://cdn.example/state', index: 'https://cdn.example/index' },
307
+ })
308
+ const one = await resolveObject({
309
+ fetch,
310
+ bases: { state: 'https://cdn.example/state', index: 'https://cdn.example/index' },
311
+ logicalKey: 'by-source/0xabc',
312
+ role: 'index',
313
+ })
314
+ ```
315
+
316
+ No Node built-ins or AWS SDK on this subpath.
317
+
318
+ ## Opt-in R2 contract test
319
+
320
+ Ordinary CI does not require R2 secrets. With credentials:
321
+
322
+ ```sh
323
+ export DAPP_R2_CONTRACT_TEST=1
324
+ export R2_ACCOUNT_ID=…
325
+ export R2_ACCESS_KEY_ID=…
326
+ export R2_SECRET_ACCESS_KEY=…
327
+ export R2_BUCKET=…
328
+ # optional: R2_PUBLIC_BASE_URL=https://pub-….r2.dev
329
+ pnpm xy test @ariestools/aries-dapp-core
330
+ ```
331
+
332
+ See `src/spec/r2.contract.spec.ts`.