@lunora/platform 1.0.0-alpha.22 → 1.0.0-alpha.23
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/conformance/index.d.mts +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/suite.d.mts +1 -1
- package/dist/conformance/suite.d.ts +1 -1
- package/dist/index.d.mts +41 -6
- package/dist/index.d.ts +41 -6
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/{CLOUDFLARE_CAPABILITIES-5FtyJhS9.mjs → CLOUDFLARE_CAPABILITIES-DJiWTHJf.mjs} +1 -1
- package/dist/packem_shared/{socket-host.d-dVPE86WP.d.mts → socket-host.d-DVgQkG8Z.d.mts} +30 -3
- package/dist/packem_shared/{socket-host.d-dVPE86WP.d.ts → socket-host.d-DVgQkG8Z.d.ts} +30 -3
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { type C as ConformanceHost, type a as ConformanceHostFactory, type R as ReferenceHost, type VitestApi, c as createReferenceHost, defineHostContractSuite } from "./suite.mjs";
|
|
2
|
-
import "../packem_shared/socket-host.d-
|
|
2
|
+
import "../packem_shared/socket-host.d-DVgQkG8Z.mjs";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { type C as ConformanceHost, type a as ConformanceHostFactory, type R as ReferenceHost, type VitestApi, c as createReferenceHost, defineHostContractSuite } from "./suite.js";
|
|
2
|
-
import "../packem_shared/socket-host.d-
|
|
2
|
+
import "../packem_shared/socket-host.d-DVgQkG8Z.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-
|
|
1
|
+
import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-DVgQkG8Z.mjs";
|
|
2
2
|
/**
|
|
3
3
|
* A conformance host bundles all four platform contracts so a single factory
|
|
4
4
|
* can stand up a complete, isolated test environment.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-
|
|
1
|
+
import { e as ShardDirectory, i as ShardKvStore, n as SocketHandle, c as SchedulerHost, f as ShardHost, o as SocketHost } from "../packem_shared/socket-host.d-DVgQkG8Z.js";
|
|
2
2
|
/**
|
|
3
3
|
* A conformance host bundles all four platform contracts so a single factory
|
|
4
4
|
* can stand up a complete, isolated test environment.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardDirectory, type f as ShardHost, type g as ShardJurisdiction, type h as ShardKvListOptions, type i as ShardKvStore, type j as ShardRegionHint, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-
|
|
1
|
+
export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardDirectory, type f as ShardHost, type g as ShardJurisdiction, type h as ShardKvListOptions, type i as ShardKvStore, type j as ShardRegionHint, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-DVgQkG8Z.mjs";
|
|
2
2
|
/**
|
|
3
3
|
* The subset of the Cloudflare `ExecutionContext` the Lunora worker entry and
|
|
4
4
|
* the framework mount seams rely on — `waitUntil` for fire-and-forget work that
|
|
@@ -565,14 +565,42 @@ interface QueueRetryOptions {
|
|
|
565
565
|
* Every other key here — `commitOrderedTables`, `httpCache`, `identityProxy`,
|
|
566
566
|
* `localSql`, `memoryTables`, `objectStorageBackups`,
|
|
567
567
|
* `objectStorageCdcArchive`, `serverReactors`, `shardAlarms`, `shardedState`,
|
|
568
|
-
* `shardPlacement`, `shardReadReplicas`, `websocketHibernation` — is
|
|
569
|
-
*
|
|
570
|
-
* from, so rating one `unsupported` omits no surface and warns nobody. It
|
|
568
|
+
* `shardPlacement`, `shardReadReplicas`, `websocketHibernation` — is
|
|
569
|
+
* **advisory**: rating one `unsupported` omits no surface and warns nobody. It
|
|
571
570
|
* still records parity honestly, which is its job; it is not a gate.
|
|
572
571
|
*
|
|
572
|
+
* # Advisory is not one thing — there are two reasons, and only one is final
|
|
573
|
+
*
|
|
574
|
+
* Most advisory keys are advisory *by nature*: the feature is engine-internal
|
|
575
|
+
* (`shardAlarms`, `shardedState`, `shardPlacement`, `shardReadReplicas`,
|
|
576
|
+
* `websocketHibernation`, `localSql`, `serverReactors`) or degrades honestly on
|
|
577
|
+
* its own (`httpCache` falls back to headers-only, `identityProxy` to header
|
|
578
|
+
* verification). There is nothing an app declares for codegen to notice, so
|
|
579
|
+
* there is nothing to gate. These stay ratings, permanently.
|
|
580
|
+
*
|
|
581
|
+
* The rest are advisory only because nobody wired them, and they are the ones
|
|
582
|
+
* to watch: an app DOES declare the feature, codegen CAN see the declaration,
|
|
583
|
+
* and the rating is still consulted by nothing. Codegen already has the shape
|
|
584
|
+
* for exactly this — `PlatformSignals` in `platform-target.ts`, the second gate
|
|
585
|
+
* pass that diagnoses app-declared features with no `ctx.*` capability row
|
|
586
|
+
* (`commitOrderedTables`, `globalTables`, `durableStreams`, `crossShardFanout`,
|
|
587
|
+
* `queues`, `secrets`). Promoting one is three lines there: a `PlatformSignals`
|
|
588
|
+
* field, plus its entry in that module's signal-key list and its human-readable
|
|
589
|
+
* label — and then setting the signal from the IR.
|
|
590
|
+
*
|
|
591
|
+
* `commitOrderedTables` was promoted that way: `TableIR.commitOrdered` sits in
|
|
592
|
+
* the same IR that feeds `globalTables`, and until it was read a host rating it
|
|
593
|
+
* `unsupported` emitted the full `.commitOrdered()` surface with no diagnostic
|
|
594
|
+
* and silently lost commit ordering — the one guarantee the feature is.
|
|
595
|
+
* `memoryTables`, `objectStorageBackups` and `objectStorageCdcArchive` remain
|
|
596
|
+
* weaker instances of the same shape, still unpromoted.
|
|
597
|
+
*
|
|
573
598
|
* **Adding a feature key is therefore half a change.** The other half is a row
|
|
574
|
-
* in `CAPABILITY_ROWS` and an entry in `CAPABILITY_TO_FEATURE
|
|
575
|
-
*
|
|
599
|
+
* in `CAPABILITY_ROWS` and an entry in `CAPABILITY_TO_FEATURE` (for an
|
|
600
|
+
* app-imported `ctx.*` module), or a `PlatformSignals` entry (for something the
|
|
601
|
+
* app declares in its schema), or a deliberate decision that the key is advisory
|
|
602
|
+
* by nature — recorded here. Silence means the rating ships as documentation
|
|
603
|
+
* while the surface it describes is emitted anyway.
|
|
576
604
|
*/
|
|
577
605
|
/** Support level for a single feature on a target platform. */
|
|
578
606
|
type CapabilityLevel = "native" | "emulated" | "unsupported";
|
|
@@ -608,6 +636,13 @@ interface PlatformCapabilities {
|
|
|
608
636
|
* interleave their allocations. A host that offers neither can still
|
|
609
637
|
* create the counter and hand out increasing numbers — they just would
|
|
610
638
|
* not order commits, which is the whole contract.
|
|
639
|
+
*
|
|
640
|
+
* **Advisory today, and it should not be.** Nothing consults this
|
|
641
|
+
* rating: a host rating it `unsupported` still gets the full
|
|
642
|
+
* `.commitOrdered()` surface emitted, with no diagnostic. `TableIR`
|
|
643
|
+
* carries `commitOrdered` in the same IR codegen already reads for the
|
|
644
|
+
* `globalTables` signal, so this is a `PlatformSignals` entry away from
|
|
645
|
+
* being gate-bearing — see this module's header.
|
|
611
646
|
*/
|
|
612
647
|
commitOrderedTables?: Capability;
|
|
613
648
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardDirectory, type f as ShardHost, type g as ShardJurisdiction, type h as ShardKvListOptions, type i as ShardKvStore, type j as ShardRegionHint, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-
|
|
1
|
+
export { type D as DirectShardDirectory, type S as ScheduleOptions, type a as ScheduledJob, type b as ScheduledJobStatus, type c as SchedulerHost, type d as ShardAlarms, type e as ShardDirectory, type f as ShardHost, type g as ShardJurisdiction, type h as ShardKvListOptions, type i as ShardKvStore, type j as ShardRegionHint, type k as ShardSqlCursor, type l as ShardSqlExec, type m as ShardStub, type n as SocketHandle, type o as SocketHost, type p as SqlRow, type T as TwoStepShardDirectory, r as resolveShard } from "./packem_shared/socket-host.d-DVgQkG8Z.js";
|
|
2
2
|
/**
|
|
3
3
|
* The subset of the Cloudflare `ExecutionContext` the Lunora worker entry and
|
|
4
4
|
* the framework mount seams rely on — `waitUntil` for fire-and-forget work that
|
|
@@ -565,14 +565,42 @@ interface QueueRetryOptions {
|
|
|
565
565
|
* Every other key here — `commitOrderedTables`, `httpCache`, `identityProxy`,
|
|
566
566
|
* `localSql`, `memoryTables`, `objectStorageBackups`,
|
|
567
567
|
* `objectStorageCdcArchive`, `serverReactors`, `shardAlarms`, `shardedState`,
|
|
568
|
-
* `shardPlacement`, `shardReadReplicas`, `websocketHibernation` — is
|
|
569
|
-
*
|
|
570
|
-
* from, so rating one `unsupported` omits no surface and warns nobody. It
|
|
568
|
+
* `shardPlacement`, `shardReadReplicas`, `websocketHibernation` — is
|
|
569
|
+
* **advisory**: rating one `unsupported` omits no surface and warns nobody. It
|
|
571
570
|
* still records parity honestly, which is its job; it is not a gate.
|
|
572
571
|
*
|
|
572
|
+
* # Advisory is not one thing — there are two reasons, and only one is final
|
|
573
|
+
*
|
|
574
|
+
* Most advisory keys are advisory *by nature*: the feature is engine-internal
|
|
575
|
+
* (`shardAlarms`, `shardedState`, `shardPlacement`, `shardReadReplicas`,
|
|
576
|
+
* `websocketHibernation`, `localSql`, `serverReactors`) or degrades honestly on
|
|
577
|
+
* its own (`httpCache` falls back to headers-only, `identityProxy` to header
|
|
578
|
+
* verification). There is nothing an app declares for codegen to notice, so
|
|
579
|
+
* there is nothing to gate. These stay ratings, permanently.
|
|
580
|
+
*
|
|
581
|
+
* The rest are advisory only because nobody wired them, and they are the ones
|
|
582
|
+
* to watch: an app DOES declare the feature, codegen CAN see the declaration,
|
|
583
|
+
* and the rating is still consulted by nothing. Codegen already has the shape
|
|
584
|
+
* for exactly this — `PlatformSignals` in `platform-target.ts`, the second gate
|
|
585
|
+
* pass that diagnoses app-declared features with no `ctx.*` capability row
|
|
586
|
+
* (`commitOrderedTables`, `globalTables`, `durableStreams`, `crossShardFanout`,
|
|
587
|
+
* `queues`, `secrets`). Promoting one is three lines there: a `PlatformSignals`
|
|
588
|
+
* field, plus its entry in that module's signal-key list and its human-readable
|
|
589
|
+
* label — and then setting the signal from the IR.
|
|
590
|
+
*
|
|
591
|
+
* `commitOrderedTables` was promoted that way: `TableIR.commitOrdered` sits in
|
|
592
|
+
* the same IR that feeds `globalTables`, and until it was read a host rating it
|
|
593
|
+
* `unsupported` emitted the full `.commitOrdered()` surface with no diagnostic
|
|
594
|
+
* and silently lost commit ordering — the one guarantee the feature is.
|
|
595
|
+
* `memoryTables`, `objectStorageBackups` and `objectStorageCdcArchive` remain
|
|
596
|
+
* weaker instances of the same shape, still unpromoted.
|
|
597
|
+
*
|
|
573
598
|
* **Adding a feature key is therefore half a change.** The other half is a row
|
|
574
|
-
* in `CAPABILITY_ROWS` and an entry in `CAPABILITY_TO_FEATURE
|
|
575
|
-
*
|
|
599
|
+
* in `CAPABILITY_ROWS` and an entry in `CAPABILITY_TO_FEATURE` (for an
|
|
600
|
+
* app-imported `ctx.*` module), or a `PlatformSignals` entry (for something the
|
|
601
|
+
* app declares in its schema), or a deliberate decision that the key is advisory
|
|
602
|
+
* by nature — recorded here. Silence means the rating ships as documentation
|
|
603
|
+
* while the surface it describes is emitted anyway.
|
|
576
604
|
*/
|
|
577
605
|
/** Support level for a single feature on a target platform. */
|
|
578
606
|
type CapabilityLevel = "native" | "emulated" | "unsupported";
|
|
@@ -608,6 +636,13 @@ interface PlatformCapabilities {
|
|
|
608
636
|
* interleave their allocations. A host that offers neither can still
|
|
609
637
|
* create the counter and hand out increasing numbers — they just would
|
|
610
638
|
* not order commits, which is the whole contract.
|
|
639
|
+
*
|
|
640
|
+
* **Advisory today, and it should not be.** Nothing consults this
|
|
641
|
+
* rating: a host rating it `unsupported` still gets the full
|
|
642
|
+
* `.commitOrdered()` surface emitted, with no diagnostic. `TableIR`
|
|
643
|
+
* carries `commitOrdered` in the same IR codegen already reads for the
|
|
644
|
+
* `globalTables` signal, so this is a `PlatformSignals` entry away from
|
|
645
|
+
* being gate-bearing — see this module's header.
|
|
611
646
|
*/
|
|
612
647
|
commitOrderedTables?: Capability;
|
|
613
648
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{NOOP_EXECUTION_CONTEXT as E}from"./packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs";import{CLOUDFLARE_CAPABILITIES as O,NODE_CAPABILITIES as e}from"./packem_shared/CLOUDFLARE_CAPABILITIES-
|
|
1
|
+
import{NOOP_EXECUTION_CONTEXT as E}from"./packem_shared/NOOP_EXECUTION_CONTEXT-YmXqH-jH.mjs";import{CLOUDFLARE_CAPABILITIES as O,NODE_CAPABILITIES as e}from"./packem_shared/CLOUDFLARE_CAPABILITIES-DJiWTHJf.mjs";import{resolveShard as C}from"./packem_shared/resolveShard-BzKOUEO4.mjs";export{O as CLOUDFLARE_CAPABILITIES,e as NODE_CAPABILITIES,E as NOOP_EXECUTION_CONTEXT,C as resolveShard};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const e={id:"cloudflare",name:"Cloudflare",features:{shardedState:{level:"native",note:"Durable Objects with SQLite"},globalTables:{level:"native",note:"D1 with Sessions API. D1 has a documented, expected baseline error rate — Cloudflare's own team calls a handful of transient errors every few hours 'not unexpected' on a healthy database — so read-only statements are retried automatically; writes are not, because every one of those errors is ambiguous about whether the statement applied and D1 has no interactive transactions to resolve it"},websocketHibernation:{level:"native",note:"DO WebSocket hibernation"},durableStreams:{level:"emulated",note:"Lunora persists each chunk to the shard's SQLite under a monotonic seq and keeps the producer alive past the socket via waitUntil; the platform has no streaming primitive of its own, and a run whose DO is evicted mid-flight ends as STREAM_INTERRUPTED rather than resuming"},commitOrderedTables:{level:"native",note:"`state.storage.transaction` makes the `__commit_seq` bump atomic with the rows it stamps, and a Durable Object executes one event at a time — so the allocation order IS the commit order, with no lock of ours in the path"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},serverReactors:{level:"emulated",note:"The wake-up is Lunora's, not the platform's: reactors ride the existing post-write refresh drain, which already exists to push subscription frames. Cloudflare supplies the two properties that make it correct — one event at a time per Durable Object, and `waitUntil` to keep the drain alive past the response — but has no notion of a server-side subscription of its own"},memoryTables:{level:"emulated",note:"The lifetime is real — an eviction drops the DO's heap and the framework clears every `.memory()` table on reconstruction, so the rows behave exactly like heap state, and their writes stay out of the CDC changelog. The STORAGE is not: workerd exposes one SQL handle and no memory-backed database, so a memory row is still written to the DO's SQLite and then deleted. `.memory()` buys the semantics, not the write"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},shardPlacement:{level:"native",note:"DurableObjectNamespace.get/getByName locationHint — best-effort, and honoured only by the resolution that creates the object"},shardReadReplicas:{level:"emulated",note:"Lunora follows the shard's CDC changelog into a replica DO placed in the reader's region; the platform replicates for durability, not for reads, so the follow loop is ours"},crossShardFanout:{level:"emulated",note:"Lunora query coordinator + relay tier over Durable Objects"},queues:{level:"native",note:"Cloudflare Queues"},workflows:{level:"native",note:"Cloudflare Workflows"},scheduler:{level:"emulated",note:"SchedulerDO (Lunora, on DO alarms) + declarative Cron Triggers; no runtime cron registration"},objectStorage:{level:"native",note:"R2"},objectStorageBackups:{level:"emulated",note:"`lunora backup create|list|restore --bucket` writes NDJSON snapshots + a manifest sidecar per snapshot through the admin storage routes (checksum-verified upload, admin-gated object read), and `backupCron`/`backupStore` runs the same layout unattended on a Cron Trigger. Both are bounded by what a single request body / a Worker isolate can hold, not by R2. `emulated` because every part of that is Lunora's — R2 supplies a bucket, and Cloudflare has no backup product being consumed here; the snapshot format, the manifest, the checksum gate and the retention report are all ours"},objectStorageCdcArchive:{level:"emulated",note:"R2 supplies the bucket and the `startAfter` listing the segment keys are indexed on; everything above that is Lunora's — the segment format, the archive-before-trim ordering the sweep defers behind `waitUntil`, and the de-overlapping read-back. The platform has no notion of a changelog to tier, so this is not a product being consumed"},keyValueStore:{level:"native",note:"Workers KV"},vectorStore:{level:"native",note:"Vectorize; query/upsert namespace scoping is native (remote filter), but getByIds/deleteByIds id-path tenant isolation is facade-enforced (client-side verification) since Vectorize's id operations take no namespace option"},ai:{level:"native",note:"Workers AI"},browser:{level:"native",note:"Browser Rendering"},images:{level:"native",note:"Cloudflare Images binding"},containers:{level:"native",note:"Cloudflare Containers; ctx.containers.<name>.exec rides the same binding over the /__lunora/exec contract, which the container image serves"},analytics:{level:"native",note:"Analytics Engine"},pipelines:{level:"native",note:"Cloudflare Pipelines"},mail:{level:"emulated",note:"Resend (third-party) via Cloudflare Queues"},secrets:{level:"native",note:"Secrets Store"},hyperdrive:{level:"native",note:"Cloudflare Hyperdrive"},httpCache:{level:"native",note:"The colo cache via caches.default. Worker-generated responses are NOT stored by it automatically — the runtime has to caches.default.put() them — and it honours Vary for Accept-Encoding only, so a varying response has to fold those header values into the cache key itself. A 206, a Vary: *, or a Set-Cookie-bearing response is refused by put()"},identityProxy:{level:"native",note:"Cloudflare Access. A policy attached to the Worker covers its custom domains, routes, workers.dev and preview URLs at once, and the authenticated identity arrives on the execution context as ctx.access — no header to verify, and nothing a request can forge to manufacture one. A hostname-scoped Access application instead stamps the Cf-Access-Jwt-Assertion header, which needs no host support at all"}}},t={id:"node",name:"Node",features:{shardedState:{level:"emulated",note:"One better-sqlite3 database per shard key, one process — no distributed placement or failover. Shard keys are percent-encoded into basenames with A-Z escaped, so `Tenant` and `tenant` stay two databases on a case-insensitive volume (APFS, NTFS) rather than folding into one"},globalTables:{level:"emulated",note:"The @lunora/sql-store core on its own SQLite file via the reference sqliteDialect — full store semantics, but one node with no replication"},websocketHibernation:{level:"emulated",note:"Socket registry with attachments/tags persisted to SQLite, so subscription state survives a process restart; nothing is ever actually evicted from memory, so this is durability without hibernation's memory saving"},durableStreams:{level:"unsupported",note:"The transcript store is host-neutral (@lunora/shard-engine), but the attach/produce state machine lives in @lunora/do and nothing in this host mounts it. Gate-bearing: codegen refuses an app that declares a durable stream on this target, rather than emitting one that silently behaves as an ephemeral stream"},commitOrderedTables:{level:"emulated",note:"The sequence orders commits correctly, but the serialization it depends on is Lunora's per-shard write gate rather than a platform property — one process, one better-sqlite3 handle per shard key. Correct here; not something the host guarantees the way a Durable Object does"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},serverReactors:{level:"emulated",note:"Same engine-level implementation as Cloudflare; the per-shard serialization it depends on is the host's own write gate rather than a platform guarantee"},memoryTables:{level:"emulated",note:"Same shape as Cloudflare and for a different reason: better-sqlite3 CAN open `:memory:`, but a shard's memory tables share the one handle its durable tables use, so they are cleared rather than never written. A host process also outlives far more than a Durable Object does, so cold starts — and therefore `onShardInit` — are much rarer here than in production on Cloudflare; do not use this target to judge how often a memory table is actually empty"},shardAlarms:{level:"emulated",note:"setTimeout over a durable row, dispatched to onAlarm and re-armed on construction, so an alarm survives a restart and one whose time elapsed while the process was down fires late rather than never"},shardPlacement:{level:"unsupported",note:"One process — every shard lives where the process does, so a location hint has nowhere to place it"},shardReadReplicas:{level:"unsupported",note:"One process and one region: a replica here would be a second copy of a database already on the same disk"},crossShardFanout:{level:"emulated",note:"@lunora/runtime's query coordinator over the in-process shard registry; listShardKeys is seeded from the shard files on disk, and answers every shard rather than only those holding the table (a correct superset, at the cost of visiting shards with nothing to say)"},queues:{level:"emulated",note:`createNodeQueueHost (@lunora/platform-node) — a QueueBindingLike producer per declared queue over a durable _lunora_queue_messages table, and a batched consumer feeding the same dispatchQueueBatch the Cloudflare host uses. delaySeconds (capped at 12h), all four content types, maxBatchSize/maxBatchTimeout assembly, per-message ack/retry with workerd's implicit-ack-on-return and retry-on-throw, maxRetries into a declared deadLetterQueue (or parked in place, never dropped), and a visibility window so a crash mid-handler redelivers. Delivery is driven by poll(); there is no timer, because this host has no dev server to own one. mode: "pull" queues are written but not consumed — nothing here serves the HTTP pull endpoint`},workflows:{level:"emulated",note:"createNodeWorkflowHost (@lunora/platform-node) compiles defineWorkflow handlers onto the @visulima/workflow engine (createRuntime): step/sleep/waitForEvent are durable + replay-safe, status maps to complete/errored/waiting/terminated, create({ id }) is honoured through a durable alias row (so ctx.spawn resolves and a retried create is one run), and runs survive a restart when backed by createNodeWorkflowStore (a SQLite WorkflowStore; the store is required, so no caller silently gets in-process-only state). Gaps: no pause/restart; terminate is not a barrier, so an activation already in flight overwrites the tombstone; ctx.run dispatches to an endpoint no Node HTTP server serves; ctx.parallel's synchronous join cannot interleave within one trigger activation"},scheduler:{level:"emulated",note:"SQLite job table dispatched to onDispatch and re-armed on construction, with retry backoff and a dead-letter queue; the only host implementing runtime cron registration (SchedulerHost.cron), which Cloudflare cannot offer"},objectStorageBackups:{level:"emulated",note:"The commands work unchanged, but the bucket underneath is createNodeR2Bucket — a directory on the same machine the CLI runs on, so a bucket-backed backup here is not the separate failure domain it is on Cloudflare. The scheduled half additionally needs this host's scheduler, which exists but is not a shipping target"},objectStorageCdcArchive:{level:"emulated",note:"createNodeR2Bucket implements the `startAfter` seek the segment index needs, so the read-back behaves as it does on R2. Same caveat as the backups above: the bucket is a directory on the machine running the host, so archiving the changelog here moves it off SQLite but not off the disk that would take the shard with it"},objectStorage:{level:"emulated",note:"createNodeR2Bucket (@lunora/platform-node) — an R2BucketLike over the local filesystem (fs/promises, head/list/range). One file per object with the metadata in a trailer, so the single rename that publishes the bytes publishes their checksum and content-type with them, and a get reads body and metadata through one handle rather than reopening the path. put streams into the staged file and .body streams the requested range; .arrayBuffer()/.text() still allocate the range they return. The body is single-use, as R2's is. Keys fold the way the host filesystem folds them, so `A` and `a` are one object on a case-insensitive volume where real R2 keeps two. No multipart uploads, no presigned URLs"},keyValueStore:{level:"emulated",note:"better-sqlite3 table behind the ShardKvStore API — not a dedicated KV product"},vectorStore:{level:"unsupported",note:"No Vectorize-equivalent binding implemented"},ai:{level:"unsupported",note:"No Workers AI-equivalent binding implemented"},browser:{level:"unsupported",note:"No headless-browser binding implemented"},images:{level:"unsupported",note:"No Images-equivalent binding implemented"},containers:{level:"unsupported",note:"No container orchestration implemented, so there is nothing for ctx.containers.<name>.exec to run a command in either"},analytics:{level:"unsupported",note:"No Analytics Engine-equivalent binding implemented"},pipelines:{level:"unsupported",note:"No Pipelines-equivalent binding implemented"},mail:{level:"unsupported",note:"The queue tier this host lacked when the rating was written now exists (createNodeQueueHost), but nothing here composes a @lunora/mail transport or the queued-send consumer, so a send would be accepted and never delivered"},secrets:{level:"unsupported",note:"No Secrets Store-equivalent binding implemented (a real host would likely map this to env vars). Gate-bearing, and it has to be: ctx.secrets is a core built-in spliced into every context, so codegen refuses an app that reads it on this target instead of emitting a surface that throws on first use"},hyperdrive:{level:"unsupported",note:"No connection-pooling binding implemented"},httpCache:{level:"unsupported",note:"Nothing sits in front of this host to cache its responses, and Node exposes no Web Cache API global — the runtime's REST edge cache finds no HttpCacheLike here and degrades to emitting Cache-Control alone, which browsers and any CDN in front still honour"},identityProxy:{level:"unsupported",note:"Nothing sits in front of this host to authenticate callers, so it never populates the execution context's access identity. @lunora/cloudflare-access still works here through its Cf-Access-Jwt-Assertion fallback, which is a plain header check and needs no host support"}}};export{e as CLOUDFLARE_CAPABILITIES,t as NODE_CAPABILITIES};
|
|
1
|
+
const e={id:"cloudflare",name:"Cloudflare",features:{shardedState:{level:"native",note:"Durable Objects with SQLite"},globalTables:{level:"native",note:"D1 with Sessions API. D1 has a documented, expected baseline error rate — Cloudflare's own team calls a handful of transient errors every few hours 'not unexpected' on a healthy database — so read-only statements are retried automatically; writes are not, because every one of those errors is ambiguous about whether the statement applied and D1 has no interactive transactions to resolve it"},websocketHibernation:{level:"native",note:"DO WebSocket hibernation"},durableStreams:{level:"emulated",note:"Lunora persists each chunk to the shard's SQLite under a monotonic seq and keeps the producer alive past the socket via waitUntil; the platform has no streaming primitive of its own, and a run whose DO is evicted mid-flight ends as STREAM_INTERRUPTED rather than resuming"},commitOrderedTables:{level:"native",note:"`state.storage.transaction` makes the `__commit_seq` bump atomic with the rows it stamps, and a Durable Object executes one event at a time — so the allocation order IS the commit order, with no lock of ours in the path"},localSql:{level:"native",note:"state.storage.sql (SQLite)"},serverReactors:{level:"emulated",note:"The wake-up is Lunora's, not the platform's: reactors ride the existing post-write refresh drain, which already exists to push subscription frames. Cloudflare supplies the two properties that make it correct — one event at a time per Durable Object, and `waitUntil` to keep the drain alive past the response — but has no notion of a server-side subscription of its own"},memoryTables:{level:"emulated",note:"The lifetime is real — an eviction drops the DO's heap and the framework clears every `.memory()` table on reconstruction, so the rows behave exactly like heap state, and their writes stay out of the CDC changelog. The STORAGE is not: workerd exposes one SQL handle and no memory-backed database, so a memory row is still written to the DO's SQLite and then deleted. `.memory()` buys the semantics, not the write"},shardAlarms:{level:"native",note:"state.storage.setAlarm"},shardPlacement:{level:"native",note:"DurableObjectNamespace.get/getByName locationHint — best-effort, and honoured only by the resolution that creates the object"},shardReadReplicas:{level:"emulated",note:"Lunora follows the shard's CDC changelog into a replica DO placed in the reader's region; the platform replicates for durability, not for reads, so the follow loop is ours"},crossShardFanout:{level:"emulated",note:"Lunora query coordinator + relay tier over Durable Objects"},queues:{level:"native",note:"Cloudflare Queues"},workflows:{level:"native",note:"Cloudflare Workflows"},scheduler:{level:"emulated",note:"SchedulerDO (Lunora, on DO alarms) + declarative Cron Triggers; no runtime cron registration"},objectStorage:{level:"native",note:"R2"},objectStorageBackups:{level:"emulated",note:"`lunora backup create|list|restore --bucket` writes NDJSON snapshots + a manifest sidecar per snapshot through the admin storage routes (checksum-verified upload, admin-gated object read), and `backupCron`/`backupStore` runs the same layout unattended on a Cron Trigger. Both are bounded by what a single request body / a Worker isolate can hold, not by R2. `emulated` because every part of that is Lunora's — R2 supplies a bucket, and Cloudflare has no backup product being consumed here; the snapshot format, the manifest, the checksum gate and the retention report are all ours"},objectStorageCdcArchive:{level:"emulated",note:"R2 supplies the bucket and the `startAfter` listing the segment keys are indexed on; everything above that is Lunora's — the segment format, the archive-before-trim ordering the sweep defers behind `waitUntil`, and the de-overlapping read-back. The platform has no notion of a changelog to tier, so this is not a product being consumed"},keyValueStore:{level:"native",note:"Workers KV"},vectorStore:{level:"native",note:"Vectorize; query/upsert namespace scoping is native (remote filter), but getByIds/deleteByIds id-path tenant isolation is facade-enforced (client-side verification) since Vectorize's id operations take no namespace option"},ai:{level:"native",note:"Workers AI"},browser:{level:"native",note:"Browser Rendering"},images:{level:"native",note:"Cloudflare Images binding"},containers:{level:"native",note:"Cloudflare Containers; ctx.containers.<name>.exec rides the same binding over the /__lunora/exec contract, which the container image serves"},analytics:{level:"native",note:"Analytics Engine"},pipelines:{level:"native",note:"Cloudflare Pipelines"},mail:{level:"emulated",note:"Resend (third-party) via Cloudflare Queues"},secrets:{level:"native",note:"Secrets Store"},hyperdrive:{level:"native",note:"Cloudflare Hyperdrive"},httpCache:{level:"native",note:"The colo cache via caches.default. Worker-generated responses are NOT stored by it automatically — the runtime has to caches.default.put() them — and it honours Vary for Accept-Encoding only, so a varying response has to fold those header values into the cache key itself. A 206, a Vary: *, or a Set-Cookie-bearing response is refused by put()"},identityProxy:{level:"native",note:"Cloudflare Access. A policy attached to the Worker covers its custom domains, routes, workers.dev and preview URLs at once, and the authenticated identity arrives on the execution context as ctx.access — no header to verify, and nothing a request can forge to manufacture one. A hostname-scoped Access application instead stamps the Cf-Access-Jwt-Assertion header, which needs no host support at all"}}},t={id:"node",name:"Node",features:{shardedState:{level:"emulated",note:"One better-sqlite3 database per shard key, one process — no distributed placement or failover. Shard keys are percent-encoded into basenames with A-Z escaped, so `Tenant` and `tenant` stay two databases on a case-insensitive volume (APFS, NTFS) rather than folding into one"},globalTables:{level:"emulated",note:"The @lunora/sql-store core on its own SQLite file via the reference sqliteDialect — full store semantics, but one node with no replication"},websocketHibernation:{level:"emulated",note:"Socket registry with attachments/tags persisted to SQLite, so subscription state survives a process restart; nothing is ever actually evicted from memory, so this is durability without hibernation's memory saving"},durableStreams:{level:"unsupported",note:"The transcript store is host-neutral (@lunora/shard-engine), but the attach/produce state machine lives in @lunora/do and nothing in this host mounts it. Gate-bearing: codegen refuses an app that declares a durable stream on this target, rather than emitting one that silently behaves as an ephemeral stream"},commitOrderedTables:{level:"emulated",note:"The sequence orders commits correctly, but the serialization it depends on is Lunora's per-shard write gate rather than a platform property — one process, one better-sqlite3 handle per shard key. Correct here; not something the host guarantees the way a Durable Object does"},localSql:{level:"native",note:"better-sqlite3 (synchronous, embedded)"},serverReactors:{level:"emulated",note:"Same engine-level implementation as Cloudflare; the per-shard serialization it depends on is the host's own write gate rather than a platform guarantee"},memoryTables:{level:"emulated",note:"Same shape as Cloudflare and for a different reason: better-sqlite3 CAN open `:memory:`, but a shard's memory tables share the one handle its durable tables use, so they are cleared rather than never written. A host process also outlives far more than a Durable Object does, so cold starts — and therefore `onShardInit` — are much rarer here than in production on Cloudflare; do not use this target to judge how often a memory table is actually empty"},shardAlarms:{level:"emulated",note:"setTimeout over a durable row, dispatched to onAlarm and re-armed on construction, so an alarm survives a restart and one whose time elapsed while the process was down fires late rather than never"},shardPlacement:{level:"unsupported",note:"One process — every shard lives where the process does, so a location hint has nowhere to place it"},shardReadReplicas:{level:"unsupported",note:"One process and one region: a replica here would be a second copy of a database already on the same disk"},crossShardFanout:{level:"emulated",note:"@lunora/runtime's query coordinator over the in-process shard registry; listShardKeys is seeded from the shard files on disk, and answers every shard rather than only those holding the table (a correct superset, at the cost of visiting shards with nothing to say)"},queues:{level:"emulated",note:`createNodeQueueHost (@lunora/platform-node) — a QueueBindingLike producer per declared queue over a durable _lunora_queue_messages table, and a batched consumer feeding the same dispatchQueueBatch the Cloudflare host uses. delaySeconds (capped at 12h), all four content types, maxBatchSize/maxBatchTimeout assembly, per-message ack/retry with workerd's implicit-ack-on-return and retry-on-throw, maxRetries into a declared deadLetterQueue (or parked in place, never dropped), and a visibility window so a crash mid-handler redelivers. Delivery is driven by poll(); there is no timer, because this host has no dev server to own one. mode: "pull" queues are written but not consumed — nothing here serves the HTTP pull endpoint`},workflows:{level:"emulated",note:"createNodeWorkflowHost (@lunora/platform-node) compiles defineWorkflow handlers onto the @visulima/workflow engine (createRuntime): step/sleep/waitForEvent are durable + replay-safe, status maps to complete/errored/waiting/terminated, create({ id }) is honoured through a durable alias row (so ctx.spawn resolves and a retried create is one run), and runs survive a restart when backed by createNodeWorkflowStore (a SQLite WorkflowStore; the store is required, so no caller silently gets in-process-only state). Gaps: no pause/restart; terminate is not a barrier, so an activation already in flight overwrites the tombstone; ctx.run dispatches to an endpoint no Node HTTP server serves; ctx.parallel's synchronous join cannot interleave within one trigger activation"},scheduler:{level:"emulated",note:"SQLite job table dispatched to onDispatch and re-armed on construction, with retry backoff and a dead-letter queue. It is also the only host implementing runtime cron registration (SchedulerHost.cron), which Cloudflare cannot offer — but nothing dispatches into it: no runtime walks the generated LUNORA_CRONS map into SchedulerHost.cron, so the conformance suite is its only caller and a declared cron does not fire on this host"},objectStorageBackups:{level:"emulated",note:"The commands work unchanged, but the bucket underneath is createNodeR2Bucket — a directory on the same machine the CLI runs on, so a bucket-backed backup here is not the separate failure domain it is on Cloudflare. The scheduled half additionally needs this host's scheduler, which exists but is not a shipping target"},objectStorageCdcArchive:{level:"emulated",note:"createNodeR2Bucket implements the `startAfter` seek the segment index needs, so the read-back behaves as it does on R2. Same caveat as the backups above: the bucket is a directory on the machine running the host, so archiving the changelog here moves it off SQLite but not off the disk that would take the shard with it"},objectStorage:{level:"emulated",note:"createNodeR2Bucket (@lunora/platform-node) — an R2BucketLike over the local filesystem (fs/promises, head/list/range). One file per object with the metadata in a trailer, so the single rename that publishes the bytes publishes their checksum and content-type with them, and a get reads body and metadata through one handle rather than reopening the path. put streams into the staged file and .body streams the requested range; .arrayBuffer()/.text() still allocate the range they return. The body is single-use, as R2's is. Keys fold the way the host filesystem folds them, so `A` and `a` are one object on a case-insensitive volume where real R2 keeps two. No multipart uploads, no presigned URLs"},keyValueStore:{level:"emulated",note:"better-sqlite3 table behind the ShardKvStore API — not a dedicated KV product"},vectorStore:{level:"unsupported",note:"No Vectorize-equivalent binding implemented"},ai:{level:"unsupported",note:"No Workers AI-equivalent binding implemented"},browser:{level:"unsupported",note:"No headless-browser binding implemented"},images:{level:"unsupported",note:"No Images-equivalent binding implemented"},containers:{level:"unsupported",note:"No container orchestration implemented, so there is nothing for ctx.containers.<name>.exec to run a command in either"},analytics:{level:"unsupported",note:"No Analytics Engine-equivalent binding implemented"},pipelines:{level:"unsupported",note:"No Pipelines-equivalent binding implemented"},mail:{level:"unsupported",note:"The queue tier this host lacked when the rating was written now exists (createNodeQueueHost), but nothing here composes a @lunora/mail transport or the queued-send consumer, so a send would be accepted and never delivered"},secrets:{level:"unsupported",note:"No Secrets Store-equivalent binding implemented (a real host would likely map this to env vars). Gate-bearing, and it has to be: ctx.secrets is a core built-in spliced into every context, so codegen refuses an app that reads it on this target instead of emitting a surface that throws on first use"},hyperdrive:{level:"unsupported",note:"No connection-pooling binding implemented"},httpCache:{level:"unsupported",note:"Nothing sits in front of this host to cache its responses, and Node exposes no Web Cache API global — the runtime's REST edge cache finds no HttpCacheLike here and degrades to emitting Cache-Control alone, which browsers and any CDN in front still honour"},identityProxy:{level:"unsupported",note:"Nothing sits in front of this host to authenticate callers, so it never populates the execution context's access identity. @lunora/cloudflare-access still works here through its Cf-Access-Jwt-Assertion fallback, which is a plain header check and needs no host support"}}};export{e as CLOUDFLARE_CAPABILITIES,t as NODE_CAPABILITIES};
|
|
@@ -141,6 +141,16 @@ interface SchedulerHost {
|
|
|
141
141
|
* presence is the host's declaration that dynamic cron works, exactly as
|
|
142
142
|
* with `SocketHost.setTag`. A caller that finds it absent must fall
|
|
143
143
|
* back to the target's declarative configuration.
|
|
144
|
+
*
|
|
145
|
+
* **Nothing in the framework calls this today.** `@lunora/platform-node`
|
|
146
|
+
* implements it (parser, persistence, timer re-arm) and the conformance
|
|
147
|
+
* suite is its only caller: no runtime walks an app's declared crons — the
|
|
148
|
+
* generated `LUNORA_CRONS` map — into it. The only cron dispatch that ships
|
|
149
|
+
* is `@lunora/runtime`'s, reached from Cloudflare's `scheduled()` handler,
|
|
150
|
+
* which is declarative and never touches this method. So a host that
|
|
151
|
+
* implements `cron` gains nothing until that wiring exists, and an app
|
|
152
|
+
* declaring crons on such a host has none of them fire. Wire it before
|
|
153
|
+
* treating a `cron`-bearing host as one that runs declared schedules.
|
|
144
154
|
*/
|
|
145
155
|
cron?: (cron: string, functionPath: string, args?: Record<string, unknown>) => Promise<void>;
|
|
146
156
|
/**
|
|
@@ -577,9 +587,26 @@ interface SocketHost {
|
|
|
577
587
|
* durable tag minted at accept (Cloudflare), a registry key, a `WeakMap`.
|
|
578
588
|
*
|
|
579
589
|
* Must answer consistently for the same socket within a wake AND across a
|
|
580
|
-
* recycle
|
|
581
|
-
*
|
|
582
|
-
*
|
|
590
|
+
* recycle. Callers outside the O(subscribers) loops are the intended
|
|
591
|
+
* consumers; do not reach for this per socket per frame.
|
|
592
|
+
*
|
|
593
|
+
* **Who actually calls it, since the previous wording named the wrong
|
|
594
|
+
* caller.** It said "the engine uses it to reassociate a rehydrated socket
|
|
595
|
+
* with its subscription state"; the engine does no such thing. Per-socket
|
|
596
|
+
* state is keyed on the handle object itself (`ShardRunner.socketFor`, over
|
|
597
|
+
* {@link SocketHost.handleFor}), and durable connection identity is the
|
|
598
|
+
* engine's own `connectionId` — a UUID minted at upgrade and stamped onto the
|
|
599
|
+
* socket attachment, which is what survives hibernation. `@lunora/shard-engine`
|
|
600
|
+
* and `@lunora/do` dispatch to `handleFor`, `getSockets` and `accept`, and to
|
|
601
|
+
* nothing else on this interface.
|
|
602
|
+
*
|
|
603
|
+
* `idFor` is the portable **identity oracle** instead: the conformance suite
|
|
604
|
+
* compares sockets through it (a host is allowed to hand back a different
|
|
605
|
+
* wrapper object for the same socket, so object identity is not a legal
|
|
606
|
+
* assertion), and it is how a host's own recycle plumbing addresses a socket
|
|
607
|
+
* by id. That is a real job and it is why this stays required — every host
|
|
608
|
+
* has to answer it for the TCK regardless — but implement it as a test and
|
|
609
|
+
* tooling contract, not as something on the frame path.
|
|
583
610
|
*
|
|
584
611
|
* **A socket this host never {@link SocketHost.accept}ed** (a foreign socket
|
|
585
612
|
* the runtime hands back — a whisper sender in another pool, a relay peer)
|
|
@@ -141,6 +141,16 @@ interface SchedulerHost {
|
|
|
141
141
|
* presence is the host's declaration that dynamic cron works, exactly as
|
|
142
142
|
* with `SocketHost.setTag`. A caller that finds it absent must fall
|
|
143
143
|
* back to the target's declarative configuration.
|
|
144
|
+
*
|
|
145
|
+
* **Nothing in the framework calls this today.** `@lunora/platform-node`
|
|
146
|
+
* implements it (parser, persistence, timer re-arm) and the conformance
|
|
147
|
+
* suite is its only caller: no runtime walks an app's declared crons — the
|
|
148
|
+
* generated `LUNORA_CRONS` map — into it. The only cron dispatch that ships
|
|
149
|
+
* is `@lunora/runtime`'s, reached from Cloudflare's `scheduled()` handler,
|
|
150
|
+
* which is declarative and never touches this method. So a host that
|
|
151
|
+
* implements `cron` gains nothing until that wiring exists, and an app
|
|
152
|
+
* declaring crons on such a host has none of them fire. Wire it before
|
|
153
|
+
* treating a `cron`-bearing host as one that runs declared schedules.
|
|
144
154
|
*/
|
|
145
155
|
cron?: (cron: string, functionPath: string, args?: Record<string, unknown>) => Promise<void>;
|
|
146
156
|
/**
|
|
@@ -577,9 +587,26 @@ interface SocketHost {
|
|
|
577
587
|
* durable tag minted at accept (Cloudflare), a registry key, a `WeakMap`.
|
|
578
588
|
*
|
|
579
589
|
* Must answer consistently for the same socket within a wake AND across a
|
|
580
|
-
* recycle
|
|
581
|
-
*
|
|
582
|
-
*
|
|
590
|
+
* recycle. Callers outside the O(subscribers) loops are the intended
|
|
591
|
+
* consumers; do not reach for this per socket per frame.
|
|
592
|
+
*
|
|
593
|
+
* **Who actually calls it, since the previous wording named the wrong
|
|
594
|
+
* caller.** It said "the engine uses it to reassociate a rehydrated socket
|
|
595
|
+
* with its subscription state"; the engine does no such thing. Per-socket
|
|
596
|
+
* state is keyed on the handle object itself (`ShardRunner.socketFor`, over
|
|
597
|
+
* {@link SocketHost.handleFor}), and durable connection identity is the
|
|
598
|
+
* engine's own `connectionId` — a UUID minted at upgrade and stamped onto the
|
|
599
|
+
* socket attachment, which is what survives hibernation. `@lunora/shard-engine`
|
|
600
|
+
* and `@lunora/do` dispatch to `handleFor`, `getSockets` and `accept`, and to
|
|
601
|
+
* nothing else on this interface.
|
|
602
|
+
*
|
|
603
|
+
* `idFor` is the portable **identity oracle** instead: the conformance suite
|
|
604
|
+
* compares sockets through it (a host is allowed to hand back a different
|
|
605
|
+
* wrapper object for the same socket, so object identity is not a legal
|
|
606
|
+
* assertion), and it is how a host's own recycle plumbing addresses a socket
|
|
607
|
+
* by id. That is a real job and it is why this stays required — every host
|
|
608
|
+
* has to answer it for the TCK regardless — but implement it as a test and
|
|
609
|
+
* tooling contract, not as something on the frame path.
|
|
583
610
|
*
|
|
584
611
|
* **A socket this host never {@link SocketHost.accept}ed** (a foreign socket
|
|
585
612
|
* the runtime hands back — a whisper sender in another pool, a relay peer)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/platform",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.23",
|
|
4
4
|
"description": "Provider-neutral host contracts for Lunora: shard/socket/directory/scheduler interfaces, binding projections, and the platform capability matrix",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|