@lunora/platform-node 1.0.0-alpha.129 → 1.0.0-alpha.130
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/dist/index.d.mts +64 -45
- package/dist/index.d.ts +64 -45
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/NODE_SCHEDULER_DISPATCH_PATH-CNBAtztq.mjs +1 -0
- package/dist/packem_shared/createNodePlatform-dBsmRW5b.mjs +1 -0
- package/dist/packem_shared/createNodeR2Bucket-C_oO6X63.mjs +1 -0
- package/dist/packem_shared/hmac-url-BrwGfbCr.mjs +1 -0
- package/package.json +6 -5
- package/dist/packem_shared/createNodePlatform-D29HvrOz.mjs +0 -1
- package/dist/packem_shared/createNodeR2Bucket-ChHs23n-.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -4,6 +4,39 @@ import Database from 'better-sqlite3';
|
|
|
4
4
|
import { ShardKvStore, QueueBindingLike, MessageBatchLike, SchedulerHost, ShardHost, SocketHost, DirectShardDirectory, PlatformCapabilities, ShardDirectory, R2BucketLike, SocketHandle } from '@lunora/platform';
|
|
5
5
|
import { WorkflowBindingLike } from '@lunora/workflow';
|
|
6
6
|
import { WorkflowRuntime, WorkflowStore } from '@visulima/workflow';
|
|
7
|
+
/** One function call the route has authenticated and parsed. */
|
|
8
|
+
interface NodeDispatchInvocation {
|
|
9
|
+
args: Record<string, unknown>;
|
|
10
|
+
functionPath: string;
|
|
11
|
+
/** The scheduler record or delivery id, used as the shard's idempotency key. */
|
|
12
|
+
id: string | undefined;
|
|
13
|
+
/** Identity the trusted caller forwarded, if any. */
|
|
14
|
+
identity: {
|
|
15
|
+
identity?: string;
|
|
16
|
+
userId?: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
shardKey: string;
|
|
19
|
+
traceparent: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
interface NodeDispatchRouteOptions {
|
|
22
|
+
/** `LUNORA_ADMIN_TOKEN`. Used as the bearer when no HMAC signature is presented. */
|
|
23
|
+
adminToken?: string;
|
|
24
|
+
/** The shard a body without `shardKey` is sent to. */
|
|
25
|
+
defaultShardKey: string;
|
|
26
|
+
/** Runs the call in-process and returns the shard's response, unchanged. */
|
|
27
|
+
invoke: (call: NodeDispatchInvocation) => Promise<Response>;
|
|
28
|
+
/** `LUNORA_SCHEDULER_SECRET`. When set, a signed request is verified against it. */
|
|
29
|
+
schedulerSecret?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the dispatch route. The returned function answers any request it is
|
|
33
|
+
* handed; mounting it at {@link NODE_SCHEDULER_DISPATCH_PATH} is the caller's job.
|
|
34
|
+
* With neither secret configured every request is refused, so an unconfigured
|
|
35
|
+
* route fails closed.
|
|
36
|
+
*/
|
|
37
|
+
declare const createNodeDispatchRoute: (options: NodeDispatchRouteOptions) => ((request: Request) => Promise<Response>);
|
|
38
|
+
/** The path `@lunora/dispatch` and `@lunora/scheduler` POST to. */
|
|
39
|
+
declare const NODE_SCHEDULER_DISPATCH_PATH: "/_lunora/scheduler/dispatch";
|
|
7
40
|
/**
|
|
8
41
|
* The Node store options — the shared store options minus the two this binding
|
|
9
42
|
* owns. `dialect` is fixed (SQLite) and `exec` is the store's own connection;
|
|
@@ -425,6 +458,16 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
425
458
|
close: () => void;
|
|
426
459
|
/** In-process shard directory (see `createNodeShardRegistry` for what it can and cannot do). */
|
|
427
460
|
directory: ShardDirectory;
|
|
461
|
+
/**
|
|
462
|
+
* The scheduler dispatch route, served in-process: `Request` in, shard
|
|
463
|
+
* response out, no listener. It answers the path `@lunora/dispatch` and
|
|
464
|
+
* `@lunora/scheduler` POST to. Pass it to `dispatchQueueBatch` as
|
|
465
|
+
* `fetchImpl` so a queue handler's `message.run` / `ctx.run` reaches the
|
|
466
|
+
* shard; the env you pass there must carry `LUNORA_ORIGIN_URL` and the same
|
|
467
|
+
* `LUNORA_ADMIN_TOKEN` as `dispatch.adminToken`. Every request is refused
|
|
468
|
+
* unless `dispatch` supplies a secret.
|
|
469
|
+
*/
|
|
470
|
+
dispatch: (request: Request) => Promise<Response>;
|
|
428
471
|
/**
|
|
429
472
|
* Wait for every promise handed to `shard.waitUntil` to settle.
|
|
430
473
|
*
|
|
@@ -477,6 +520,22 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
477
520
|
*/
|
|
478
521
|
workflows?: NodeWorkflowHost<Workflows>;
|
|
479
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* The secrets the dispatch route authenticates with. With neither, the route
|
|
525
|
+
* refuses everything.
|
|
526
|
+
*
|
|
527
|
+
* Queue and topic delivery authenticates with `adminToken` alone:
|
|
528
|
+
* `@lunora/dispatch` always sends `Bearer <LUNORA_ADMIN_TOKEN>` and never signs,
|
|
529
|
+
* so the value here must match the token the caller puts on the env it passes
|
|
530
|
+
* `dispatchQueueBatch`. `schedulerSecret` only verifies a signed body, which the
|
|
531
|
+
* scheduler's own delivery sends; queue delivery never does.
|
|
532
|
+
*/
|
|
533
|
+
interface NodePlatformDispatchOptions {
|
|
534
|
+
/** `LUNORA_ADMIN_TOKEN`. The bearer queue and topic delivery sends, so it must match the caller's env. */
|
|
535
|
+
adminToken?: string;
|
|
536
|
+
/** `LUNORA_SCHEDULER_SECRET`. When set, a signed body is verified against it. Queue delivery does not sign. */
|
|
537
|
+
schedulerSecret?: string;
|
|
538
|
+
}
|
|
480
539
|
/**
|
|
481
540
|
* Options for {@link createNodePlatform} — the shard host's (`path`,
|
|
482
541
|
* `shardKey`, `onAlarm`), the scheduler's (`onDispatch`), and the shard
|
|
@@ -494,6 +553,8 @@ type NodePlatformOptions<Queues extends Record<string, {
|
|
|
494
553
|
}> = Record<string, never>, Workflows extends Record<string, {
|
|
495
554
|
isLunoraWorkflow: true;
|
|
496
555
|
}> = Record<string, never>> = {
|
|
556
|
+
/** Secrets for the dispatch route. Omit to leave the route refusing every request. */
|
|
557
|
+
dispatch?: NodePlatformDispatchOptions;
|
|
497
558
|
/**
|
|
498
559
|
* Database file the `.global()` tables live in — its own file, never a
|
|
499
560
|
* shard's, because a table every shard reads must not be inside any one of
|
|
@@ -617,49 +678,7 @@ declare const createNodeSocketHost: (database: Database.Database) => NodeSocketH
|
|
|
617
678
|
* Pass the result as `createNodeWorkflowHost({ store })`.
|
|
618
679
|
*/
|
|
619
680
|
declare const createNodeWorkflowStore: (database: Database.Database) => WorkflowStore;
|
|
620
|
-
export {
|
|
621
|
-
/**
|
|
622
|
-
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
623
|
-
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
624
|
-
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
625
|
-
* scheduler registry.
|
|
626
|
-
*
|
|
627
|
-
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
628
|
-
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
629
|
-
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
630
|
-
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
631
|
-
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
632
|
-
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
633
|
-
* a restart test — a second host over the same database file — not only by the
|
|
634
|
-
* TCK's simulated recycle.
|
|
635
|
-
*
|
|
636
|
-
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
637
|
-
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
638
|
-
* lifecycle/global-store tests.
|
|
639
|
-
*
|
|
640
|
-
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
641
|
-
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
642
|
-
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
643
|
-
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
644
|
-
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
645
|
-
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
646
|
-
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
647
|
-
* is the one combination that fails at runtime with no diagnostic before it.
|
|
648
|
-
*
|
|
649
|
-
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
650
|
-
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
651
|
-
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
652
|
-
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
653
|
-
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
654
|
-
*
|
|
655
|
-
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
656
|
-
* for `provision` — which reports, once, which declared features this target
|
|
657
|
-
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
658
|
-
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
659
|
-
* server, which is why both statements above hold at once. See
|
|
660
|
-
* `plans/234-node-host-findings.md`.
|
|
661
|
-
*/
|
|
662
|
-
type NodeGlobalContextDatabaseOptions,
|
|
681
|
+
export { NODE_SCHEDULER_DISPATCH_PATH,
|
|
663
682
|
/**
|
|
664
683
|
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
665
684
|
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
@@ -701,7 +720,7 @@ type NodeGlobalContextDatabaseOptions,
|
|
|
701
720
|
* server, which is why both statements above hold at once. See
|
|
702
721
|
* `plans/234-node-host-findings.md`.
|
|
703
722
|
*/
|
|
704
|
-
type
|
|
723
|
+
type NodeDispatchInvocation,
|
|
705
724
|
/**
|
|
706
725
|
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
707
726
|
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
@@ -743,4 +762,4 @@ type NodeGlobalStore,
|
|
|
743
762
|
* server, which is why both statements above hold at once. See
|
|
744
763
|
* `plans/234-node-host-findings.md`.
|
|
745
764
|
*/
|
|
746
|
-
type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformOptions, type NodeProfileHandler, type NodeProfileHandlerOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeGlobalStore, createNodePlatform, createNodeProfileHandler, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
|
|
765
|
+
type NodeDispatchRouteOptions, type NodeGlobalContextDatabaseOptions, type NodeGlobalStore, type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformDispatchOptions, type NodePlatformOptions, type NodeProfileHandler, type NodeProfileHandlerOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeDispatchRoute, createNodeGlobalStore, createNodePlatform, createNodeProfileHandler, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,39 @@ import Database from 'better-sqlite3';
|
|
|
4
4
|
import { ShardKvStore, QueueBindingLike, MessageBatchLike, SchedulerHost, ShardHost, SocketHost, DirectShardDirectory, PlatformCapabilities, ShardDirectory, R2BucketLike, SocketHandle } from '@lunora/platform';
|
|
5
5
|
import { WorkflowBindingLike } from '@lunora/workflow';
|
|
6
6
|
import { WorkflowRuntime, WorkflowStore } from '@visulima/workflow';
|
|
7
|
+
/** One function call the route has authenticated and parsed. */
|
|
8
|
+
interface NodeDispatchInvocation {
|
|
9
|
+
args: Record<string, unknown>;
|
|
10
|
+
functionPath: string;
|
|
11
|
+
/** The scheduler record or delivery id, used as the shard's idempotency key. */
|
|
12
|
+
id: string | undefined;
|
|
13
|
+
/** Identity the trusted caller forwarded, if any. */
|
|
14
|
+
identity: {
|
|
15
|
+
identity?: string;
|
|
16
|
+
userId?: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
shardKey: string;
|
|
19
|
+
traceparent: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
interface NodeDispatchRouteOptions {
|
|
22
|
+
/** `LUNORA_ADMIN_TOKEN`. Used as the bearer when no HMAC signature is presented. */
|
|
23
|
+
adminToken?: string;
|
|
24
|
+
/** The shard a body without `shardKey` is sent to. */
|
|
25
|
+
defaultShardKey: string;
|
|
26
|
+
/** Runs the call in-process and returns the shard's response, unchanged. */
|
|
27
|
+
invoke: (call: NodeDispatchInvocation) => Promise<Response>;
|
|
28
|
+
/** `LUNORA_SCHEDULER_SECRET`. When set, a signed request is verified against it. */
|
|
29
|
+
schedulerSecret?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the dispatch route. The returned function answers any request it is
|
|
33
|
+
* handed; mounting it at {@link NODE_SCHEDULER_DISPATCH_PATH} is the caller's job.
|
|
34
|
+
* With neither secret configured every request is refused, so an unconfigured
|
|
35
|
+
* route fails closed.
|
|
36
|
+
*/
|
|
37
|
+
declare const createNodeDispatchRoute: (options: NodeDispatchRouteOptions) => ((request: Request) => Promise<Response>);
|
|
38
|
+
/** The path `@lunora/dispatch` and `@lunora/scheduler` POST to. */
|
|
39
|
+
declare const NODE_SCHEDULER_DISPATCH_PATH: "/_lunora/scheduler/dispatch";
|
|
7
40
|
/**
|
|
8
41
|
* The Node store options — the shared store options minus the two this binding
|
|
9
42
|
* owns. `dialect` is fixed (SQLite) and `exec` is the store's own connection;
|
|
@@ -425,6 +458,16 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
425
458
|
close: () => void;
|
|
426
459
|
/** In-process shard directory (see `createNodeShardRegistry` for what it can and cannot do). */
|
|
427
460
|
directory: ShardDirectory;
|
|
461
|
+
/**
|
|
462
|
+
* The scheduler dispatch route, served in-process: `Request` in, shard
|
|
463
|
+
* response out, no listener. It answers the path `@lunora/dispatch` and
|
|
464
|
+
* `@lunora/scheduler` POST to. Pass it to `dispatchQueueBatch` as
|
|
465
|
+
* `fetchImpl` so a queue handler's `message.run` / `ctx.run` reaches the
|
|
466
|
+
* shard; the env you pass there must carry `LUNORA_ORIGIN_URL` and the same
|
|
467
|
+
* `LUNORA_ADMIN_TOKEN` as `dispatch.adminToken`. Every request is refused
|
|
468
|
+
* unless `dispatch` supplies a secret.
|
|
469
|
+
*/
|
|
470
|
+
dispatch: (request: Request) => Promise<Response>;
|
|
428
471
|
/**
|
|
429
472
|
* Wait for every promise handed to `shard.waitUntil` to settle.
|
|
430
473
|
*
|
|
@@ -477,6 +520,22 @@ interface NodePlatform<Queues extends Record<string, {
|
|
|
477
520
|
*/
|
|
478
521
|
workflows?: NodeWorkflowHost<Workflows>;
|
|
479
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* The secrets the dispatch route authenticates with. With neither, the route
|
|
525
|
+
* refuses everything.
|
|
526
|
+
*
|
|
527
|
+
* Queue and topic delivery authenticates with `adminToken` alone:
|
|
528
|
+
* `@lunora/dispatch` always sends `Bearer <LUNORA_ADMIN_TOKEN>` and never signs,
|
|
529
|
+
* so the value here must match the token the caller puts on the env it passes
|
|
530
|
+
* `dispatchQueueBatch`. `schedulerSecret` only verifies a signed body, which the
|
|
531
|
+
* scheduler's own delivery sends; queue delivery never does.
|
|
532
|
+
*/
|
|
533
|
+
interface NodePlatformDispatchOptions {
|
|
534
|
+
/** `LUNORA_ADMIN_TOKEN`. The bearer queue and topic delivery sends, so it must match the caller's env. */
|
|
535
|
+
adminToken?: string;
|
|
536
|
+
/** `LUNORA_SCHEDULER_SECRET`. When set, a signed body is verified against it. Queue delivery does not sign. */
|
|
537
|
+
schedulerSecret?: string;
|
|
538
|
+
}
|
|
480
539
|
/**
|
|
481
540
|
* Options for {@link createNodePlatform} — the shard host's (`path`,
|
|
482
541
|
* `shardKey`, `onAlarm`), the scheduler's (`onDispatch`), and the shard
|
|
@@ -494,6 +553,8 @@ type NodePlatformOptions<Queues extends Record<string, {
|
|
|
494
553
|
}> = Record<string, never>, Workflows extends Record<string, {
|
|
495
554
|
isLunoraWorkflow: true;
|
|
496
555
|
}> = Record<string, never>> = {
|
|
556
|
+
/** Secrets for the dispatch route. Omit to leave the route refusing every request. */
|
|
557
|
+
dispatch?: NodePlatformDispatchOptions;
|
|
497
558
|
/**
|
|
498
559
|
* Database file the `.global()` tables live in — its own file, never a
|
|
499
560
|
* shard's, because a table every shard reads must not be inside any one of
|
|
@@ -617,49 +678,7 @@ declare const createNodeSocketHost: (database: Database.Database) => NodeSocketH
|
|
|
617
678
|
* Pass the result as `createNodeWorkflowHost({ store })`.
|
|
618
679
|
*/
|
|
619
680
|
declare const createNodeWorkflowStore: (database: Database.Database) => WorkflowStore;
|
|
620
|
-
export {
|
|
621
|
-
/**
|
|
622
|
-
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
623
|
-
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
624
|
-
* `SchedulerHost`) over `better-sqlite3` and an in-process socket/directory/
|
|
625
|
-
* scheduler registry.
|
|
626
|
-
*
|
|
627
|
-
* Promoted from `@lunora/platform`'s `node:sqlite` reference host
|
|
628
|
-
* (`src/conformance/reference-host.ts`) under plan 234, then hardened until the
|
|
629
|
-
* durability half of each contract actually holds: alarms and scheduler jobs
|
|
630
|
-
* are persisted **and re-armed on construction**, socket attachments and tags
|
|
631
|
-
* live in SQLite rather than a `Map`, and `.global()` tables run the real
|
|
632
|
-
* `@lunora/sql-store` core (`./node-global-store`). Each of those is pinned by
|
|
633
|
-
* a restart test — a second host over the same database file — not only by the
|
|
634
|
-
* TCK's simulated recycle.
|
|
635
|
-
*
|
|
636
|
-
* Three suites run against this package: `@lunora/platform/conformance`'s host
|
|
637
|
-
* TCK, `@lunora/shard-engine/conformance`'s engine suite, and its own
|
|
638
|
-
* lifecycle/global-store tests.
|
|
639
|
-
*
|
|
640
|
-
* **Emulated here**, and rated accordingly in `NODE_CAPABILITIES`
|
|
641
|
-
* (`@lunora/platform`): queues over a durable table (`./node-queue-host`), R2
|
|
642
|
-
* buckets over the local filesystem (`./node-r2-bucket`), workflows over the
|
|
643
|
-
* `@visulima/workflow` engine (`./node-workflow-host`), and cross-shard fan-out
|
|
644
|
-
* via `@lunora/runtime`'s query coordinator over the in-process shard registry.
|
|
645
|
-
* `createNodePlatform` binds all three declarations (`queues`, `workflows`,
|
|
646
|
-
* `objectStorageDirectory`) — a capability rated `emulated` with nothing bound
|
|
647
|
-
* is the one combination that fails at runtime with no diagnostic before it.
|
|
648
|
-
*
|
|
649
|
-
* **Still missing:** a dev server. There is no `lunora dev --target node`, and
|
|
650
|
-
* nothing here owns a timer, so queue delivery is driven by an explicit
|
|
651
|
-
* `poll()`. Also absent are the Cloudflare product bindings with no local
|
|
652
|
-
* equivalent — Vectorize, Workers AI, Browser Rendering, Containers, Analytics
|
|
653
|
-
* Engine, Pipelines, Secrets Store, Hyperdrive.
|
|
654
|
-
*
|
|
655
|
-
* `@lunora/config` ships a `node` **deploy** driver, so `--target node` resolves
|
|
656
|
-
* for `provision` — which reports, once, which declared features this target
|
|
657
|
-
* cannot serve, and writes nothing: there is no hosted control plane to deploy
|
|
658
|
-
* to and no `wrangler`-equivalent to shell out to. A deploy driver is not a dev
|
|
659
|
-
* server, which is why both statements above hold at once. See
|
|
660
|
-
* `plans/234-node-host-findings.md`.
|
|
661
|
-
*/
|
|
662
|
-
type NodeGlobalContextDatabaseOptions,
|
|
681
|
+
export { NODE_SCHEDULER_DISPATCH_PATH,
|
|
663
682
|
/**
|
|
664
683
|
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
665
684
|
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
@@ -701,7 +720,7 @@ type NodeGlobalContextDatabaseOptions,
|
|
|
701
720
|
* server, which is why both statements above hold at once. See
|
|
702
721
|
* `plans/234-node-host-findings.md`.
|
|
703
722
|
*/
|
|
704
|
-
type
|
|
723
|
+
type NodeDispatchInvocation,
|
|
705
724
|
/**
|
|
706
725
|
* `@lunora/platform-node` — a Node implementation of the `@lunora/platform`
|
|
707
726
|
* host contracts (`ShardHost`, `SocketHost`, `ShardDirectory`, `ShardKvStore`,
|
|
@@ -743,4 +762,4 @@ type NodeGlobalStore,
|
|
|
743
762
|
* server, which is why both statements above hold at once. See
|
|
744
763
|
* `plans/234-node-host-findings.md`.
|
|
745
764
|
*/
|
|
746
|
-
type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformOptions, type NodeProfileHandler, type NodeProfileHandlerOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeGlobalStore, createNodePlatform, createNodeProfileHandler, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
|
|
765
|
+
type NodeDispatchRouteOptions, type NodeGlobalContextDatabaseOptions, type NodeGlobalStore, type NodeGlobalStoreOptions, type NodePlatform, type NodePlatformDispatchOptions, type NodePlatformOptions, type NodeProfileHandler, type NodeProfileHandlerOptions, type NodeQueueHost, type NodeQueueHostOptions, type NodeR2BucketOptions, type NodeSchedulerHost, type NodeSchedulerHostOptions, type NodeShard, type NodeShardHostOptions, type NodeShardRegistry, type NodeShardRegistryOptions, type NodeShardState, type NodeSocketHost, type NodeWorkflowHost, type NodeWorkflowHostOptions, createNodeDispatchRoute, createNodeGlobalStore, createNodePlatform, createNodeProfileHandler, createNodeQueueHost, createNodeR2Bucket, createNodeSchedulerHost, createNodeShardHost, createNodeShardKvStore, createNodeShardRegistry, createNodeShardState, createNodeSocketHost, createNodeSqlExec, createNodeWorkflowHost, createNodeWorkflowStore };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{NODE_SCHEDULER_DISPATCH_PATH as r,createNodeDispatchRoute as t}from"./packem_shared/NODE_SCHEDULER_DISPATCH_PATH-CNBAtztq.mjs";import{createNodeGlobalStore as d,createNodeSqlExec as c}from"./packem_shared/createNodeGlobalStore-DiGxjmD7.mjs";import{createNodeShardKvStore as N}from"./packem_shared/createNodeShardKvStore-DVi1OsvH.mjs";import{createNodePlatform as p}from"./packem_shared/createNodePlatform-dBsmRW5b.mjs";import{createNodeProfileHandler as S}from"./packem_shared/createNodeProfileHandler-MX5xKPg0.mjs";import{createNodeQueueHost as H}from"./packem_shared/createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as h}from"./packem_shared/createNodeR2Bucket-C_oO6X63.mjs";import{createNodeSchedulerHost as k}from"./packem_shared/createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as E}from"./packem_shared/createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as R}from"./packem_shared/createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeShardState as _}from"./packem_shared/createNodeShardState-D6XMWbFs.mjs";import{createNodeSocketHost as A}from"./packem_shared/createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as T}from"./packem_shared/createNodeWorkflowHost-B76TKiw_.mjs";import{createNodeWorkflowStore as b}from"./packem_shared/createNodeWorkflowStore-CL7RYP6H.mjs";export{r as NODE_SCHEDULER_DISPATCH_PATH,t as createNodeDispatchRoute,d as createNodeGlobalStore,p as createNodePlatform,S as createNodeProfileHandler,H as createNodeQueueHost,h as createNodeR2Bucket,k as createNodeSchedulerHost,E as createNodeShardHost,N as createNodeShardKvStore,R as createNodeShardRegistry,_ as createNodeShardState,A as createNodeSocketHost,c as createNodeSqlExec,T as createNodeWorkflowHost,b as createNodeWorkflowStore};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as i}from"@lunora/errors";import{toErrorResponse as h,resolveShard as u}from"@lunora/runtime";import{s as l}from"./hmac-url-BrwGfbCr.mjs";const f=async(t,e)=>{if(!t.body)return"";const n=t.body.getReader(),r=new TextDecoder;let a=0,o="";for(;;){const{done:s,value:d}=await n.read();if(s)break;if(d){if(a+=d.byteLength,a>e)return await n.cancel().catch(()=>{}),null;o+=r.decode(d,{stream:!0})}}return o+=r.decode(),o},c=(t,e)=>{const n=Math.max(t.length,e.length);let r=t.length^e.length;for(let a=0;a<n;a+=1){const o=a<t.length?t.charCodeAt(a):0,s=a<e.length?e.charCodeAt(a):0;r|=o^s}return r===0},y="/_lunora/scheduler/dispatch",g="__lunora_relation__:",p=t=>t.startsWith(g),w=(t,e)=>{if(!e||e.length===0)return!1;const n=t.headers.get("authorization");if(!n)return!1;const[r,...a]=n.split(" ");return r?.toLowerCase()!=="bearer"?!1:c(e,a.join(" ").trim())},_=async(t,e,n)=>{const r=t.headers.get("x-lunora-scheduler-signature"),{schedulerSecret:a}=n;return r!==null&&r.length>0&&a!==void 0&&a.length>0?c(await l(a,e),r):w(t,n.adminToken)},A="x-lunora-paywall",S="1",E=({forwardedIdentity:t,mutationId:e,traceparent:n})=>{const r={"content-type":"application/json",[A]:S,"x-lunora-system":"1"};return t?.userId!==void 0&&t.userId.length>0&&(r["x-lunora-userid"]=t.userId),t?.identity!==void 0&&t.identity.length>0&&(r["x-lunora-identity"]=t.identity),e!==void 0&&e.length>0&&(r["x-lunora-mutation-id"]=e),n!==void 0&&n.length>0&&(r.traceparent=n),r},T=t=>{const e=t.headers.get("x-lunora-userid"),n=t.headers.get("x-lunora-identity");if(!(e===null&&n===null))return{...n===null?{}:{identity:n},...e===null?{}:{userId:e}}},P=1048576,m=async t=>{const e=await f(t,P);if(e===null)throw new i("PAYLOAD_TOO_LARGE","Scheduler dispatch body is too large");return e},D=async(t,e)=>{if(t.method!=="POST")throw new i("METHOD_NOT_ALLOWED","Scheduler dispatch endpoint requires POST",{status:405});const n=await m(t);if(!await _(t,n,{adminToken:e.adminToken,schedulerSecret:e.schedulerSecret}))throw new i("DISPATCH_UNAUTHENTICATED","Scheduler dispatch requires a valid signature or admin bearer");let a;try{a=JSON.parse(n)}catch{throw new i("BAD_REQUEST","Scheduler dispatch body must be valid JSON")}const o=a??{};if(typeof o.workflow=="string"||o.pool!==void 0)throw new i("SERVICE_UNAVAILABLE","Scheduler dispatch for a workflow or pool target is not served by the Node dispatch route");if(typeof o.functionPath!="string"||o.functionPath.length===0)throw new i("BAD_REQUEST","Scheduler dispatch is missing `functionPath`");if(p(o.functionPath))throw new i("FORBIDDEN","`__lunora_relation__:*` is a fan-out-only reserved RPC and cannot be dispatched to a single shard");const s=typeof o.shardKey=="string"&&o.shardKey.length>0?o.shardKey:e.defaultShardKey;return e.invoke({args:o.args??{},functionPath:o.functionPath,id:typeof o.id=="string"&&o.id.length>0?o.id:void 0,identity:T(t),shardKey:s,traceparent:t.headers.get("traceparent")??void 0})},L=(t,e)=>n=>{if(!e)throw new i("SERVICE_UNAVAILABLE","@lunora/platform-node: the dispatch route needs `onFetch` to run a function; without it a shard only echoes its key");const r=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:n.args,functionPath:n.functionPath}),headers:E({forwardedIdentity:n.identity,mutationId:n.id,traceparent:n.traceparent}),method:"POST"});return u(t,n.shardKey).fetch(r)},O=t=>async e=>{try{return await D(e,t)}catch(n){return h(n)}},N=y;export{N as NODE_SCHEDULER_DISPATCH_PATH,O as createNodeDispatchRoute,L as createShardInvoke};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as k}from"@lunora/errors";import{NODE_CAPABILITIES as N}from"@lunora/platform";import{createNodeDispatchRoute as b,createShardInvoke as g}from"./NODE_SCHEDULER_DISPATCH_PATH-CNBAtztq.mjs";import{createNodeGlobalStore as y}from"./createNodeGlobalStore-DiGxjmD7.mjs";import{createNodeShardKvStore as R}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeQueueHost as T}from"./createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as q}from"./createNodeR2Bucket-C_oO6X63.mjs";import{createNodeSchedulerHost as B}from"./createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as I}from"./createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as _}from"./createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeSocketHost as D}from"./createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as H}from"./createNodeWorkflowHost-B76TKiw_.mjs";import{createNodeWorkflowStore as P}from"./createNodeWorkflowStore-CL7RYP6H.mjs";const z=(e={})=>{const o=[],{database:r,dispose:u,drain:i,host:l}=I(e);o.push(u);try{const a=R(r),t=_(e);o.push(()=>{t.close()});const{directory:d}=t,m=b({adminToken:e.dispatch?.adminToken,defaultShardKey:"__root__",invoke:g(d,e.onFetch!==void 0),schedulerSecret:e.dispatch?.schedulerSecret}),{socket:f}=D(r),{dispose:n,scheduler:p}=B(r,e);o.push(n);const S=e.queues===void 0?void 0:T(r,{onBatch:e.onQueueBatch??(()=>{throw new k("VALIDATION_ERROR","@lunora/platform-node: createNodePlatform was given `queues` without `onQueueBatch`, so a delivered batch has nowhere to go — pass dispatchQueueBatch from @lunora/queue")}),queues:e.queues}),v=e.objectStorageDirectory===void 0?void 0:q({directory:e.objectStorageDirectory}),c=e.globalTablesPath===void 0?void 0:y({path:e.globalTablesPath});c!==void 0&&o.push(()=>{c.dispose()});const w=e.workflows===void 0?void 0:H({store:P(r),workflows:e.workflows}),h=()=>{for(const s of o.toReversed())s()};return{capabilities:N,close:h,directory:d,dispatch:s=>m(s),drain:i,globalTables:c,kv:a,objectStorage:v,queues:S,scheduler:p,shard:l,sockets:f,workflows:w,[Symbol.dispose]:h}}catch(a){for(const t of o.toReversed())try{t()}catch{}throw a}};export{z as createNodePlatform};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as W,createHash as X}from"node:crypto";import{mkdir as L,open as j,unlink as Y,rename as _,rmdir as H,readdir as J}from"node:fs/promises";import{join as y,dirname as T,sep as Z}from"node:path";import{LunoraError as c}from"@lunora/errors";import{h as q}from"./hmac-url-BrwGfbCr.mjs";import{t as P}from"./to-array-buffer-DVyXU4Ep.mjs";const b=".lunora-tmp",k="LNR1",p=8,B=1024,$=255,V=2048,Q=/[%A-Z:]/g,F=e=>{const t=e.replaceAll(Q,n=>`%${n.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`),r=t.at(-1);return r==="."||r===" "?`${t.slice(0,-1)}%${r.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`:t},tt=e=>{try{return decodeURIComponent(e)}catch{return}},A=e=>e.split("/").map(t=>F(t)).join("/"),et=e=>{const t=[];for(const r of e.split("/")){const n=tt(r);if(n===void 0)return;t.push(n)}return t.join("/")},rt=new TextEncoder,nt=e=>rt.encode(e).length,I=e=>{if(typeof e!="string"||e.length===0)throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key must be a non-empty string");if(nt(e)>B)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key exceeds ${String(B)}-byte limit`);if(e.includes("\0"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a NUL byte");if(q(e))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a control character (including CR/LF)");if(e.includes("\\"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a backslash (a path separator on Windows)");if(e.startsWith("/"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key must not start with `/`");for(const t of e.split("/")){if(Buffer.byteLength(F(t),"utf8")>$)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key segment "${t}" exceeds ${String($)} bytes once escaped for the filesystem`);if(t===""||t==="."||t==="..")throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key has an empty, \`.\` or \`..\` path segment ("${e}")`);if(t===b)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key is reserved (no segment may be "${b}")`)}},ot=e=>{if(e===void 0)return;let t=0;for(const[r,n]of Object.entries(e))t+=Buffer.byteLength(r,"utf8")+Buffer.byteLength(n,"utf8");if(t>V)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 customMetadata is ${String(t)} bytes, over the ${String(V)}-byte ceiling R2 applies to the summed keys and values — your metadata headers exceed the maximum allowed metadata size`)},at=/^[\da-f]{64}$/,st=e=>{if(typeof e=="string"){const t=e.toLowerCase();if(!at.test(t))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put sha256 must be 64 hex characters or a 32-byte buffer");return t}if(e.byteLength!==32)throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put sha256 must be 64 hex characters or a 32-byte buffer");return Buffer.from(e).toString("hex")},D=e=>e instanceof Error&&"code"in e&&e.code==="ENOENT",K=(e,t)=>{const r=t instanceof Error&&"code"in t?t.code:void 0;return r==="EEXIST"||r==="EISDIR"||r==="ENOTDIR"?new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key "${e}" collides with an existing object at one of its path prefixes — this host stores one file per key, so a key and a prefix of it cannot both hold objects`):t},it=e=>{const t=Buffer.from(JSON.stringify(e),"utf8"),r=Buffer.alloc(p);return r.writeUInt32BE(t.byteLength,0),r.write(k,4,"ascii"),Buffer.concat([t,r])},ct=e=>{if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.sha256Hex=="string"&&typeof t.size=="number"&&typeof t.uploaded=="string"},ft=async e=>{const t=await e.stat();if(!t.isFile()||t.size<p)return;const r=Buffer.alloc(p);if(await e.read(r,0,p,t.size-p),r.toString("ascii",4,p)!==k)return;const n=r.readUInt32BE(0),o=t.size-p-n;if(n===0||o<0)return;const a=Buffer.alloc(n),{bytesRead:i}=await e.read(a,0,n,o);if(i!==n)return;let s;try{s=JSON.parse(a.toString("utf8"))}catch{return}return ct(s)?{bodySize:o,meta:s}:void 0},z=async e=>{let t;try{t=await j(e,"r")}catch(r){if(D(r))return;throw r}try{const r=await ft(t);if(r===void 0){await t.close();return}return{...r,handle:t}}catch(r){throw await t.close(),r}},C=new FinalizationRegistry(e=>{e.close().catch(()=>{})}),x=async e=>{const t=await z(e);if(t!==void 0)return await t.handle.close(),{bodySize:t.bodySize,meta:t.meta}},E=(e,t)=>{const{sha256Hex:r}=t,n=Buffer.from(r,"hex");return{checksums:{sha256:P(n)},customMetadata:t.customMetadata,etag:r,httpEtag:`"${r}"`,httpMetadata:t.httpMetadata,key:e,sha256:r,sha256Base64:n.toString("base64"),size:t.size,uploaded:new Date(t.uploaded)}},S=(e,t,r)=>e===void 0||!Number.isFinite(e)?t:Math.min(Math.max(0,Math.floor(e)),r),lt=(e,t)=>{if(t===void 0)return{end:e,start:0};if("suffix"in t)return{end:e,start:e-S(t.suffix,e,e)};const r=S(t.offset,0,e);return{end:t.length===void 0||!Number.isFinite(t.length)?e:Math.min(r+S(t.length,0,e),e),start:r}},dt=(e,t,r,n)=>{const o=e.createReadStream({autoClose:!1,end:r-1,start:t})[Symbol.asyncIterator]();return new ReadableStream({cancel:async()=>{await o.return?.(),await n()},pull:async a=>{let i;try{i=await o.next()}catch(s){throw await n(),s}i.done===!0?(a.close(),await n()):a.enqueue(i.value)}})},ut=async(e,t,r)=>{const n=r-t;if(n<=0)return Buffer.alloc(0);const o=Buffer.alloc(n),{bytesRead:a}=await e.read(o,0,n,t);return o.subarray(0,a)},G=async function*(e){if(e!==null){if(typeof e=="string"){yield new TextEncoder().encode(e);return}if(e instanceof ArrayBuffer){yield new Uint8Array(e);return}if(ArrayBuffer.isView(e)){yield new Uint8Array(e.buffer,e.byteOffset,e.byteLength);return}if(e instanceof Blob){yield*G(e.stream());return}if(e instanceof ReadableStream){for await(const t of e)if(t instanceof Uint8Array)yield t;else if(t instanceof ArrayBuffer)yield new Uint8Array(t);else throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put stream must yield byte chunks");return}throw new c("VALIDATION_ERROR","@lunora/platform-node: unsupported R2 put body")}},ht=async e=>{const t=[],r=async(n,o)=>{let a;try{a=await J(n,{withFileTypes:!0})}catch(s){if(D(s))return;throw s}const i=[];for(const s of a){if(s.name===b)continue;const d=o===""?s.name:`${o}/${s.name}`;if(s.isDirectory())i.push(r(y(n,s.name),d));else{const h=et(d);h!==void 0&&t.push(h)}}await Promise.all(i)};return await r(e,""),t.sort((n,o)=>n<o?-1:+(n>o)),t},wt=(e,t)=>{let r=0,n=e.length;for(;r<n;){const o=r+Math.floor((n-r)/2),a=e[o];a===void 0||a<=t?r=o+1:n=o}return r},mt=new Set(["EISDIR","ENOENT","ENOTDIR","EPERM"]),O=async e=>{try{await Y(e)}catch(t){const r=t instanceof Error&&"code"in t?t.code:void 0;if(r===void 0||!mt.has(r))throw t}},Rt=async(e,t)=>{let r=e;for(;r!==t&&r.startsWith(`${t}${Z}`);){try{await H(r)}catch(n){if(!D(n))return}r=T(r)}},U=async(e,t,r)=>{try{await _(e,t)}catch{try{await H(t)}catch{}try{await L(T(t),{recursive:!0}),await _(e,t)}catch(n){throw await O(e),K(r,n)}}},N=new Map,yt=async(e,t)=>{const n=(N.get(e)??Promise.resolve()).then(t,t),o=n.catch(()=>{});N.set(e,o);try{return await n}finally{N.get(e)===o&&N.delete(e)}},pt=async(e,t)=>{const n=(await x(e))?.meta.sha256Hex,o=a=>n!==void 0&&(a==="*"||a===n);return t.etagMatches!==void 0&&!o(t.etagMatches)?!1:t.etagDoesNotMatch===void 0||!o(t.etagDoesNotMatch)},St=e=>{const{directory:t}=e;return{delete:async r=>{const n=typeof r=="string"?[r]:r;for(const o of n)I(o);for(const o of n){const a=y(t,A(o));await O(a),await Rt(a,t)}},get:async(r,n)=>{I(r);const o=await z(y(t,A(r)));if(o===void 0)return null;const{bodySize:a,handle:i,meta:s}=o,{end:d,start:h}=lt(a,n?.range),u={};let w=!1;const l=async()=>{w||(w=!0,C.unregister(u),await i.close())},m=()=>{if(w)throw new c("BAD_REQUEST",`@lunora/platform-node: R2 object "${r}" body has already been consumed`)},R=async()=>{m();try{return await ut(i,h,d)}finally{await l()}},v={...E(r,s),arrayBuffer:async()=>P(await R()),get body(){return m(),d<=h?(l().catch(()=>{}),new Blob([]).stream()):dt(i,h,d,l)},text:async()=>(await R()).toString("utf8")};return C.register(v,{close:l},u),v},head:async r=>{I(r);const n=await x(y(t,A(r)));return n===void 0?null:E(r,n.meta)},list:async(r={})=>{const n=Math.max(1,S(r.limit,1e3,1e3));if(r.prefix?.includes("\0"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 list prefix contains a NUL byte");const o=await ht(t),a=r.prefix??"",i=o.filter(f=>f.startsWith(a)),{delimiter:s}=r,d=s===void 0?i:i.filter(f=>!f.slice(a.length).includes(s)),h=s===void 0?void 0:[...new Set(i.map(f=>{const M=f.slice(a.length).indexOf(s);return M===-1?void 0:f.slice(0,a.length+M+s.length)}).filter(f=>f!==void 0))].toSorted((f,g)=>f<g?-1:+(f>g));let u=r.cursor;r.startAfter!==void 0&&(u===void 0||r.startAfter>u)&&(u=r.startAfter);const w=u===void 0?0:wt(d,u),l=d.slice(w,w+n),m=w+n<d.length,v=(await Promise.all(l.map(async f=>{const g=await x(y(t,A(f)));return g===void 0?void 0:E(f,g.meta)}))).filter(f=>f!==void 0);return{cursor:m?l.at(-1):void 0,delimitedPrefixes:h,objects:v,truncated:m}},put:(async(r,n,o)=>{I(r),ot(o?.customMetadata);const a=o?.sha256===void 0?void 0:st(o.sha256),i=y(t,A(r)),s=y(t,b,W());try{await L(T(i),{recursive:!0})}catch(l){throw K(r,l)}await L(T(s),{recursive:!0});const d=X("sha256");let h=0,u;try{const l=await j(s,"w");try{for await(const R of G(n))d.update(R),h+=R.byteLength,await l.write(R);const m=d.digest("hex");if(a!==void 0&&a!==m)throw new c("VALIDATION_ERROR",`@lunora/platform-node: the SHA-256 checksum you specified for R2 key "${r}" did not match what we received. You provided a SHA-256 checksum with value: ${a}. Actual SHA-256 was: ${m}`);u={customMetadata:o?.customMetadata,httpMetadata:o?.httpMetadata,sha256Hex:m,size:h,uploaded:new Date().toISOString()},await l.write(it(u))}finally{await l.close()}}catch(l){throw await O(s),l}const w=o?.onlyIf;return w!==void 0?yt(r,async()=>await pt(i,w)?(await U(s,i,r),E(r,u)):(await O(s),null)):(await U(s,i,r),E(r,u))})}};export{St as createNodeR2Bucket};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",l=t=>{let e="",n=0;const r=t.length-2;for(;n<r;n+=3){const o=t[n]<<16|t[n+1]<<8|t[n+2];e+=c.charAt(o>>18&63)+c.charAt(o>>12&63)+c.charAt(o>>6&63)+c.charAt(o&63)}const s=t.length-n;if(s===1){const o=t[n]<<16;e+=c.charAt(o>>18&63)+c.charAt(o>>12&63)}else if(s===2){const o=t[n]<<16|t[n+1]<<8;e+=c.charAt(o>>18&63)+c.charAt(o>>12&63)+c.charAt(o>>6&63)}return e},h=(t,e)=>{if(t.size<e)return;const n=t.keys().next().value;n!==void 0&&t.delete(n)},A=(t,e,n,r)=>{const s=t.get(e);if(s!==void 0)return s;h(t,r);const o=n().catch(i=>{throw t.get(e)===o&&t.delete(e),i});return t.set(e,o),o},a=new TextEncoder,d=Array.from({length:32},(t,e)=>e),C=new RegExp(`[${d.map(t=>String.fromCodePoint(t)).join("")}]`,"u"),g=64,f=new Map,u=async t=>A(f,t,async()=>crypto.subtle.importKey("raw",a.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]),g),E=t=>C.test(t),w=async(t,e)=>{const n=await u(t),r=await crypto.subtle.sign("HMAC",n,a.encode(e));return l(new Uint8Array(r))};export{E as h,w as s};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/platform-node",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.130",
|
|
4
4
|
"description": "Node implementation of the Lunora platform contracts: ShardHost, SocketHost, ShardDirectory, ShardKvStore, SchedulerHost over better-sqlite3 and an in-process socket registry (dev/test target)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"better-sqlite3",
|
|
@@ -46,11 +46,12 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/d1": "1.0.0-alpha.
|
|
49
|
+
"@lunora/d1": "1.0.0-alpha.170",
|
|
50
50
|
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
51
|
-
"@lunora/platform": "1.0.0-alpha.
|
|
52
|
-
"@lunora/queue": "1.0.0-alpha.
|
|
53
|
-
"@lunora/
|
|
51
|
+
"@lunora/platform": "1.0.0-alpha.59",
|
|
52
|
+
"@lunora/queue": "1.0.0-alpha.98",
|
|
53
|
+
"@lunora/runtime": "1.0.0-alpha.190",
|
|
54
|
+
"@lunora/sql-store": "1.0.0-alpha.171",
|
|
54
55
|
"@lunora/workflow": "1.0.0-alpha.74",
|
|
55
56
|
"@visulima/workflow": "3.0.0",
|
|
56
57
|
"better-sqlite3": "^13.0.3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as S}from"@lunora/errors";import{NODE_CAPABILITIES as b}from"@lunora/platform";import{createNodeGlobalStore as N}from"./createNodeGlobalStore-DiGxjmD7.mjs";import{createNodeShardKvStore as k}from"./createNodeShardKvStore-DVi1OsvH.mjs";import{createNodeQueueHost as g}from"./createNodeQueueHost-CttDb5sY.mjs";import{createNodeR2Bucket as y}from"./createNodeR2Bucket-ChHs23n-.mjs";import{createNodeSchedulerHost as R}from"./createNodeSchedulerHost-BEAX6I4m.mjs";import{createNodeShardHost as q}from"./createNodeShardHost-Da19DFX0.mjs";import{createNodeShardRegistry as B}from"./createNodeShardRegistry-D-KxwdUc.mjs";import{createNodeSocketHost as H}from"./createNodeSocketHost-DQs85G7W.mjs";import{createNodeWorkflowHost as I}from"./createNodeWorkflowHost-B76TKiw_.mjs";import{createNodeWorkflowStore as P}from"./createNodeWorkflowStore-CL7RYP6H.mjs";const G=(o={})=>{const e=[],{database:r,dispose:d,drain:u,host:i}=q(o);e.push(d);try{const s=k(r),t=B(o);e.push(()=>{t.close()});const{directory:l}=t,{socket:h}=H(r),{dispose:m,scheduler:f}=R(r,o);e.push(m);const n=o.queues===void 0?void 0:g(r,{onBatch:o.onQueueBatch??(()=>{throw new S("VALIDATION_ERROR","@lunora/platform-node: createNodePlatform was given `queues` without `onQueueBatch`, so a delivered batch has nowhere to go — pass dispatchQueueBatch from @lunora/queue")}),queues:o.queues}),p=o.objectStorageDirectory===void 0?void 0:y({directory:o.objectStorageDirectory}),a=o.globalTablesPath===void 0?void 0:N({path:o.globalTablesPath});a!==void 0&&e.push(()=>{a.dispose()});const w=o.workflows===void 0?void 0:I({store:P(r),workflows:o.workflows}),c=()=>{for(const v of e.toReversed())v()};return{capabilities:b,close:c,directory:l,drain:u,globalTables:a,kv:s,objectStorage:p,queues:n,scheduler:f,shard:i,sockets:h,workflows:w,[Symbol.dispose]:c}}catch(s){for(const t of e.toReversed())try{t()}catch{}throw s}};export{G as createNodePlatform};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{randomUUID as W,createHash as X}from"node:crypto";import{mkdir as L,open as U,unlink as Y,rename as M,rmdir as H,readdir as J}from"node:fs/promises";import{join as y,dirname as T,sep as Z}from"node:path";import{LunoraError as c}from"@lunora/errors";import{t as P}from"./to-array-buffer-DVyXU4Ep.mjs";new TextEncoder;const q=Array.from({length:32},(e,t)=>t),Q=new RegExp(`[${q.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),tt=e=>Q.test(e),b=".lunora-tmp",k="LNR1",p=8,B=1024,C=255,$=2048,et=/[%A-Z:]/g,F=e=>{const t=e.replaceAll(et,n=>`%${n.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`),r=t.at(-1);return r==="."||r===" "?`${t.slice(0,-1)}%${r.codePointAt(0)?.toString(16).padStart(2,"0").toUpperCase()??""}`:t},rt=e=>{try{return decodeURIComponent(e)}catch{return}},A=e=>e.split("/").map(t=>F(t)).join("/"),nt=e=>{const t=[];for(const r of e.split("/")){const n=rt(r);if(n===void 0)return;t.push(n)}return t.join("/")},ot=new TextEncoder,at=e=>ot.encode(e).length,I=e=>{if(typeof e!="string"||e.length===0)throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key must be a non-empty string");if(at(e)>B)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key exceeds ${String(B)}-byte limit`);if(e.includes("\0"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a NUL byte");if(tt(e))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a control character (including CR/LF)");if(e.includes("\\"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key contains a backslash (a path separator on Windows)");if(e.startsWith("/"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 key must not start with `/`");for(const t of e.split("/")){if(Buffer.byteLength(F(t),"utf8")>C)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key segment "${t}" exceeds ${String(C)} bytes once escaped for the filesystem`);if(t===""||t==="."||t==="..")throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key has an empty, \`.\` or \`..\` path segment ("${e}")`);if(t===b)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key is reserved (no segment may be "${b}")`)}},st=e=>{if(e===void 0)return;let t=0;for(const[r,n]of Object.entries(e))t+=Buffer.byteLength(r,"utf8")+Buffer.byteLength(n,"utf8");if(t>$)throw new c("VALIDATION_ERROR",`@lunora/platform-node: R2 customMetadata is ${String(t)} bytes, over the ${String($)}-byte ceiling R2 applies to the summed keys and values — your metadata headers exceed the maximum allowed metadata size`)},it=/^[\da-f]{64}$/,ct=e=>{if(typeof e=="string"){const t=e.toLowerCase();if(!it.test(t))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put sha256 must be 64 hex characters or a 32-byte buffer");return t}if(e.byteLength!==32)throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put sha256 must be 64 hex characters or a 32-byte buffer");return Buffer.from(e).toString("hex")},x=e=>e instanceof Error&&"code"in e&&e.code==="ENOENT",K=(e,t)=>{const r=t instanceof Error&&"code"in t?t.code:void 0;return r==="EEXIST"||r==="EISDIR"||r==="ENOTDIR"?new c("VALIDATION_ERROR",`@lunora/platform-node: R2 key "${e}" collides with an existing object at one of its path prefixes — this host stores one file per key, so a key and a prefix of it cannot both hold objects`):t},ft=e=>{const t=Buffer.from(JSON.stringify(e),"utf8"),r=Buffer.alloc(p);return r.writeUInt32BE(t.byteLength,0),r.write(k,4,"ascii"),Buffer.concat([t,r])},lt=e=>{if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.sha256Hex=="string"&&typeof t.size=="number"&&typeof t.uploaded=="string"},dt=async e=>{const t=await e.stat();if(!t.isFile()||t.size<p)return;const r=Buffer.alloc(p);if(await e.read(r,0,p,t.size-p),r.toString("ascii",4,p)!==k)return;const n=r.readUInt32BE(0),o=t.size-p-n;if(n===0||o<0)return;const a=Buffer.alloc(n),{bytesRead:i}=await e.read(a,0,n,o);if(i!==n)return;let s;try{s=JSON.parse(a.toString("utf8"))}catch{return}return lt(s)?{bodySize:o,meta:s}:void 0},z=async e=>{let t;try{t=await U(e,"r")}catch(r){if(x(r))return;throw r}try{const r=await dt(t);if(r===void 0){await t.close();return}return{...r,handle:t}}catch(r){throw await t.close(),r}},V=new FinalizationRegistry(e=>{e.close().catch(()=>{})}),_=async e=>{const t=await z(e);if(t!==void 0)return await t.handle.close(),{bodySize:t.bodySize,meta:t.meta}},E=(e,t)=>{const{sha256Hex:r}=t,n=Buffer.from(r,"hex");return{checksums:{sha256:P(n)},customMetadata:t.customMetadata,etag:r,httpEtag:`"${r}"`,httpMetadata:t.httpMetadata,key:e,sha256:r,sha256Base64:n.toString("base64"),size:t.size,uploaded:new Date(t.uploaded)}},N=(e,t,r)=>e===void 0||!Number.isFinite(e)?t:Math.min(Math.max(0,Math.floor(e)),r),ut=(e,t)=>{if(t===void 0)return{end:e,start:0};if("suffix"in t)return{end:e,start:e-N(t.suffix,e,e)};const r=N(t.offset,0,e);return{end:t.length===void 0||!Number.isFinite(t.length)?e:Math.min(r+N(t.length,0,e),e),start:r}},ht=(e,t,r,n)=>{const o=e.createReadStream({autoClose:!1,end:r-1,start:t})[Symbol.asyncIterator]();return new ReadableStream({cancel:async()=>{await o.return?.(),await n()},pull:async a=>{let i;try{i=await o.next()}catch(s){throw await n(),s}i.done===!0?(a.close(),await n()):a.enqueue(i.value)}})},wt=async(e,t,r)=>{const n=r-t;if(n<=0)return Buffer.alloc(0);const o=Buffer.alloc(n),{bytesRead:a}=await e.read(o,0,n,t);return o.subarray(0,a)},G=async function*(e){if(e!==null){if(typeof e=="string"){yield new TextEncoder().encode(e);return}if(e instanceof ArrayBuffer){yield new Uint8Array(e);return}if(ArrayBuffer.isView(e)){yield new Uint8Array(e.buffer,e.byteOffset,e.byteLength);return}if(e instanceof Blob){yield*G(e.stream());return}if(e instanceof ReadableStream){for await(const t of e)if(t instanceof Uint8Array)yield t;else if(t instanceof ArrayBuffer)yield new Uint8Array(t);else throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 put stream must yield byte chunks");return}throw new c("VALIDATION_ERROR","@lunora/platform-node: unsupported R2 put body")}},mt=async e=>{const t=[],r=async(n,o)=>{let a;try{a=await J(n,{withFileTypes:!0})}catch(s){if(x(s))return;throw s}const i=[];for(const s of a){if(s.name===b)continue;const d=o===""?s.name:`${o}/${s.name}`;if(s.isDirectory())i.push(r(y(n,s.name),d));else{const h=nt(d);h!==void 0&&t.push(h)}}await Promise.all(i)};return await r(e,""),t.sort((n,o)=>n<o?-1:+(n>o)),t},Rt=(e,t)=>{let r=0,n=e.length;for(;r<n;){const o=r+Math.floor((n-r)/2),a=e[o];a===void 0||a<=t?r=o+1:n=o}return r},yt=new Set(["EISDIR","ENOENT","ENOTDIR","EPERM"]),S=async e=>{try{await Y(e)}catch(t){const r=t instanceof Error&&"code"in t?t.code:void 0;if(r===void 0||!yt.has(r))throw t}},pt=async(e,t)=>{let r=e;for(;r!==t&&r.startsWith(`${t}${Z}`);){try{await H(r)}catch(n){if(!x(n))return}r=T(r)}},j=async(e,t,r)=>{try{await M(e,t)}catch{try{await H(t)}catch{}try{await L(T(t),{recursive:!0}),await M(e,t)}catch(n){throw await S(e),K(r,n)}}},O=new Map,gt=async(e,t)=>{const n=(O.get(e)??Promise.resolve()).then(t,t),o=n.catch(()=>{});O.set(e,o);try{return await n}finally{O.get(e)===o&&O.delete(e)}},At=async(e,t)=>{const n=(await _(e))?.meta.sha256Hex,o=a=>n!==void 0&&(a==="*"||a===n);return t.etagMatches!==void 0&&!o(t.etagMatches)?!1:t.etagDoesNotMatch===void 0||!o(t.etagDoesNotMatch)},St=e=>{const{directory:t}=e;return{delete:async r=>{const n=typeof r=="string"?[r]:r;for(const o of n)I(o);for(const o of n){const a=y(t,A(o));await S(a),await pt(a,t)}},get:async(r,n)=>{I(r);const o=await z(y(t,A(r)));if(o===void 0)return null;const{bodySize:a,handle:i,meta:s}=o,{end:d,start:h}=ut(a,n?.range),u={};let w=!1;const l=async()=>{w||(w=!0,V.unregister(u),await i.close())},m=()=>{if(w)throw new c("BAD_REQUEST",`@lunora/platform-node: R2 object "${r}" body has already been consumed`)},R=async()=>{m();try{return await wt(i,h,d)}finally{await l()}},v={...E(r,s),arrayBuffer:async()=>P(await R()),get body(){return m(),d<=h?(l().catch(()=>{}),new Blob([]).stream()):ht(i,h,d,l)},text:async()=>(await R()).toString("utf8")};return V.register(v,{close:l},u),v},head:async r=>{I(r);const n=await _(y(t,A(r)));return n===void 0?null:E(r,n.meta)},list:async(r={})=>{const n=Math.max(1,N(r.limit,1e3,1e3));if(r.prefix?.includes("\0"))throw new c("VALIDATION_ERROR","@lunora/platform-node: R2 list prefix contains a NUL byte");const o=await mt(t),a=r.prefix??"",i=o.filter(f=>f.startsWith(a)),{delimiter:s}=r,d=s===void 0?i:i.filter(f=>!f.slice(a.length).includes(s)),h=s===void 0?void 0:[...new Set(i.map(f=>{const D=f.slice(a.length).indexOf(s);return D===-1?void 0:f.slice(0,a.length+D+s.length)}).filter(f=>f!==void 0))].toSorted((f,g)=>f<g?-1:+(f>g));let u=r.cursor;r.startAfter!==void 0&&(u===void 0||r.startAfter>u)&&(u=r.startAfter);const w=u===void 0?0:Rt(d,u),l=d.slice(w,w+n),m=w+n<d.length,v=(await Promise.all(l.map(async f=>{const g=await _(y(t,A(f)));return g===void 0?void 0:E(f,g.meta)}))).filter(f=>f!==void 0);return{cursor:m?l.at(-1):void 0,delimitedPrefixes:h,objects:v,truncated:m}},put:(async(r,n,o)=>{I(r),st(o?.customMetadata);const a=o?.sha256===void 0?void 0:ct(o.sha256),i=y(t,A(r)),s=y(t,b,W());try{await L(T(i),{recursive:!0})}catch(l){throw K(r,l)}await L(T(s),{recursive:!0});const d=X("sha256");let h=0,u;try{const l=await U(s,"w");try{for await(const R of G(n))d.update(R),h+=R.byteLength,await l.write(R);const m=d.digest("hex");if(a!==void 0&&a!==m)throw new c("VALIDATION_ERROR",`@lunora/platform-node: the SHA-256 checksum you specified for R2 key "${r}" did not match what we received. You provided a SHA-256 checksum with value: ${a}. Actual SHA-256 was: ${m}`);u={customMetadata:o?.customMetadata,httpMetadata:o?.httpMetadata,sha256Hex:m,size:h,uploaded:new Date().toISOString()},await l.write(ft(u))}finally{await l.close()}}catch(l){throw await S(s),l}const w=o?.onlyIf;return w!==void 0?gt(r,async()=>await At(i,w)?(await j(s,i,r),E(r,u)):(await S(s),null)):(await j(s,i,r),E(r,u))})}};export{St as createNodeR2Bucket};
|