@ariestools/cli 0.1.8 → 0.1.10

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.
@@ -0,0 +1,970 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { DAPP_BACKINGS, DAPP_BUCKETS, isDappBackingKind, resolveBacking, startDappServer } from "@ariestools/aries-dapp-serve";
7
+ import { AbstractCreatable, ConsoleLogger, IdLogger, assertEx, creatable } from "@ariestools/sdk";
8
+ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
9
+ import { pathToFileURL } from "node:url";
10
+ //#region src/actor/AbstractActor.ts
11
+ function createDeferred$1() {
12
+ let resolve;
13
+ let reject;
14
+ const promise = new Promise((res, rej) => {
15
+ resolve = res;
16
+ reject = rej;
17
+ });
18
+ promise.catch(() => {});
19
+ return {
20
+ promise,
21
+ reject,
22
+ resolve
23
+ };
24
+ }
25
+ /**
26
+ * Lean, dapp-owned fork of the xl1 `ActorV3` pattern: the create/start/stop
27
+ * lifecycle from `AbstractCreatable`, plus non-overlapping interval timers and
28
+ * a readiness contract. It deliberately swaps the chain's
29
+ * `ProviderFactoryLocator`/`LocatorConfig` machinery for the small
30
+ * {@link DappProviderLocator} seam, so a dapp actor resolves providers by
31
+ * moniker without dragging the chain protocol config model in.
32
+ *
33
+ * The locator is process-wide: the daemon builds it once and passes the same
34
+ * instance to every actor via `params.locator` (see `bootDappActors`). When
35
+ * this graduates into a shared cli/actor toolkit — and, later, xl1-protocol —
36
+ * this base can re-adopt the real locator.
37
+ */
38
+ var AbstractActor = class extends AbstractCreatable {
39
+ _intervals = /* @__PURE__ */ new Map();
40
+ _timeouts = /* @__PURE__ */ new Map();
41
+ _abortController = new AbortController();
42
+ _idLogger;
43
+ _inFlight = /* @__PURE__ */ new Map();
44
+ _readyDeferred = createDeferred$1();
45
+ _readyError;
46
+ _readyState = "pending";
47
+ get logger() {
48
+ this._idLogger ??= new IdLogger(assertEx(this.context.logger, () => `Logger is required in context for actor ${this.name}.`), () => this.name);
49
+ return this._idLogger;
50
+ }
51
+ get readyError() {
52
+ return this._readyError;
53
+ }
54
+ get readyState() {
55
+ return this._readyState;
56
+ }
57
+ get context() {
58
+ return this.locator.context;
59
+ }
60
+ get locator() {
61
+ return this.params.locator;
62
+ }
63
+ /** Abort signal cancelled when the actor stops (or is replaced on restart). */
64
+ get signal() {
65
+ return this._abortController.signal;
66
+ }
67
+ static async paramsHandler(params) {
68
+ const inParams = params ?? {};
69
+ const baseParams = await super.paramsHandler({
70
+ ...inParams,
71
+ name: inParams.name ?? "UnknownActor"
72
+ });
73
+ const locator = assertEx(inParams.locator, () => `params.locator is required for actor ${String(baseParams.name)}.`);
74
+ return {
75
+ ...baseParams,
76
+ locator
77
+ };
78
+ }
79
+ /** Override to prove the actor can do useful work. Default: no-op. */
80
+ async readyHandler() {}
81
+ /**
82
+ * Register a recurring task. The first invocation fires after `dueTimeMs`
83
+ * (the documented first-run delay); subsequent invocations fire every
84
+ * `periodMs` while the actor is `started`. A run is skipped if the previous
85
+ * one is still in flight, so a slow pass can never stack on top of itself.
86
+ * Must be called from `startHandler` (guards on `starting` status).
87
+ */
88
+ registerTimer(timerName, callback, dueTimeMs, periodMs) {
89
+ if (this.status !== "starting") {
90
+ this.logger.warn(`Cannot register timer '${timerName}' because actor is not starting.`);
91
+ return;
92
+ }
93
+ const tick = () => {
94
+ if (this.status !== "started") return;
95
+ if (this._inFlight.has(timerName)) {
96
+ this.logger.warn(`Skipping timer '${this.name}:${timerName}' because the previous run is still in flight.`);
97
+ return;
98
+ }
99
+ const run = (async () => {
100
+ const startTime = Date.now();
101
+ try {
102
+ await callback();
103
+ } catch (error) {
104
+ const err = error instanceof Error ? error : new Error(String(error));
105
+ this.logger.error(`Error in timer '${this.name}:${timerName}': ${err.message}`);
106
+ } finally {
107
+ const duration = Date.now() - startTime;
108
+ if (duration > periodMs) this.logger.warn(`Timer '${this.name}:${timerName}' took ${duration}ms, longer than its ${periodMs}ms period.`);
109
+ }
110
+ })();
111
+ this._inFlight.set(timerName, run.finally(() => {
112
+ this._inFlight.delete(timerName);
113
+ }));
114
+ };
115
+ const timeoutId = setTimeout(() => {
116
+ if (this.status !== "started") return;
117
+ this._intervals.set(timerName, setInterval(tick, periodMs));
118
+ tick();
119
+ }, dueTimeMs);
120
+ this._timeouts.set(timerName, timeoutId);
121
+ }
122
+ /**
123
+ * Run the warm-pass once. The standard boot path invokes this after
124
+ * `start()` so readiness either resolves or rejects.
125
+ */
126
+ async runReadyHandler() {
127
+ if (this._readyState !== "pending") return;
128
+ try {
129
+ await this.readyHandler();
130
+ this._readyState = "ready";
131
+ this._readyDeferred.resolve();
132
+ } catch (error) {
133
+ const err = error instanceof Error ? error : new Error(String(error));
134
+ this._readyState = "failed";
135
+ this._readyError = err;
136
+ this._readyDeferred.reject(err);
137
+ throw err;
138
+ }
139
+ }
140
+ async startHandler() {
141
+ if (this._abortController.signal.aborted) this._abortController = new AbortController();
142
+ await super.startHandler();
143
+ }
144
+ async stopHandler() {
145
+ await super.stopHandler();
146
+ this._abortController.abort();
147
+ for (const timeoutRef of this._timeouts.values()) clearTimeout(timeoutRef);
148
+ this._timeouts.clear();
149
+ for (const intervalRef of this._intervals.values()) clearInterval(intervalRef);
150
+ this._intervals.clear();
151
+ const inFlight = [...this._inFlight.values()];
152
+ this._inFlight.clear();
153
+ const timeoutMs = this.params.shutdownTimeoutMs;
154
+ if (timeoutMs === void 0) {
155
+ await Promise.allSettled(inFlight);
156
+ return;
157
+ }
158
+ await Promise.race([Promise.allSettled(inFlight), new Promise((resolve) => {
159
+ setTimeout(resolve, timeoutMs);
160
+ })]);
161
+ }
162
+ async whenReady() {
163
+ await this._readyDeferred.promise;
164
+ }
165
+ };
166
+ //#endregion
167
+ //#region src/providers/capabilities.ts
168
+ /** Thrown when a read-only capability wrapper receives a write. */
169
+ var ReadOnlyStoreError = class extends Error {
170
+ name = "ReadOnlyStoreError";
171
+ constructor(role, operation = "put") {
172
+ super(`Store role "${role}" is read-only; ${operation} is not permitted`);
173
+ }
174
+ };
175
+ function bindReader(store) {
176
+ return {
177
+ role: store.role,
178
+ bucket: store.bucket,
179
+ prefix: store.prefix,
180
+ ...store.publicBaseUrl !== void 0 && { publicBaseUrl: store.publicBaseUrl },
181
+ destroy: () => store.destroy(),
182
+ get: (key) => store.get(key),
183
+ getWithMeta: (key) => store.getWithMeta(key),
184
+ list: (listPrefix) => store.list(listPrefix),
185
+ listAsync: (listPrefix) => store.listAsync(listPrefix),
186
+ listPage: (options) => store.listPage(options),
187
+ publicUrl: (key) => store.publicUrl(key),
188
+ resolveKey: (key) => store.resolveKey(key),
189
+ stat: (key) => store.stat(key)
190
+ };
191
+ }
192
+ /**
193
+ * Wrap any writer as a read-only reader. Other methods and destroy delegate to
194
+ * the underlying store.
195
+ */
196
+ function asReadOnly(store) {
197
+ return bindReader(store);
198
+ }
199
+ /**
200
+ * Wrap a writer so `put` fails even if the caller still holds the writer type
201
+ * (defense in depth for the data-role locator registration).
202
+ */
203
+ function asReadOnlyWriter(store) {
204
+ return {
205
+ ...bindReader(store),
206
+ async put() {
207
+ throw new ReadOnlyStoreError(store.role, "put");
208
+ },
209
+ async delete() {
210
+ throw new ReadOnlyStoreError(store.role, "delete");
211
+ }
212
+ };
213
+ }
214
+ //#endregion
215
+ //#region src/providers/monikers.ts
216
+ /**
217
+ * Provider monikers for the three dapp buckets. An actor declares these in its
218
+ * `static dependencies` and resolves them from the locator at create time.
219
+ */
220
+ const DappDataStoreMoniker = "DappDataStore";
221
+ const DappStateStoreMoniker = "DappStateStore";
222
+ const DappIndexStoreMoniker = "DappIndexStore";
223
+ //#endregion
224
+ //#region src/publication/constants.ts
225
+ /** Well-known relative keys published by the coherent generation protocol. */
226
+ const HEAD_KEY = "head.json";
227
+ const STATUS_KEY = "status.json";
228
+ /** CDN caching: mutable head and status objects that must revalidate. */
229
+ const CACHE_CONTROL_REVALIDATE = "public, max-age=0, must-revalidate";
230
+ const INDEXER_STATUS_SCHEMA = "network.aries.dapp.indexer.status";
231
+ //#endregion
232
+ //#region src/publication/status.ts
233
+ function encodeStatus(status) {
234
+ return `${JSON.stringify(status, null, 2)}\n`;
235
+ }
236
+ /**
237
+ * Read the durable indexer status object from a state (or status) store.
238
+ * Returns undefined when no status has been published yet.
239
+ */
240
+ async function readIndexerStatus(store, key = STATUS_KEY) {
241
+ const body = await store.get(key);
242
+ if (body === void 0) return void 0;
243
+ const parsed = JSON.parse(new TextDecoder().decode(body));
244
+ if (parsed === null || typeof parsed !== "object") throw new Error(`Invalid indexer status at "${key}": not an object`);
245
+ const record = parsed;
246
+ if (record.schema !== "network.aries.dapp.indexer.status") throw new Error(`Invalid indexer status schema at "${key}": ${String(record.schema)}`);
247
+ return record;
248
+ }
249
+ /**
250
+ * Publish a machine-readable status object with revalidate CDN caching.
251
+ * Status never carries credentials or private application material.
252
+ */
253
+ async function writeIndexerStatus(store, input) {
254
+ const key = input.key ?? "status.json";
255
+ const status = {
256
+ schema: INDEXER_STATUS_SCHEMA,
257
+ consecutiveFailures: input.consecutiveFailures,
258
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
259
+ ...input.floor !== void 0 && { floor: input.floor },
260
+ ...input.cursor !== void 0 && { cursor: input.cursor },
261
+ ...input.lastCompletedPosition !== void 0 && { lastCompletedPosition: input.lastCompletedPosition },
262
+ ...input.observedSourceHead !== void 0 && { observedSourceHead: input.observedSourceHead },
263
+ ...input.generation !== void 0 && { generation: input.generation },
264
+ ...input.generationRoot !== void 0 && { generationRoot: input.generationRoot },
265
+ ...input.lastSuccessAt !== void 0 && { lastSuccessAt: input.lastSuccessAt },
266
+ ...input.lastErrorAt !== void 0 && { lastErrorAt: input.lastErrorAt },
267
+ ...input.lastError !== void 0 && { lastError: input.lastError }
268
+ };
269
+ await store.put(key, encodeStatus(status), {
270
+ contentType: "application/json",
271
+ cacheControl: CACHE_CONTROL_REVALIDATE
272
+ });
273
+ return status;
274
+ }
275
+ //#endregion
276
+ //#region src/publication/statusPatch.ts
277
+ function pickDefined(source, keys) {
278
+ const out = {};
279
+ for (const key of keys) {
280
+ const value = source[key];
281
+ if (value !== void 0) out[key] = value;
282
+ }
283
+ return out;
284
+ }
285
+ const PROGRESS_KEYS = [
286
+ "floor",
287
+ "cursor",
288
+ "lastCompletedPosition",
289
+ "observedSourceHead",
290
+ "generation",
291
+ "generationRoot"
292
+ ];
293
+ /** Carry forward durable progress fields from a prior status object. */
294
+ function priorProgressFields(prior) {
295
+ if (prior === void 0) return {};
296
+ return {
297
+ ...pickDefined(prior, PROGRESS_KEYS),
298
+ ...prior.lastSuccessAt !== void 0 && { lastSuccessAt: prior.lastSuccessAt }
299
+ };
300
+ }
301
+ /** Build success status fields from reduce progress, falling back to prior. */
302
+ function successStatusFields(progress, prior, now) {
303
+ const fromProgress = progress === void 0 ? {} : pickDefined(progress, PROGRESS_KEYS);
304
+ const generation = progress?.generation ?? prior?.generation;
305
+ const generationRoot = progress?.generationRoot ?? prior?.generationRoot;
306
+ return {
307
+ consecutiveFailures: 0,
308
+ ...fromProgress,
309
+ ...generation !== void 0 && { generation },
310
+ ...generationRoot !== void 0 && { generationRoot },
311
+ lastSuccessAt: now,
312
+ ...prior?.lastError !== void 0 && { lastError: prior.lastError },
313
+ ...prior?.lastErrorAt !== void 0 && { lastErrorAt: prior.lastErrorAt }
314
+ };
315
+ }
316
+ /** Build failure status fields while preserving prior checkpoint progress. */
317
+ function failureStatusFields(prior, consecutiveFailures, error, now) {
318
+ return {
319
+ consecutiveFailures,
320
+ ...priorProgressFields(prior),
321
+ lastError: error.message,
322
+ lastErrorAt: now
323
+ };
324
+ }
325
+ //#endregion
326
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/decorate.js
327
+ function __decorate(decorators, target, key, desc) {
328
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
329
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
330
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
331
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
332
+ }
333
+ //#endregion
334
+ //#region src/DappActor.ts
335
+ const DEFAULT_REDUCE_INTERVAL_MS = 5e3;
336
+ const DEFAULT_FIRST_RUN_DELAY_MS = 2e3;
337
+ const REDUCE_TIMER = "DappReduce";
338
+ function createDeferred() {
339
+ let resolve;
340
+ let reject;
341
+ const promise = new Promise((res, rej) => {
342
+ resolve = res;
343
+ reject = rej;
344
+ });
345
+ promise.catch(() => {});
346
+ return {
347
+ promise,
348
+ reject,
349
+ resolve
350
+ };
351
+ }
352
+ function withDefaultGenerationRoot(result) {
353
+ if (result === void 0) return;
354
+ if (result.generation === void 0 || result.generationRoot !== void 0) return result;
355
+ return {
356
+ ...result,
357
+ generationRoot: HEAD_KEY
358
+ };
359
+ }
360
+ let DappActor = class DappActor extends AbstractActor {
361
+ static dependencies = [
362
+ DappDataStoreMoniker,
363
+ DappStateStoreMoniker,
364
+ DappIndexStoreMoniker
365
+ ];
366
+ _data;
367
+ _index;
368
+ _state;
369
+ _consecutiveFailures = 0;
370
+ _firstPass = createDeferred();
371
+ _firstPassSettled = false;
372
+ /** Consecutive reduce failures since the last success. */
373
+ get consecutiveFailures() {
374
+ return this._consecutiveFailures;
375
+ }
376
+ async createHandler() {
377
+ await super.createHandler();
378
+ const dataWriter = await this.locator.getInstance(DappDataStoreMoniker);
379
+ this._data = asReadOnly(dataWriter);
380
+ this._state = await this.locator.getInstance(DappStateStoreMoniker);
381
+ this._index = await this.locator.getInstance(DappIndexStoreMoniker);
382
+ }
383
+ async readyHandler() {
384
+ await this._firstPass.promise;
385
+ }
386
+ async startHandler() {
387
+ await super.startHandler();
388
+ const interval = this.params.reduceIntervalMs ?? DEFAULT_REDUCE_INTERVAL_MS;
389
+ const firstRunDelay = this.params.firstRunDelayMs ?? DEFAULT_FIRST_RUN_DELAY_MS;
390
+ this.registerTimer(REDUCE_TIMER, async () => {
391
+ await this.runReducePass();
392
+ }, firstRunDelay, interval);
393
+ this.logger.info(`DappActor started: reducer '${this.params.reducer.name}' every ${interval}ms (first run in ${firstRunDelay}ms)`);
394
+ }
395
+ async stopHandler() {
396
+ this.settleFirstPass(/* @__PURE__ */ new Error("Actor stopped before first reduce pass completed"));
397
+ await super.stopHandler();
398
+ }
399
+ async publishStatusFromProgress(progress, error) {
400
+ if (this.params.publishStatus === false) return;
401
+ let prior;
402
+ try {
403
+ prior = await readIndexerStatus(this._state);
404
+ } catch {
405
+ prior = void 0;
406
+ }
407
+ const now = (/* @__PURE__ */ new Date()).toISOString();
408
+ const fields = error === void 0 ? successStatusFields(progress, prior, now) : failureStatusFields(prior, this._consecutiveFailures, error, now);
409
+ await writeIndexerStatus(this._state, fields);
410
+ }
411
+ async runReducePass() {
412
+ try {
413
+ const result = await this.params.reducer.reduce({
414
+ data: this._data,
415
+ index: this._index,
416
+ logger: this.logger,
417
+ signal: this.signal,
418
+ state: this._state
419
+ });
420
+ const progress = withDefaultGenerationRoot(result === void 0 ? void 0 : result);
421
+ this._consecutiveFailures = 0;
422
+ await this.safePublishStatus(progress);
423
+ this.settleFirstPass();
424
+ } catch (error) {
425
+ const err = error instanceof Error ? error : new Error(String(error));
426
+ this._consecutiveFailures += 1;
427
+ await this.safePublishStatus(void 0, err);
428
+ this.settleFirstPass(err);
429
+ this.logger.error(`Error in reduce pass for '${this.params.reducer.name}': ${err.message}`);
430
+ const max = this.params.maxConsecutiveFailures;
431
+ if (max !== void 0 && this._consecutiveFailures >= max) {
432
+ this.logger.error(`Reducer '${this.params.reducer.name}' failed ${this._consecutiveFailures} consecutive times; stopping actor`);
433
+ setTimeout(() => {
434
+ this.stop();
435
+ }, 0);
436
+ }
437
+ }
438
+ }
439
+ async safePublishStatus(progress, error) {
440
+ try {
441
+ await this.publishStatusFromProgress(progress, error);
442
+ } catch (statusError) {
443
+ this.logger.error(`Failed to publish indexer status: ${String(statusError)}`);
444
+ }
445
+ }
446
+ settleFirstPass(error) {
447
+ if (this._firstPassSettled) return;
448
+ this._firstPassSettled = true;
449
+ if (error === void 0) this._firstPass.resolve();
450
+ else this._firstPass.reject(error);
451
+ }
452
+ };
453
+ DappActor = __decorate([creatable()], DappActor);
454
+ //#endregion
455
+ //#region src/boot/bootDappActors.ts
456
+ /**
457
+ * Creates and starts one {@link DappActor} per spec, all sharing the single
458
+ * process-wide locator. On partial failure, actors already started are stopped
459
+ * before the error is rethrown. Returns them so the caller can stop them on
460
+ * shutdown.
461
+ */
462
+ async function bootDappActors(locator, specs) {
463
+ const actors = [];
464
+ try {
465
+ for (const spec of specs) {
466
+ const params = {
467
+ locator,
468
+ name: spec.name,
469
+ reducer: spec.reducer,
470
+ ...spec.reduceIntervalMs !== void 0 && { reduceIntervalMs: spec.reduceIntervalMs },
471
+ ...spec.firstRunDelayMs !== void 0 && { firstRunDelayMs: spec.firstRunDelayMs },
472
+ ...spec.maxConsecutiveFailures !== void 0 && { maxConsecutiveFailures: spec.maxConsecutiveFailures },
473
+ ...spec.publishStatus !== void 0 && { publishStatus: spec.publishStatus },
474
+ ...spec.shutdownTimeoutMs !== void 0 && { shutdownTimeoutMs: spec.shutdownTimeoutMs }
475
+ };
476
+ const actor = await DappActor.create(params);
477
+ await actor.start();
478
+ actors.push(actor);
479
+ if (spec.runReady !== false) await actor.runReadyHandler();
480
+ }
481
+ return actors;
482
+ } catch (error) {
483
+ for (const actor of [...actors].reverse()) try {
484
+ await actor.stop();
485
+ } catch {}
486
+ throw error;
487
+ }
488
+ }
489
+ //#endregion
490
+ //#region src/providers/errors.ts
491
+ /**
492
+ * Thrown when a conditional put fails (object already exists under
493
+ * `ifNotExists`, or ETag mismatch under `ifMatch`).
494
+ */
495
+ var ConditionalWriteError = class extends Error {
496
+ key;
497
+ name = "ConditionalWriteError";
498
+ constructor(key, message) {
499
+ super(message ?? `Conditional write failed for key "${key}"`);
500
+ this.key = key;
501
+ }
502
+ };
503
+ //#endregion
504
+ //#region src/providers/joinKey.ts
505
+ /**
506
+ * Join a role prefix with a relative object key into a physical S3 key.
507
+ * Empty prefix yields the relative key unchanged; empty relative yields the
508
+ * prefix without a trailing slash.
509
+ */
510
+ function joinKey(prefix, relativeKey) {
511
+ const cleanPrefix = prefix.replaceAll(/^\/+|\/+$/g, "");
512
+ const cleanKey = relativeKey.replaceAll(/^\/+/g, "");
513
+ if (cleanPrefix.length === 0) return cleanKey;
514
+ if (cleanKey.length === 0) return cleanPrefix;
515
+ return `${cleanPrefix}/${cleanKey}`;
516
+ }
517
+ /** Strip a role prefix from a physical key, returning the relative key. */
518
+ function stripPrefix(prefix, physicalKey) {
519
+ const cleanPrefix = prefix.replaceAll(/^\/+|\/+$/g, "");
520
+ if (cleanPrefix.length === 0) return physicalKey;
521
+ const withSlash = `${cleanPrefix}/`;
522
+ if (physicalKey === cleanPrefix) return "";
523
+ if (physicalKey.startsWith(withSlash)) return physicalKey.slice(withSlash.length);
524
+ return physicalKey;
525
+ }
526
+ /** Normalize an optional public base URL (no trailing slash). */
527
+ function normalizePublicBaseUrl(url) {
528
+ if (url === void 0 || url.length === 0) return void 0;
529
+ return url.replace(/\/+$/, "");
530
+ }
531
+ //#endregion
532
+ //#region src/providers/DappBucketStore.ts
533
+ const DEFAULT_CREDENTIAL = "S3RVER";
534
+ const DEFAULT_REGION = "us-east-1";
535
+ function isNotFound(error) {
536
+ const name = error.name;
537
+ const status = error.$metadata?.httpStatusCode;
538
+ return name === "NoSuchKey" || name === "NotFound" || name === "404" || status === 404;
539
+ }
540
+ function isConditionalFailure(error) {
541
+ const name = error.name;
542
+ const status = error.$metadata?.httpStatusCode;
543
+ return name === "PreconditionFailed" || name === "ConditionalRequestConflict" || name === "412" || status === 412;
544
+ }
545
+ function toBodyBytes(body) {
546
+ return typeof body === "string" ? new TextEncoder().encode(body) : body;
547
+ }
548
+ function metaFromResponse(response) {
549
+ return {
550
+ ...response.CacheControl !== void 0 && { cacheControl: response.CacheControl },
551
+ ...response.ContentEncoding !== void 0 && { contentEncoding: response.ContentEncoding },
552
+ ...response.ContentLength !== void 0 && { contentLength: response.ContentLength },
553
+ ...response.ContentType !== void 0 && { contentType: response.ContentType },
554
+ ...response.ETag !== void 0 && { etag: response.ETag },
555
+ ...response.LastModified !== void 0 && { lastModified: response.LastModified },
556
+ ...response.Metadata !== void 0 && Object.keys(response.Metadata).length > 0 && { metadata: response.Metadata }
557
+ };
558
+ }
559
+ /**
560
+ * Resolve a shared S3 client from an injected instance or construct options.
561
+ * Callers that build three role stores should resolve once and pass the same
562
+ * client to each so dapp-core does not own one client per role.
563
+ */
564
+ function resolveClientCredentials(options) {
565
+ if (options.credentials !== void 0) return options.credentials;
566
+ if (options.endpoint === void 0) return void 0;
567
+ return {
568
+ accessKeyId: DEFAULT_CREDENTIAL,
569
+ secretAccessKey: DEFAULT_CREDENTIAL
570
+ };
571
+ }
572
+ function resolveS3Client(options) {
573
+ if (options.client !== void 0) return {
574
+ client: options.client,
575
+ ownClient: options.ownClient ?? false
576
+ };
577
+ const credentials = resolveClientCredentials(options);
578
+ return {
579
+ client: new S3Client({
580
+ ...options.endpoint !== void 0 && { endpoint: options.endpoint },
581
+ region: options.region ?? DEFAULT_REGION,
582
+ forcePathStyle: options.forcePathStyle ?? options.endpoint !== void 0,
583
+ ...credentials !== void 0 && { credentials }
584
+ }),
585
+ ownClient: options.ownClient ?? true
586
+ };
587
+ }
588
+ /**
589
+ * S3-backed {@link DappBucketStore}. Accepts an injected client (production)
590
+ * or builds one from endpoint/credentials (local s3rver / simple configs).
591
+ * Path-style addressing is the default when an endpoint is provided so local
592
+ * fixtures and many R2 setups work without virtual-host DNS.
593
+ */
594
+ function createS3BucketStore(options) {
595
+ const { role } = options;
596
+ const bindingPrefix = (options.binding.prefix ?? "").replaceAll(/^\/+|\/+$/g, "");
597
+ const publicBaseUrl = normalizePublicBaseUrl(options.binding.publicBaseUrl);
598
+ const { client, ownClient } = resolveS3Client(options);
599
+ const bucket = options.binding.bucket;
600
+ let destroyed = false;
601
+ function resolve(relativeKey) {
602
+ return joinKey(bindingPrefix, relativeKey);
603
+ }
604
+ return {
605
+ role,
606
+ bucket,
607
+ prefix: bindingPrefix,
608
+ ...publicBaseUrl !== void 0 && { publicBaseUrl },
609
+ resolveKey(key) {
610
+ return resolve(key);
611
+ },
612
+ publicUrl(key) {
613
+ if (publicBaseUrl === void 0) return void 0;
614
+ const relative = key.replaceAll(/^\/+/g, "");
615
+ return relative.length === 0 ? publicBaseUrl : `${publicBaseUrl}/${relative}`;
616
+ },
617
+ destroy() {
618
+ if (destroyed) return;
619
+ destroyed = true;
620
+ if (ownClient) client.destroy();
621
+ },
622
+ async get(key) {
623
+ return (await this.getWithMeta(key))?.body;
624
+ },
625
+ async getWithMeta(key) {
626
+ try {
627
+ const response = await client.send(new GetObjectCommand({
628
+ Bucket: bucket,
629
+ Key: resolve(key)
630
+ }));
631
+ const body = await response.Body?.transformToByteArray();
632
+ if (body === void 0) return void 0;
633
+ return {
634
+ body,
635
+ meta: metaFromResponse(response)
636
+ };
637
+ } catch (error) {
638
+ if (isNotFound(error)) return void 0;
639
+ throw error;
640
+ }
641
+ },
642
+ async stat(key) {
643
+ try {
644
+ return metaFromResponse(await client.send(new HeadObjectCommand({
645
+ Bucket: bucket,
646
+ Key: resolve(key)
647
+ })));
648
+ } catch (error) {
649
+ if (isNotFound(error)) return void 0;
650
+ throw error;
651
+ }
652
+ },
653
+ async put(key, body, putOptions) {
654
+ const physicalKey = resolve(key);
655
+ try {
656
+ const response = await client.send(new PutObjectCommand({
657
+ Bucket: bucket,
658
+ Key: physicalKey,
659
+ Body: toBodyBytes(body),
660
+ ...putOptions?.contentType !== void 0 && { ContentType: putOptions.contentType },
661
+ ...putOptions?.contentEncoding !== void 0 && { ContentEncoding: putOptions.contentEncoding },
662
+ ...putOptions?.cacheControl !== void 0 && { CacheControl: putOptions.cacheControl },
663
+ ...putOptions?.metadata !== void 0 && { Metadata: putOptions.metadata },
664
+ ...putOptions?.ifNotExists === true && { IfNoneMatch: "*" },
665
+ ...putOptions?.ifMatch !== void 0 && { IfMatch: putOptions.ifMatch }
666
+ }));
667
+ return { ...response.ETag !== void 0 && { etag: response.ETag } };
668
+ } catch (error) {
669
+ if (isConditionalFailure(error)) throw new ConditionalWriteError(key);
670
+ throw error;
671
+ }
672
+ },
673
+ async delete(key) {
674
+ await client.send(new DeleteObjectCommand({
675
+ Bucket: bucket,
676
+ Key: resolve(key)
677
+ }));
678
+ },
679
+ async list(prefix) {
680
+ const keys = [];
681
+ for await (const key of this.listAsync(prefix)) keys.push(key);
682
+ return keys;
683
+ },
684
+ async listPage(pageOptions = {}) {
685
+ const physicalPrefix = pageOptions.prefix === void 0 ? bindingPrefix.length === 0 ? void 0 : `${bindingPrefix}/` : resolve(pageOptions.prefix);
686
+ const response = await client.send(new ListObjectsV2Command({
687
+ Bucket: bucket,
688
+ ...physicalPrefix !== void 0 && { Prefix: physicalPrefix },
689
+ ...pageOptions.continuationToken !== void 0 && { ContinuationToken: pageOptions.continuationToken },
690
+ ...pageOptions.maxKeys !== void 0 && { MaxKeys: pageOptions.maxKeys }
691
+ }));
692
+ const keys = [];
693
+ for (const item of response.Contents ?? []) {
694
+ if (item.Key === void 0) continue;
695
+ keys.push(stripPrefix(bindingPrefix, item.Key));
696
+ }
697
+ const isTruncated = response.IsTruncated === true;
698
+ return {
699
+ keys,
700
+ isTruncated,
701
+ ...isTruncated && response.NextContinuationToken !== void 0 && { continuationToken: response.NextContinuationToken }
702
+ };
703
+ },
704
+ async *listAsync(prefix) {
705
+ let token;
706
+ do {
707
+ const page = await this.listPage({
708
+ ...prefix !== void 0 && { prefix },
709
+ ...token !== void 0 && { continuationToken: token }
710
+ });
711
+ for (const key of page.keys) yield key;
712
+ token = page.continuationToken;
713
+ } while (token !== void 0);
714
+ }
715
+ };
716
+ }
717
+ //#endregion
718
+ //#region src/locator/createDappLocator.ts
719
+ const DEFAULT_BINDINGS = {
720
+ data: { bucket: "data" },
721
+ state: { bucket: "state" },
722
+ index: { bucket: "index" }
723
+ };
724
+ const ROLE_MONIKERS = {
725
+ data: DappDataStoreMoniker,
726
+ state: DappStateStoreMoniker,
727
+ index: DappIndexStoreMoniker
728
+ };
729
+ function resolveCredentials(options) {
730
+ if (options.credentials !== void 0) return options.credentials;
731
+ if (options.accessKeyId !== void 0 || options.secretAccessKey !== void 0) return {
732
+ accessKeyId: options.accessKeyId ?? "S3RVER",
733
+ secretAccessKey: options.secretAccessKey ?? "S3RVER"
734
+ };
735
+ }
736
+ function resolveSharedClient(options) {
737
+ const credentials = resolveCredentials(options);
738
+ if (options.client !== void 0) return {
739
+ client: options.client,
740
+ ownClient: options.ownClient ?? false
741
+ };
742
+ return resolveS3Client({
743
+ ...options.endpoint !== void 0 && { endpoint: options.endpoint },
744
+ ...options.region !== void 0 && { region: options.region },
745
+ ...options.forcePathStyle !== void 0 && { forcePathStyle: options.forcePathStyle },
746
+ ...credentials !== void 0 && { credentials },
747
+ ownClient: options.ownClient
748
+ });
749
+ }
750
+ function resolveRoleClient(role, options, shared) {
751
+ const override = options.clients?.[role];
752
+ if (override === void 0) return shared;
753
+ if (typeof override.send === "function") return {
754
+ client: override,
755
+ ownClient: false
756
+ };
757
+ return resolveS3Client({
758
+ ...override,
759
+ ownClient: override.ownClient ?? true
760
+ });
761
+ }
762
+ /**
763
+ * Builds the process-wide locator: one instance, shared across every actor
764
+ * (xl1-cli style). It pre-registers an S3-backed store per logical role using
765
+ * either the default local bucket names or explicit production bindings.
766
+ * Extra providers can be `register`ed before actors are created.
767
+ *
768
+ * Prefer a shared {@link client} when all roles share one principal; use
769
+ * {@link CreateDappLocatorOptions.clients} for distinct least-privilege
770
+ * credentials per role. Call `destroy()` on shutdown when the locator owns
771
+ * any client it constructed.
772
+ */
773
+ function createDappLocator(options) {
774
+ const logger = options.logger ?? new ConsoleLogger();
775
+ const bindings = options.bindings ?? DEFAULT_BINDINGS;
776
+ const shared = resolveSharedClient(options);
777
+ const readOnlyData = options.readOnlyData !== false;
778
+ const roles = [
779
+ "data",
780
+ "state",
781
+ "index"
782
+ ];
783
+ const instances = /* @__PURE__ */ new Map();
784
+ const stores = [];
785
+ const ownedClients = [];
786
+ const seenClients = /* @__PURE__ */ new Set();
787
+ const trackClient = (owned) => {
788
+ if (!owned.ownClient || seenClients.has(owned.client)) return;
789
+ seenClients.add(owned.client);
790
+ ownedClients.push(owned);
791
+ };
792
+ trackClient(shared);
793
+ for (const role of roles) {
794
+ const binding = bindings[role];
795
+ const roleClient = resolveRoleClient(role, options, shared);
796
+ trackClient(roleClient);
797
+ let store = createS3BucketStore({
798
+ role,
799
+ binding,
800
+ client: roleClient.client,
801
+ ownClient: false
802
+ });
803
+ if (role === "data" && readOnlyData) store = asReadOnlyWriter(store);
804
+ stores.push(store);
805
+ instances.set(ROLE_MONIKERS[role], store);
806
+ }
807
+ let destroyed = false;
808
+ return {
809
+ context: { logger },
810
+ has(moniker) {
811
+ return instances.has(moniker);
812
+ },
813
+ register(moniker, instance) {
814
+ instances.set(moniker, instance);
815
+ },
816
+ async getInstance(moniker) {
817
+ const found = instances.get(moniker);
818
+ if (found === void 0) throw new Error(`No provider registered for moniker "${moniker}"`);
819
+ return found;
820
+ },
821
+ async tryGetInstance(moniker) {
822
+ return instances.get(moniker);
823
+ },
824
+ destroy() {
825
+ if (destroyed) return;
826
+ destroyed = true;
827
+ for (const store of stores) store.destroy();
828
+ for (const owned of ownedClients) if (owned.ownClient) owned.client.destroy();
829
+ }
830
+ };
831
+ }
832
+ //#endregion
833
+ //#region src/reducer/DappReducer.ts
834
+ /**
835
+ * Default reducer: does nothing. The local harness runs the full timer/provider
836
+ * spine with this in place; production entrypoints must supply a real reducer.
837
+ * **Development only** — not a production indexer.
838
+ */
839
+ const noopReducer = {
840
+ name: "noop",
841
+ async reduce() {}
842
+ };
843
+ //#endregion
844
+ //#region src/bin/loadReducer.ts
845
+ const require = createRequire(import.meta.url);
846
+ function isPathSpec(spec) {
847
+ return spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("file:") || path.isAbsolute(spec) || spec.endsWith(".mjs") || spec.endsWith(".js") || spec.endsWith(".cjs");
848
+ }
849
+ function resolveFileUrl(spec) {
850
+ if (spec.startsWith("file:")) return spec;
851
+ const absolute = path.isAbsolute(spec) ? spec : path.resolve(process.cwd(), spec);
852
+ if (!existsSync(absolute)) throw new Error(`Reducer module not found: ${absolute}`);
853
+ return pathToFileURL(absolute).href;
854
+ }
855
+ function resolvePackageUrl(spec) {
856
+ try {
857
+ return pathToFileURL(require.resolve(spec, { paths: [process.cwd()] })).href;
858
+ } catch {
859
+ throw new Error(`Could not resolve reducer package "${spec}" from ${process.cwd()}. Pass a path to a .mjs file or an installed package export.`);
860
+ }
861
+ }
862
+ function pickReducer(mod, exportName) {
863
+ if (exportName !== void 0 && exportName.length > 0) {
864
+ if (!(exportName in mod)) throw new Error(`Reducer module has no export named "${exportName}"`);
865
+ return mod[exportName];
866
+ }
867
+ if (mod.default !== void 0) return mod.default;
868
+ if (mod.reducer !== void 0) return mod.reducer;
869
+ throw new Error("Reducer module must default-export a DappReducer, or export `reducer`, or set DAPP_REDUCER_EXPORT / --reducer-export to a named export");
870
+ }
871
+ function assertReducer(value, label) {
872
+ if (value === null || typeof value !== "object") throw new TypeError(`${label} is not an object`);
873
+ const candidate = value;
874
+ if (typeof candidate.name !== "string" || candidate.name.length === 0) throw new TypeError(`${label} must have a non-empty string "name"`);
875
+ if (typeof candidate.reduce !== "function") throw new TypeError(`${label} must have a "reduce" function`);
876
+ return candidate;
877
+ }
878
+ /**
879
+ * Dynamically load a project-owned reducer for the local dapp daemon.
880
+ * Supports filesystem `.mjs`/`.js` paths and package export strings.
881
+ * TypeScript sources are not supported — build to ESM first.
882
+ */
883
+ async function loadReducer(options) {
884
+ return assertReducer(pickReducer(await (isPathSpec(options.spec) ? import(resolveFileUrl(options.spec)) : import(resolvePackageUrl(options.spec))), options.exportName), `Reducer from ${options.spec}`);
885
+ }
886
+ //#endregion
887
+ //#region src/bin/dappServer.ts
888
+ /**
889
+ * **Local development only.** Composed dapp daemon: boots the embedded
890
+ * S3-compatible storage server (from `@ariestools/aries-dapp-serve`) and, in
891
+ * the same process, a shared process-wide locator plus DappActor(s).
892
+ * Spawned by `aries dapp up`.
893
+ *
894
+ * Optional `DAPP_REDUCER` loads a project-owned ESM reducer (`.mjs` path or
895
+ * package export). Without it, `noopReducer` runs so the spine still works.
896
+ *
897
+ * Production indexers should **not** use this binary. Compose `dapp-core` from
898
+ * a project-owned entrypoint with real R2/S3 bindings and a real reducer —
899
+ * see the package README and `examples/`.
900
+ */
901
+ const PORT = Number(process.env.DAPP_PORT ?? 8801);
902
+ const HOST = process.env.HOST ?? "127.0.0.1";
903
+ const BACKING = process.env.DAPP_BACKING ?? "memory";
904
+ const PUBLIC_HOST = process.env.DAPP_PUBLIC_HOST;
905
+ const TLS_CERT_PATH = process.env.DAPP_TLS_CERT;
906
+ const TLS_KEY_PATH = process.env.DAPP_TLS_KEY;
907
+ const REDUCE_INTERVAL_MS = Number(process.env.DAPP_REDUCE_INTERVAL_MS ?? 5e3);
908
+ const FIRST_RUN_DELAY_MS = Number(process.env.DAPP_FIRST_RUN_DELAY_MS ?? 0);
909
+ const REDUCER_SPEC = process.env.DAPP_REDUCER;
910
+ const REDUCER_EXPORT = process.env.DAPP_REDUCER_EXPORT;
911
+ const DATA_DIR = process.env.DAPP_DATA_DIR ?? path.join(process.env.ARIES_HOME ?? path.join(homedir(), ".aries"), "dapp", "data");
912
+ async function resolveDaemonReducer() {
913
+ if (REDUCER_SPEC === void 0 || REDUCER_SPEC.length === 0) return noopReducer;
914
+ return await loadReducer({
915
+ spec: REDUCER_SPEC,
916
+ ...REDUCER_EXPORT !== void 0 && REDUCER_EXPORT.length > 0 && { exportName: REDUCER_EXPORT }
917
+ });
918
+ }
919
+ async function main() {
920
+ if (!isDappBackingKind(BACKING)) throw new Error(`Unsupported DAPP_BACKING '${BACKING}' (supported: ${DAPP_BACKINGS.join(", ")})`);
921
+ if (TLS_CERT_PATH === void 0 !== (TLS_KEY_PATH === void 0)) throw new Error("DAPP_TLS_CERT and DAPP_TLS_KEY must be provided together");
922
+ const reducer = await resolveDaemonReducer();
923
+ const backing = resolveBacking(BACKING, { ...BACKING === "disk" && { directory: DATA_DIR } });
924
+ const server = await startDappServer({
925
+ backing,
926
+ host: HOST,
927
+ port: PORT,
928
+ publicHost: PUBLIC_HOST,
929
+ tls: TLS_CERT_PATH === void 0 || TLS_KEY_PATH === void 0 ? void 0 : {
930
+ cert: readFileSync(TLS_CERT_PATH),
931
+ key: readFileSync(TLS_KEY_PATH)
932
+ }
933
+ });
934
+ const logger = new ConsoleLogger();
935
+ const locator = createDappLocator({
936
+ endpoint: server.baseUrl,
937
+ logger,
938
+ readOnlyData: false
939
+ });
940
+ process.env.DAPP_PUBLICATION_SAFETY ??= "unfenced";
941
+ process.env.DAPP_ENDPOINT ??= server.baseUrl;
942
+ const actors = await bootDappActors(locator, [{
943
+ name: "DappActor",
944
+ reducer,
945
+ reduceIntervalMs: REDUCE_INTERVAL_MS,
946
+ firstRunDelayMs: FIRST_RUN_DELAY_MS,
947
+ publishStatus: reducer.name !== "noop"
948
+ }]);
949
+ const versionSuffix = reducer.version === void 0 ? "" : `@${reducer.version}`;
950
+ const reducerLine = REDUCER_SPEC === void 0 ? "\nreducer: noop (pass DAPP_REDUCER / --reducer for real logic)" : `\nreducer: ${REDUCER_SPEC}`;
951
+ console.log(`[dev-only] dapp server listening at ${server.baseUrl}\n` + DAPP_BUCKETS.map((bucket) => ` /${bucket.padEnd(5)} ${server.baseUrl}/${bucket}`).join("\n") + `\nbacking: ${backing.description}\nactors: ${actors.length} (reducer '${reducer.name}'${versionSuffix}, every ${REDUCE_INTERVAL_MS}ms)` + reducerLine);
952
+ const shutdown = async () => {
953
+ for (const actor of actors) await actor.stop();
954
+ locator.destroy();
955
+ await server.close();
956
+ process.exit(0);
957
+ };
958
+ process.on("SIGINT", () => {
959
+ shutdown();
960
+ });
961
+ process.on("SIGTERM", () => {
962
+ shutdown();
963
+ });
964
+ }
965
+ main().catch((error) => {
966
+ console.error(error);
967
+ process.exit(1);
968
+ });
969
+ //#endregion
970
+ export {};