@lunora/runtime 1.0.0-alpha.30 → 1.0.0-alpha.32

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 CHANGED
@@ -1,3 +1,4 @@
1
+ import { R2SqlClient } from '@lunora/bindings/r2sql';
1
2
  import { RankDirection, RankPageRow, DatabaseWriterLike } from '@lunora/do';
2
3
  export type { RankDirection as RankPageDirection, RankPageRowKey as RankPageKey, RankPageRow, ShardRankPageResult } from '@lunora/do';
3
4
  import { WorkflowsRestClient } from '@lunora/workflow';
@@ -661,6 +662,172 @@ interface LogEvent {
661
662
  /** Acting userId, or absent when anonymous. */
662
663
  userId?: string;
663
664
  }
665
+ /**
666
+ * The written-column contract: every field `pipelineLogSink` emits, mapped to the
667
+ * column it is stored under by default (the identity mapping). Also the source of
668
+ * truth for the {@link PipelineLogField} union. Mirrors the record built in
669
+ * `pipelineLogSink` — the read side of the same contract.
670
+ */
671
+ declare const DEFAULT_COLUMNS: {
672
+ readonly fields: "fields";
673
+ readonly functionPath: "functionPath";
674
+ readonly level: "level";
675
+ readonly message: "message";
676
+ readonly shardKey: "shardKey";
677
+ readonly spanId: "spanId";
678
+ readonly traceId: "traceId";
679
+ readonly ts: "ts";
680
+ readonly userId: "userId";
681
+ };
682
+ /**
683
+ * The canonical field names of one persisted log record — the keys
684
+ * `pipelineLogSink` writes. Used as the {@link PipelineLogColumnMap} keys and the
685
+ * {@link PipelineLogRow} shape, so the reader stays decoupled from whatever
686
+ * physical column names the operator's Iceberg table happens to use.
687
+ */
688
+ type PipelineLogField = keyof typeof DEFAULT_COLUMNS;
689
+ /**
690
+ * Field-to-column-name map. Defaults to the identity mapping (each field stored
691
+ * under its own name, matching what `pipelineLogSink` writes). Override per-field
692
+ * when the Iceberg schema renames a column; unspecified fields keep their
693
+ * default. This is the single knob that lets one reader serve differently shaped
694
+ * Data Catalog tables.
695
+ */
696
+ type PipelineLogColumnMap = Partial<Record<PipelineLogField, string>>;
697
+ /** An opaque keyset cursor: the `ts` of the row after the last one returned. */
698
+ interface PipelineLogCursor {
699
+ /** Epoch-millis boundary; the next page is every row strictly older than this. */
700
+ ts: number;
701
+ }
702
+ /** Filters for one {@link PipelineLogReader} query. Every value is inlined safely (`lit`/`sql`). */
703
+ interface PipelineLogQuery {
704
+ /** Continue after a previous page (keyset on `ts DESC`). Combined with the other filters. */
705
+ cursor?: PipelineLogCursor;
706
+ /** Match only this exact function path's records. Prefer `functionPathPrefix` for a namespace sweep. */
707
+ functionPath?: string;
708
+ /** Match records whose `functionPath` starts with this string (rendered as a `LIKE 'prefix%'`). */
709
+ functionPathPrefix?: string;
710
+ /** Match only this exact severity. When set, `minLevel` is ignored for the same field. */
711
+ level?: ContextLogLevel;
712
+ /** Max rows to return. Clamped to `[1, 10000]`; defaults to {@link DEFAULT_LOG_LIMIT}. */
713
+ limit?: number;
714
+ /** Severity floor: keep every level at or above this in {@link LOG_LEVEL_ORDER} (e.g. `warn` keeps warn, error, fatal). */
715
+ minLevel?: ContextLogLevel;
716
+ /** Match only this shard key. */
717
+ shardKey?: string;
718
+ /** Lower time bound, inclusive (`ts` at or after this), epoch-millis. */
719
+ sinceTs?: number;
720
+ /** Match only this trace id. */
721
+ traceId?: string;
722
+ /** Upper time bound, inclusive (`ts` at or before this), epoch-millis. */
723
+ untilTs?: number;
724
+ /** Match only this acting user id. */
725
+ userId?: string;
726
+ }
727
+ /**
728
+ * One decoded log record. Always keyed by the canonical {@link PipelineLogField}
729
+ * names regardless of the physical columns (the reader remaps via `columnMap`),
730
+ * so consumers never see the operator's storage names.
731
+ */
732
+ interface PipelineLogRow {
733
+ /**
734
+ * Structured fields, when the record carried them. A `serializeFields` sink
735
+ * stores these as a JSON string, which the reader parses back to an object;
736
+ * a plain string that is not valid JSON is returned verbatim.
737
+ */
738
+ fields?: unknown;
739
+ /** Function path that emitted the line, e.g. `"messages:list"`. */
740
+ functionPath: string;
741
+ /** Severity the line was logged at. */
742
+ level: ContextLogLevel;
743
+ /** Rendered message. */
744
+ message: string;
745
+ /** Shard key for single-shard calls, when present. */
746
+ shardKey?: string;
747
+ /** Span id the line was emitted under, when present. */
748
+ spanId?: string;
749
+ /** Trace id the line belongs to, when present. */
750
+ traceId?: string;
751
+ /** Epoch-millis the line was emitted. */
752
+ ts: number;
753
+ /** Acting user id, when present. */
754
+ userId?: string;
755
+ }
756
+ /** One page of {@link PipelineLogRow}s, newest first, plus the cursor for the next page (absent means last page). */
757
+ interface PipelineLogPage {
758
+ /** The cursor to pass as {@link PipelineLogQuery} `cursor` for the following page; absent when this is the last page. */
759
+ nextCursor?: PipelineLogCursor;
760
+ /** The rows, ordered `ts DESC` (newest first). At most `limit` of them. */
761
+ rows: PipelineLogRow[];
762
+ }
763
+ /** Options for {@link createPipelineLogReader}. */
764
+ interface PipelineLogReaderOptions {
765
+ /**
766
+ * Override any physical column name that diverges from the default (identity)
767
+ * mapping. Unspecified fields keep their {@link DEFAULT_LOG_COLUMNS} name.
768
+ */
769
+ columnMap?: PipelineLogColumnMap;
770
+ /**
771
+ * The Iceberg namespace the `table` lives in (R2 Data Catalog database).
772
+ * Combined as `namespace.table` in the `FROM` clause; omit when `table`
773
+ * already carries its namespace.
774
+ */
775
+ namespace?: string;
776
+ /** The Iceberg table name the Pipeline writes log records to (e.g. `"logs"`). */
777
+ table: string;
778
+ }
779
+ /** The reader surface: a single keyset-paginated {@link PipelineLogPage} query. */
780
+ interface PipelineLogReader {
781
+ /** Run one filtered, keyset-paginated read and return a {@link PipelineLogPage}. */
782
+ query: (query?: PipelineLogQuery) => Promise<PipelineLogPage>;
783
+ }
784
+ /** The written-column contract exposed publicly: canonical field to default physical column name. */
785
+ declare const DEFAULT_LOG_COLUMNS: Readonly<Record<PipelineLogField, string>>;
786
+ /** Default page size when a query omits `limit`. */
787
+ declare const DEFAULT_LOG_LIMIT: number;
788
+ /**
789
+ * Build a durable-log reader over one R2 Data Catalog (Iceberg) table.
790
+ *
791
+ * The returned {@link PipelineLogReader} compiles each call to a safe
792
+ * `SELECT ... WHERE ... ORDER BY ts DESC LIMIT n` (over-fetching by one) and
793
+ * decodes the rows back to the canonical {@link PipelineLogRow} shape. All value
794
+ * filters are escaped through `@lunora/bindings/r2sql`'s `sql`/`lit`; column
795
+ * names come from `options.columnMap` (operator config), spliced with `raw`.
796
+ * @param client An {@link R2SqlClient} (`createR2Sql({ accountId, apiToken, bucket })`).
797
+ * @param options The target `table` (plus optional `namespace`) and any `columnMap` overrides.
798
+ */
799
+ declare const createPipelineLogReader: (client: R2SqlClient, options: PipelineLogReaderOptions) => PipelineLogReader;
800
+ /**
801
+ * Wire constants for the durable log archive, shared between the server route
802
+ * (`@lunora/runtime`'s `log-archive-admin-routes`) and the Studio Archive feed
803
+ * (`@lunora/studio`). Kept here — not in `@lunora/runtime` — because the studio
804
+ * is a browser bundle that must not import a runtime *value* (which would drag
805
+ * the DO/R2-SQL runtime into the browser). This file is dependency-free and
806
+ * bundler-inlined into each consumer, so both sides share one source of truth
807
+ * with no dependency edge.
808
+ */
809
+ /**
810
+ * The error `code` the archive route returns (400) when the operator has wired
811
+ * no archive table (`logArchive`) or the `R2_SQL_*` credentials are missing. The
812
+ * Studio keys its "not configured" empty state off this exact value.
813
+ */
814
+ declare const LOG_ARCHIVE_NOT_CONFIGURED = "LOG_ARCHIVE_NOT_CONFIGURED";
815
+ /** The route the studio's `queryLogArchive` client method POSTs to. */
816
+ declare const LOG_ARCHIVE_PATH = "/_lunora/admin/logs/archive";
817
+ /**
818
+ * The app-level archive config the worker passes through: which Data Catalog
819
+ * table the Pipeline writes log records to (the read-side of `pipelineLogSink`),
820
+ * plus optional namespace / physical-column overrides. The R2 SQL *credentials*
821
+ * are NOT here — they live on `env` (`R2_SQL_*`), read per request.
822
+ */
823
+ interface LogArchiveConfig {
824
+ /** Override any physical column name that diverges from the {@link createPipelineLogReader} default mapping. */
825
+ columnMap?: PipelineLogColumnMap;
826
+ /** The Iceberg namespace (R2 Data Catalog database) the table lives in; omit when `table` already carries it. */
827
+ namespace?: string;
828
+ /** The Iceberg table the Pipeline writes log records to (e.g. `"logs"`). */
829
+ table: string;
830
+ }
664
831
  /**
665
832
  * What kind of instrument produced a measurement, which decides how a collector
666
833
  * aggregates it:
@@ -2274,6 +2441,15 @@ interface WorkerOptions {
2274
2441
  * Omit it and those endpoints respond `KV_NOT_CONFIGURED`.
2275
2442
  */
2276
2443
  kvIntrospector?: KvIntrospector;
2444
+ /**
2445
+ * The durable log archive's read config — the R2 Data Catalog (Iceberg)
2446
+ * table `pipelineLogSink` writes to, so the studio Logs panel's Archive feed
2447
+ * (and the `/_lunora/admin/logs/archive` route) can read it back via R2 SQL.
2448
+ * The R2 SQL credentials come from `env` (`R2_SQL_ACCOUNT_ID` / `R2_SQL_TOKEN`
2449
+ * / `R2_SQL_BUCKET`); this only names the table (+ optional namespace / column
2450
+ * overrides). Absent → the Archive feed reports "not configured".
2451
+ */
2452
+ logArchive?: LogArchiveConfig;
2277
2453
  /**
2278
2454
  * Optional telemetry sink. When supplied, the worker emits one
2279
2455
  * `onRpc` event per dispatched RPC (single-shard forward or fan-out)
@@ -2924,6 +3100,17 @@ interface PipelineLike {
2924
3100
  interface PipelineLogSinkOptions {
2925
3101
  /** The Cloudflare Pipeline binding each log record is durably sent to. */
2926
3102
  pipeline: PipelineLike;
3103
+ /**
3104
+ * When true, `fields` is written as a **JSON string** (`JSON.stringify`)
3105
+ * rather than a nested object. Defaults to `false` for back-compatibility.
3106
+ *
3107
+ * Turn it on when the destination Iceberg table types `fields` as a `string`
3108
+ * column so the archive stays queryable (R2 SQL can `LIKE`/compare a string
3109
+ * column, but not index into an arbitrarily-shaped struct). The reader
3110
+ * (`createPipelineLogReader`) parses such a JSON string back to an object on
3111
+ * read. Leave it off when the table types `fields` as a native struct.
3112
+ */
3113
+ serializeFields?: boolean;
2927
3114
  }
2928
3115
  /**
2929
3116
  * A sink that durably persists each `ctx.log` line to a Cloudflare Pipeline
@@ -2933,6 +3120,20 @@ interface PipelineLogSinkOptions {
2933
3120
  * structured record (message, level, function path, fields, trace ids, shard,
2934
3121
  * user, timestamp) in object storage under the app's own account.
2935
3122
  *
3123
+ * **Written-column contract.** Each record is a flat object; this is the exact
3124
+ * read-side schema `createPipelineLogReader` (`pipeline-log-reader.ts`) mirrors
3125
+ * in its `DEFAULT_LOG_COLUMNS`. Keep the two in lockstep — a column added here
3126
+ * must gain a default there:
3127
+ * - `functionPath` (string) — always present
3128
+ * - `level` (string severity) — always present
3129
+ * - `message` (string) — always present
3130
+ * - `ts` (number, epoch-millis) — always present
3131
+ * - `fields` (nested object, or a JSON string when `serializeFields`) — when set
3132
+ * - `shardKey` (string) — when set
3133
+ * - `userId` (string) — when set
3134
+ * - `traceId` (string) — when set
3135
+ * - `spanId` (string) — when set
3136
+ *
2936
3137
  * Only `onLog` is implemented — RPC-span metrics belong in
2937
3138
  * {@link analyticsEngineSink}. `Pipeline.send` is durable/fire-and-forget on the
2938
3139
  * platform; the call is registered with the request's `context.waitUntil` when
@@ -2943,7 +3144,8 @@ interface PipelineLogSinkOptions {
2943
3144
  * Privacy: the persisted record carries `message` + structured `fields` (not the
2944
3145
  * raw positional args). They may include user input — the R2 bucket is your own,
2945
3146
  * but treat it as a log store and gate PII upstream if that is a concern.
2946
- * @param options Sink options: `pipeline` is the Cloudflare Pipeline binding.
3147
+ * @param options Sink options: `pipeline` is the Cloudflare Pipeline binding;
3148
+ * `serializeFields` stores `fields` as a queryable JSON string.
2947
3149
  */
2948
3150
  declare const pipelineLogSink: (options: PipelineLogSinkOptions) => ObservabilitySink;
2949
3151
  /** Options for {@link otlpSink}. */
@@ -3011,4 +3213,4 @@ declare const otlpSink: (options: OtlpSinkOptions) => ObservabilitySink;
3011
3213
  */
3012
3214
  declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
3013
3215
  declare const VERSION: string;
3014
- export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, type ListAuthUsersOptions, type LogEvent, type LogFields, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MetricEvent, type MetricKind, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type PipelineLike, type PipelineLogSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type SpanEvent, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, pipelineLogSink, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
3216
+ export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_LOG_COLUMNS, DEFAULT_LOG_LIMIT, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, LOG_ARCHIVE_NOT_CONFIGURED, LOG_ARCHIVE_PATH, type ListAuthUsersOptions, type LogArchiveConfig, type LogEvent, type LogFields, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MetricEvent, type MetricKind, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type PipelineLike, type PipelineLogColumnMap, type PipelineLogCursor, type PipelineLogField, type PipelineLogPage, type PipelineLogQuery, type PipelineLogReader, type PipelineLogReaderOptions, type PipelineLogRow, type PipelineLogSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type SpanEvent, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createPipelineLogReader, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, pipelineLogSink, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { R2SqlClient } from '@lunora/bindings/r2sql';
1
2
  import { RankDirection, RankPageRow, DatabaseWriterLike } from '@lunora/do';
2
3
  export type { RankDirection as RankPageDirection, RankPageRowKey as RankPageKey, RankPageRow, ShardRankPageResult } from '@lunora/do';
3
4
  import { WorkflowsRestClient } from '@lunora/workflow';
@@ -661,6 +662,172 @@ interface LogEvent {
661
662
  /** Acting userId, or absent when anonymous. */
662
663
  userId?: string;
663
664
  }
665
+ /**
666
+ * The written-column contract: every field `pipelineLogSink` emits, mapped to the
667
+ * column it is stored under by default (the identity mapping). Also the source of
668
+ * truth for the {@link PipelineLogField} union. Mirrors the record built in
669
+ * `pipelineLogSink` — the read side of the same contract.
670
+ */
671
+ declare const DEFAULT_COLUMNS: {
672
+ readonly fields: "fields";
673
+ readonly functionPath: "functionPath";
674
+ readonly level: "level";
675
+ readonly message: "message";
676
+ readonly shardKey: "shardKey";
677
+ readonly spanId: "spanId";
678
+ readonly traceId: "traceId";
679
+ readonly ts: "ts";
680
+ readonly userId: "userId";
681
+ };
682
+ /**
683
+ * The canonical field names of one persisted log record — the keys
684
+ * `pipelineLogSink` writes. Used as the {@link PipelineLogColumnMap} keys and the
685
+ * {@link PipelineLogRow} shape, so the reader stays decoupled from whatever
686
+ * physical column names the operator's Iceberg table happens to use.
687
+ */
688
+ type PipelineLogField = keyof typeof DEFAULT_COLUMNS;
689
+ /**
690
+ * Field-to-column-name map. Defaults to the identity mapping (each field stored
691
+ * under its own name, matching what `pipelineLogSink` writes). Override per-field
692
+ * when the Iceberg schema renames a column; unspecified fields keep their
693
+ * default. This is the single knob that lets one reader serve differently shaped
694
+ * Data Catalog tables.
695
+ */
696
+ type PipelineLogColumnMap = Partial<Record<PipelineLogField, string>>;
697
+ /** An opaque keyset cursor: the `ts` of the row after the last one returned. */
698
+ interface PipelineLogCursor {
699
+ /** Epoch-millis boundary; the next page is every row strictly older than this. */
700
+ ts: number;
701
+ }
702
+ /** Filters for one {@link PipelineLogReader} query. Every value is inlined safely (`lit`/`sql`). */
703
+ interface PipelineLogQuery {
704
+ /** Continue after a previous page (keyset on `ts DESC`). Combined with the other filters. */
705
+ cursor?: PipelineLogCursor;
706
+ /** Match only this exact function path's records. Prefer `functionPathPrefix` for a namespace sweep. */
707
+ functionPath?: string;
708
+ /** Match records whose `functionPath` starts with this string (rendered as a `LIKE 'prefix%'`). */
709
+ functionPathPrefix?: string;
710
+ /** Match only this exact severity. When set, `minLevel` is ignored for the same field. */
711
+ level?: ContextLogLevel;
712
+ /** Max rows to return. Clamped to `[1, 10000]`; defaults to {@link DEFAULT_LOG_LIMIT}. */
713
+ limit?: number;
714
+ /** Severity floor: keep every level at or above this in {@link LOG_LEVEL_ORDER} (e.g. `warn` keeps warn, error, fatal). */
715
+ minLevel?: ContextLogLevel;
716
+ /** Match only this shard key. */
717
+ shardKey?: string;
718
+ /** Lower time bound, inclusive (`ts` at or after this), epoch-millis. */
719
+ sinceTs?: number;
720
+ /** Match only this trace id. */
721
+ traceId?: string;
722
+ /** Upper time bound, inclusive (`ts` at or before this), epoch-millis. */
723
+ untilTs?: number;
724
+ /** Match only this acting user id. */
725
+ userId?: string;
726
+ }
727
+ /**
728
+ * One decoded log record. Always keyed by the canonical {@link PipelineLogField}
729
+ * names regardless of the physical columns (the reader remaps via `columnMap`),
730
+ * so consumers never see the operator's storage names.
731
+ */
732
+ interface PipelineLogRow {
733
+ /**
734
+ * Structured fields, when the record carried them. A `serializeFields` sink
735
+ * stores these as a JSON string, which the reader parses back to an object;
736
+ * a plain string that is not valid JSON is returned verbatim.
737
+ */
738
+ fields?: unknown;
739
+ /** Function path that emitted the line, e.g. `"messages:list"`. */
740
+ functionPath: string;
741
+ /** Severity the line was logged at. */
742
+ level: ContextLogLevel;
743
+ /** Rendered message. */
744
+ message: string;
745
+ /** Shard key for single-shard calls, when present. */
746
+ shardKey?: string;
747
+ /** Span id the line was emitted under, when present. */
748
+ spanId?: string;
749
+ /** Trace id the line belongs to, when present. */
750
+ traceId?: string;
751
+ /** Epoch-millis the line was emitted. */
752
+ ts: number;
753
+ /** Acting user id, when present. */
754
+ userId?: string;
755
+ }
756
+ /** One page of {@link PipelineLogRow}s, newest first, plus the cursor for the next page (absent means last page). */
757
+ interface PipelineLogPage {
758
+ /** The cursor to pass as {@link PipelineLogQuery} `cursor` for the following page; absent when this is the last page. */
759
+ nextCursor?: PipelineLogCursor;
760
+ /** The rows, ordered `ts DESC` (newest first). At most `limit` of them. */
761
+ rows: PipelineLogRow[];
762
+ }
763
+ /** Options for {@link createPipelineLogReader}. */
764
+ interface PipelineLogReaderOptions {
765
+ /**
766
+ * Override any physical column name that diverges from the default (identity)
767
+ * mapping. Unspecified fields keep their {@link DEFAULT_LOG_COLUMNS} name.
768
+ */
769
+ columnMap?: PipelineLogColumnMap;
770
+ /**
771
+ * The Iceberg namespace the `table` lives in (R2 Data Catalog database).
772
+ * Combined as `namespace.table` in the `FROM` clause; omit when `table`
773
+ * already carries its namespace.
774
+ */
775
+ namespace?: string;
776
+ /** The Iceberg table name the Pipeline writes log records to (e.g. `"logs"`). */
777
+ table: string;
778
+ }
779
+ /** The reader surface: a single keyset-paginated {@link PipelineLogPage} query. */
780
+ interface PipelineLogReader {
781
+ /** Run one filtered, keyset-paginated read and return a {@link PipelineLogPage}. */
782
+ query: (query?: PipelineLogQuery) => Promise<PipelineLogPage>;
783
+ }
784
+ /** The written-column contract exposed publicly: canonical field to default physical column name. */
785
+ declare const DEFAULT_LOG_COLUMNS: Readonly<Record<PipelineLogField, string>>;
786
+ /** Default page size when a query omits `limit`. */
787
+ declare const DEFAULT_LOG_LIMIT: number;
788
+ /**
789
+ * Build a durable-log reader over one R2 Data Catalog (Iceberg) table.
790
+ *
791
+ * The returned {@link PipelineLogReader} compiles each call to a safe
792
+ * `SELECT ... WHERE ... ORDER BY ts DESC LIMIT n` (over-fetching by one) and
793
+ * decodes the rows back to the canonical {@link PipelineLogRow} shape. All value
794
+ * filters are escaped through `@lunora/bindings/r2sql`'s `sql`/`lit`; column
795
+ * names come from `options.columnMap` (operator config), spliced with `raw`.
796
+ * @param client An {@link R2SqlClient} (`createR2Sql({ accountId, apiToken, bucket })`).
797
+ * @param options The target `table` (plus optional `namespace`) and any `columnMap` overrides.
798
+ */
799
+ declare const createPipelineLogReader: (client: R2SqlClient, options: PipelineLogReaderOptions) => PipelineLogReader;
800
+ /**
801
+ * Wire constants for the durable log archive, shared between the server route
802
+ * (`@lunora/runtime`'s `log-archive-admin-routes`) and the Studio Archive feed
803
+ * (`@lunora/studio`). Kept here — not in `@lunora/runtime` — because the studio
804
+ * is a browser bundle that must not import a runtime *value* (which would drag
805
+ * the DO/R2-SQL runtime into the browser). This file is dependency-free and
806
+ * bundler-inlined into each consumer, so both sides share one source of truth
807
+ * with no dependency edge.
808
+ */
809
+ /**
810
+ * The error `code` the archive route returns (400) when the operator has wired
811
+ * no archive table (`logArchive`) or the `R2_SQL_*` credentials are missing. The
812
+ * Studio keys its "not configured" empty state off this exact value.
813
+ */
814
+ declare const LOG_ARCHIVE_NOT_CONFIGURED = "LOG_ARCHIVE_NOT_CONFIGURED";
815
+ /** The route the studio's `queryLogArchive` client method POSTs to. */
816
+ declare const LOG_ARCHIVE_PATH = "/_lunora/admin/logs/archive";
817
+ /**
818
+ * The app-level archive config the worker passes through: which Data Catalog
819
+ * table the Pipeline writes log records to (the read-side of `pipelineLogSink`),
820
+ * plus optional namespace / physical-column overrides. The R2 SQL *credentials*
821
+ * are NOT here — they live on `env` (`R2_SQL_*`), read per request.
822
+ */
823
+ interface LogArchiveConfig {
824
+ /** Override any physical column name that diverges from the {@link createPipelineLogReader} default mapping. */
825
+ columnMap?: PipelineLogColumnMap;
826
+ /** The Iceberg namespace (R2 Data Catalog database) the table lives in; omit when `table` already carries it. */
827
+ namespace?: string;
828
+ /** The Iceberg table the Pipeline writes log records to (e.g. `"logs"`). */
829
+ table: string;
830
+ }
664
831
  /**
665
832
  * What kind of instrument produced a measurement, which decides how a collector
666
833
  * aggregates it:
@@ -2274,6 +2441,15 @@ interface WorkerOptions {
2274
2441
  * Omit it and those endpoints respond `KV_NOT_CONFIGURED`.
2275
2442
  */
2276
2443
  kvIntrospector?: KvIntrospector;
2444
+ /**
2445
+ * The durable log archive's read config — the R2 Data Catalog (Iceberg)
2446
+ * table `pipelineLogSink` writes to, so the studio Logs panel's Archive feed
2447
+ * (and the `/_lunora/admin/logs/archive` route) can read it back via R2 SQL.
2448
+ * The R2 SQL credentials come from `env` (`R2_SQL_ACCOUNT_ID` / `R2_SQL_TOKEN`
2449
+ * / `R2_SQL_BUCKET`); this only names the table (+ optional namespace / column
2450
+ * overrides). Absent → the Archive feed reports "not configured".
2451
+ */
2452
+ logArchive?: LogArchiveConfig;
2277
2453
  /**
2278
2454
  * Optional telemetry sink. When supplied, the worker emits one
2279
2455
  * `onRpc` event per dispatched RPC (single-shard forward or fan-out)
@@ -2924,6 +3100,17 @@ interface PipelineLike {
2924
3100
  interface PipelineLogSinkOptions {
2925
3101
  /** The Cloudflare Pipeline binding each log record is durably sent to. */
2926
3102
  pipeline: PipelineLike;
3103
+ /**
3104
+ * When true, `fields` is written as a **JSON string** (`JSON.stringify`)
3105
+ * rather than a nested object. Defaults to `false` for back-compatibility.
3106
+ *
3107
+ * Turn it on when the destination Iceberg table types `fields` as a `string`
3108
+ * column so the archive stays queryable (R2 SQL can `LIKE`/compare a string
3109
+ * column, but not index into an arbitrarily-shaped struct). The reader
3110
+ * (`createPipelineLogReader`) parses such a JSON string back to an object on
3111
+ * read. Leave it off when the table types `fields` as a native struct.
3112
+ */
3113
+ serializeFields?: boolean;
2927
3114
  }
2928
3115
  /**
2929
3116
  * A sink that durably persists each `ctx.log` line to a Cloudflare Pipeline
@@ -2933,6 +3120,20 @@ interface PipelineLogSinkOptions {
2933
3120
  * structured record (message, level, function path, fields, trace ids, shard,
2934
3121
  * user, timestamp) in object storage under the app's own account.
2935
3122
  *
3123
+ * **Written-column contract.** Each record is a flat object; this is the exact
3124
+ * read-side schema `createPipelineLogReader` (`pipeline-log-reader.ts`) mirrors
3125
+ * in its `DEFAULT_LOG_COLUMNS`. Keep the two in lockstep — a column added here
3126
+ * must gain a default there:
3127
+ * - `functionPath` (string) — always present
3128
+ * - `level` (string severity) — always present
3129
+ * - `message` (string) — always present
3130
+ * - `ts` (number, epoch-millis) — always present
3131
+ * - `fields` (nested object, or a JSON string when `serializeFields`) — when set
3132
+ * - `shardKey` (string) — when set
3133
+ * - `userId` (string) — when set
3134
+ * - `traceId` (string) — when set
3135
+ * - `spanId` (string) — when set
3136
+ *
2936
3137
  * Only `onLog` is implemented — RPC-span metrics belong in
2937
3138
  * {@link analyticsEngineSink}. `Pipeline.send` is durable/fire-and-forget on the
2938
3139
  * platform; the call is registered with the request's `context.waitUntil` when
@@ -2943,7 +3144,8 @@ interface PipelineLogSinkOptions {
2943
3144
  * Privacy: the persisted record carries `message` + structured `fields` (not the
2944
3145
  * raw positional args). They may include user input — the R2 bucket is your own,
2945
3146
  * but treat it as a log store and gate PII upstream if that is a concern.
2946
- * @param options Sink options: `pipeline` is the Cloudflare Pipeline binding.
3147
+ * @param options Sink options: `pipeline` is the Cloudflare Pipeline binding;
3148
+ * `serializeFields` stores `fields` as a queryable JSON string.
2947
3149
  */
2948
3150
  declare const pipelineLogSink: (options: PipelineLogSinkOptions) => ObservabilitySink;
2949
3151
  /** Options for {@link otlpSink}. */
@@ -3011,4 +3213,4 @@ declare const otlpSink: (options: OtlpSinkOptions) => ObservabilitySink;
3011
3213
  */
3012
3214
  declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
3013
3215
  declare const VERSION: string;
3014
- export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, type ListAuthUsersOptions, type LogEvent, type LogFields, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MetricEvent, type MetricKind, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type PipelineLike, type PipelineLogSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type SpanEvent, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, pipelineLogSink, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
3216
+ export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthConfigInfo, type AuthImpersonation, type AuthPage, type AuthSession, type AuthUser, type AuthUserFieldSpec, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_LOG_COLUMNS, DEFAULT_LOG_LIMIT, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, LOG_ARCHIVE_NOT_CONFIGURED, LOG_ARCHIVE_PATH, type ListAuthUsersOptions, type LogArchiveConfig, type LogEvent, type LogFields, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MetricEvent, type MetricKind, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type OtlpSinkOptions, type PipelineLike, type PipelineLogColumnMap, type PipelineLogCursor, type PipelineLogField, type PipelineLogPage, type PipelineLogQuery, type PipelineLogReader, type PipelineLogReaderOptions, type PipelineLogRow, type PipelineLogSinkOptions, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type SpanEvent, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createPipelineLogReader, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, otlpSink, pipelineLogSink, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
package/dist/index.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  export { toAirbyteMessages, toFivetranResponse } from './packem_shared/toAirbyteMessages-DrHdplb4.mjs';
2
- export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-CqUlK17X.mjs';
2
+ export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-DHyRiFho.mjs';
3
3
  export { createCrossShardRelationCapabilities } from './packem_shared/createCrossShardRelationCapabilities-CbcWjkAn.mjs';
4
4
  export { DEFAULT_REGISTRY_CACHE_TTL_MS, SHARD_REGISTRY_DO_NAME, createDynamicShardRegistry } from './packem_shared/DEFAULT_REGISTRY_CACHE_TTL_MS-B3pA7aXp.mjs';
5
5
  export { LunoraError, toErrorResponse } from './packem_shared/LunoraError-Bpb9EFJ3.mjs';
6
+ export { LOG_ARCHIVE_PATH } from './packem_shared/LOG_ARCHIVE_PATH-iKUShT0P.mjs';
6
7
  export { emitLogEvent, emitRpcEvent } from './packem_shared/emitLogEvent-pEdtqAK8.mjs';
7
- export { analyticsEngineSink, combineSinks, consoleSink, otlpSink, pipelineLogSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-DWUhiYHC.mjs';
8
+ export { analyticsEngineSink, combineSinks, consoleSink, otlpSink, pipelineLogSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-Bn7p0URe.mjs';
9
+ export { D as DEFAULT_LOG_COLUMNS, a as DEFAULT_LOG_LIMIT, c as createPipelineLogReader } from './packem_shared/pipeline-log-reader-BXULGNC3.mjs';
8
10
  export { createQueryCoordinator, createStaticShardRegistry, mergeStrategyForAggregate } from './packem_shared/createQueryCoordinator-DNCJzOZE.mjs';
9
11
  export { applyJurisdiction, resolveShard } from './packem_shared/applyJurisdiction-BkZtTkct.mjs';
10
12
  export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-DRWQFNhF.mjs';
13
+ export { LOG_ARCHIVE_NOT_CONFIGURED } from './packem_shared/LOG_ARCHIVE_NOT_CONFIGURED-acNcguqc.mjs';
11
14
  export { NOOP_EXECUTION_CONTEXT } from './packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
12
15
  export { composeIdentityResolvers, routeIdentityResolvers } from './packem_shared/composeIdentityResolvers-XGjO7V1J.mjs';
13
16
 
@@ -0,0 +1,2 @@
1
+ import '@lunora/bindings/r2sql';
2
+ export { D as DEFAULT_LOG_COLUMNS, a as DEFAULT_LOG_LIMIT, c as createPipelineLogReader } from './pipeline-log-reader-BXULGNC3.mjs';
@@ -0,0 +1,3 @@
1
+ const LOG_ARCHIVE_NOT_CONFIGURED = "LOG_ARCHIVE_NOT_CONFIGURED";
2
+
3
+ export { LOG_ARCHIVE_NOT_CONFIGURED };
@@ -0,0 +1,111 @@
1
+ import { createR2Sql } from '@lunora/bindings/r2sql';
2
+ import { LOG_ARCHIVE_NOT_CONFIGURED } from './LOG_ARCHIVE_NOT_CONFIGURED-acNcguqc.mjs';
3
+ import { L as LOG_LEVEL_ORDER, c as createPipelineLogReader } from './pipeline-log-reader-BXULGNC3.mjs';
4
+ import { LunoraError } from './LunoraError-Bpb9EFJ3.mjs';
5
+
6
+ const LOG_ARCHIVE_PATH = "/_lunora/admin/logs/archive";
7
+ const LOG_LEVELS = new Set(LOG_LEVEL_ORDER);
8
+ const optionalString = (value) => typeof value === "string" && value !== "" ? value : void 0;
9
+ const parseLevel = (value, field) => {
10
+ if (value === void 0) {
11
+ return void 0;
12
+ }
13
+ if (typeof value !== "string" || !LOG_LEVELS.has(value)) {
14
+ throw new LunoraError(`logs archive: invalid \`${field}\` — expected one of ${LOG_LEVEL_ORDER.join(", ")}`, { code: "BAD_REQUEST", status: 400 });
15
+ }
16
+ return value;
17
+ };
18
+ const parseFiniteNumber = (value, field) => {
19
+ if (value === void 0) {
20
+ return void 0;
21
+ }
22
+ if (typeof value !== "number" || !Number.isFinite(value)) {
23
+ throw new LunoraError(`logs archive: invalid \`${field}\` — expected a finite number`, { code: "BAD_REQUEST", status: 400 });
24
+ }
25
+ return value;
26
+ };
27
+ const parseQuery = (body) => {
28
+ const query = {};
29
+ const assignString = (field) => {
30
+ const value = optionalString(body[field]);
31
+ if (value !== void 0) {
32
+ query[field] = value;
33
+ }
34
+ };
35
+ assignString("functionPath");
36
+ assignString("functionPathPrefix");
37
+ assignString("traceId");
38
+ assignString("shardKey");
39
+ assignString("userId");
40
+ const level = parseLevel(body["level"], "level");
41
+ if (level !== void 0) {
42
+ query.level = level;
43
+ }
44
+ const minLevel = parseLevel(body["minLevel"], "minLevel");
45
+ if (minLevel !== void 0) {
46
+ query.minLevel = minLevel;
47
+ }
48
+ const sinceTs = parseFiniteNumber(body["sinceTs"], "sinceTs");
49
+ if (sinceTs !== void 0) {
50
+ query.sinceTs = sinceTs;
51
+ }
52
+ const untilTs = parseFiniteNumber(body["untilTs"], "untilTs");
53
+ if (untilTs !== void 0) {
54
+ query.untilTs = untilTs;
55
+ }
56
+ const limit = parseFiniteNumber(body["limit"], "limit");
57
+ if (limit !== void 0) {
58
+ query.limit = limit;
59
+ }
60
+ const rawCursor = body["cursor"];
61
+ if (typeof rawCursor === "object" && rawCursor !== null) {
62
+ const cursorTs = parseFiniteNumber(rawCursor["ts"], "cursor.ts");
63
+ if (cursorTs !== void 0) {
64
+ query.cursor = { ts: cursorTs };
65
+ }
66
+ }
67
+ return query;
68
+ };
69
+ const resolveCredentials = (environment) => {
70
+ const accountId = environment.R2_SQL_ACCOUNT_ID ?? environment.CLOUDFLARE_ACCOUNT_ID;
71
+ const apiToken = environment.R2_SQL_TOKEN;
72
+ const bucket = environment.R2_SQL_BUCKET;
73
+ const missing = [];
74
+ if (accountId === void 0 || accountId === "") {
75
+ missing.push("R2_SQL_ACCOUNT_ID");
76
+ }
77
+ if (apiToken === void 0 || apiToken === "") {
78
+ missing.push("R2_SQL_TOKEN");
79
+ }
80
+ if (bucket === void 0 || bucket === "") {
81
+ missing.push("R2_SQL_BUCKET");
82
+ }
83
+ if (missing.length > 0) {
84
+ throw new LunoraError(
85
+ `log archive not configured (missing ${missing.join(", ")}). The Pipeline must write to an R2 Data Catalog (Iceberg) table, and R2_SQL_ACCOUNT_ID / R2_SQL_TOKEN / R2_SQL_BUCKET must be set — see the observability docs.`,
86
+ { code: LOG_ARCHIVE_NOT_CONFIGURED, status: 400 }
87
+ );
88
+ }
89
+ return { accountId, apiToken, bucket };
90
+ };
91
+ const buildLogArchiveAdminRoutes = (deps) => {
92
+ const { createReader = createPipelineLogReader, readJsonBody, requireAdminOption } = deps;
93
+ const handleLogArchive = async (request, env) => {
94
+ if (request.method !== "POST") {
95
+ throw new LunoraError("Log-archive endpoint requires POST", { code: "METHOD_NOT_ALLOWED", status: 405 });
96
+ }
97
+ const config = requireAdminOption(request, deps.logArchive, {
98
+ code: LOG_ARCHIVE_NOT_CONFIGURED,
99
+ message: "log archive requires a `logArchive` config (an R2 Data Catalog table) on the worker — see the observability docs"
100
+ });
101
+ const { accountId, apiToken, bucket } = resolveCredentials(env ?? {});
102
+ const query = parseQuery(await readJsonBody(request));
103
+ const client = createR2Sql({ accountId, apiToken, bucket });
104
+ const reader = createReader(client, { columnMap: config.columnMap, namespace: config.namespace, table: config.table });
105
+ const page = await reader.query(query);
106
+ return Response.json(page, { headers: { "content-type": "application/json" }, status: 200 });
107
+ };
108
+ return { [LOG_ARCHIVE_PATH]: handleLogArchive };
109
+ };
110
+
111
+ export { LOG_ARCHIVE_NOT_CONFIGURED, LOG_ARCHIVE_PATH, buildLogArchiveAdminRoutes };
@@ -270,7 +270,7 @@ const analyticsEngineSink = (options) => {
270
270
  };
271
271
  };
272
272
  const pipelineLogSink = (options) => {
273
- const { pipeline } = options;
273
+ const { pipeline, serializeFields } = options;
274
274
  return {
275
275
  onLog: (event, context) => {
276
276
  try {
@@ -281,7 +281,7 @@ const pipelineLogSink = (options) => {
281
281
  ts: event.ts
282
282
  };
283
283
  if (event.fields) {
284
- record.fields = event.fields;
284
+ record.fields = serializeFields === true ? JSON.stringify(event.fields) : event.fields;
285
285
  }
286
286
  if (event.shardKey !== void 0) {
287
287
  record.shardKey = event.shardKey;
@@ -4,6 +4,7 @@ import { o as otlpRandomHex, b as buildTraceparent } from './otlp-DOLuy1Aj.mjs';
4
4
  import { LunoraError, toErrorResponse } from './LunoraError-Bpb9EFJ3.mjs';
5
5
  import { wrapResolverWithContract } from './composeIdentityResolvers-XGjO7V1J.mjs';
6
6
  export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-XGjO7V1J.mjs';
7
+ import { buildLogArchiveAdminRoutes } from './LOG_ARCHIVE_PATH-iKUShT0P.mjs';
7
8
  import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
8
9
  import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
9
10
  import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-DRWQFNhF.mjs';
@@ -2569,6 +2570,11 @@ const createWorker = (options) => {
2569
2570
  readJsonBody: readJsonBodyWithLimit,
2570
2571
  requireAdminOption
2571
2572
  });
2573
+ const logArchiveAdminRoutes = buildLogArchiveAdminRoutes({
2574
+ logArchive: options.logArchive,
2575
+ readJsonBody: readJsonBodyWithLimit,
2576
+ requireAdminOption
2577
+ });
2572
2578
  const introspectionAdminRoutes = buildIntrospectionAdminRoutes({
2573
2579
  assertAdmin: assertAdminAuthorized,
2574
2580
  options: {
@@ -3208,6 +3214,7 @@ const createWorker = (options) => {
3208
3214
  ...storageAdminRoutes,
3209
3215
  ...vectorAdminRoutes,
3210
3216
  ...kvAdminRoutes,
3217
+ ...logArchiveAdminRoutes,
3211
3218
  ...introspectionAdminRoutes,
3212
3219
  // `/_lunora/admin/auth/*` — the whole user-management plane, one route per
3213
3220
  // `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
@@ -0,0 +1,135 @@
1
+ import { raw, sql, desc } from '@lunora/bindings/r2sql';
2
+
3
+ const LOG_LEVEL_ORDER = ["trace", "debug", "log", "info", "warn", "error", "fatal"];
4
+
5
+ const DEFAULT_COLUMNS = {
6
+ fields: "fields",
7
+ functionPath: "functionPath",
8
+ level: "level",
9
+ message: "message",
10
+ shardKey: "shardKey",
11
+ spanId: "spanId",
12
+ traceId: "traceId",
13
+ ts: "ts",
14
+ userId: "userId"
15
+ };
16
+ const DEFAULT_LIMIT = 500;
17
+ const MAX_LIMIT = 1e4;
18
+ const clampLimit = (limit) => {
19
+ if (limit === void 0 || !Number.isFinite(limit)) {
20
+ return DEFAULT_LIMIT;
21
+ }
22
+ const floored = Math.floor(limit);
23
+ if (floored < 1) {
24
+ return 1;
25
+ }
26
+ if (floored > MAX_LIMIT) {
27
+ return MAX_LIMIT;
28
+ }
29
+ return floored;
30
+ };
31
+ const toTs = (value) => typeof value === "number" ? value : Number(value);
32
+ const renderCell = (value) => value === null || value === void 0 ? "" : String(value);
33
+ const decodeFields = (value) => {
34
+ if (typeof value !== "string") {
35
+ return value;
36
+ }
37
+ try {
38
+ return JSON.parse(value);
39
+ } catch {
40
+ return value;
41
+ }
42
+ };
43
+ const DEFAULT_LOG_COLUMNS = DEFAULT_COLUMNS;
44
+ const DEFAULT_LOG_LIMIT = DEFAULT_LIMIT;
45
+ const createPipelineLogReader = (client, options) => {
46
+ const columns = { ...DEFAULT_COLUMNS, ...options.columnMap };
47
+ const tableReference = options.namespace === void 0 ? options.table : `${options.namespace}.${options.table}`;
48
+ return {
49
+ query: async (query = {}) => {
50
+ const limit = clampLimit(query.limit);
51
+ const fetchLimit = Math.min(limit + 1, MAX_LIMIT);
52
+ const builder = client.from(tableReference).select(
53
+ raw(columns.functionPath),
54
+ raw(columns.level),
55
+ raw(columns.message),
56
+ raw(columns.ts),
57
+ raw(columns.fields),
58
+ raw(columns.shardKey),
59
+ raw(columns.userId),
60
+ raw(columns.traceId),
61
+ raw(columns.spanId)
62
+ );
63
+ if (query.sinceTs !== void 0) {
64
+ builder.where(sql`${raw(columns.ts)} >= ${query.sinceTs}`);
65
+ }
66
+ if (query.untilTs !== void 0) {
67
+ builder.where(sql`${raw(columns.ts)} <= ${query.untilTs}`);
68
+ }
69
+ if (query.level !== void 0) {
70
+ builder.where(sql`${raw(columns.level)} = ${query.level}`);
71
+ } else if (query.minLevel !== void 0) {
72
+ const floorIndex = LOG_LEVEL_ORDER.indexOf(query.minLevel);
73
+ const allowed = floorIndex === -1 ? [...LOG_LEVEL_ORDER] : LOG_LEVEL_ORDER.slice(floorIndex);
74
+ builder.where(sql`${raw(columns.level)} IN ${allowed}`);
75
+ }
76
+ if (query.functionPath !== void 0) {
77
+ builder.where(sql`${raw(columns.functionPath)} = ${query.functionPath}`);
78
+ }
79
+ if (query.functionPathPrefix !== void 0) {
80
+ builder.where(sql`${raw(columns.functionPath)} LIKE ${`${query.functionPathPrefix}%`}`);
81
+ }
82
+ if (query.traceId !== void 0) {
83
+ builder.where(sql`${raw(columns.traceId)} = ${query.traceId}`);
84
+ }
85
+ if (query.shardKey !== void 0) {
86
+ builder.where(sql`${raw(columns.shardKey)} = ${query.shardKey}`);
87
+ }
88
+ if (query.userId !== void 0) {
89
+ builder.where(sql`${raw(columns.userId)} = ${query.userId}`);
90
+ }
91
+ if (query.cursor !== void 0) {
92
+ builder.where(sql`${raw(columns.ts)} < ${query.cursor.ts}`);
93
+ }
94
+ builder.orderBy(desc(raw(columns.ts))).limit(fetchLimit);
95
+ const { rows } = await builder.run();
96
+ const hasMore = rows.length > limit;
97
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
98
+ const overflow = hasMore ? rows[limit] : void 0;
99
+ const decoded = pageRows.map((row) => {
100
+ const out = {
101
+ functionPath: renderCell(row[columns.functionPath]),
102
+ // The stored `level` is one of the canonical severities; the
103
+ // reader trusts the writer's contract here rather than re-validating.
104
+ level: renderCell(row[columns.level]),
105
+ message: renderCell(row[columns.message]),
106
+ ts: toTs(row[columns.ts])
107
+ };
108
+ const fields = row[columns.fields];
109
+ if (fields !== void 0 && fields !== null) {
110
+ out.fields = decodeFields(fields);
111
+ }
112
+ const shardKey = row[columns.shardKey];
113
+ if (shardKey !== void 0 && shardKey !== null) {
114
+ out.shardKey = renderCell(shardKey);
115
+ }
116
+ const userId = row[columns.userId];
117
+ if (userId !== void 0 && userId !== null) {
118
+ out.userId = renderCell(userId);
119
+ }
120
+ const traceId = row[columns.traceId];
121
+ if (traceId !== void 0 && traceId !== null) {
122
+ out.traceId = renderCell(traceId);
123
+ }
124
+ const spanId = row[columns.spanId];
125
+ if (spanId !== void 0 && spanId !== null) {
126
+ out.spanId = renderCell(spanId);
127
+ }
128
+ return out;
129
+ });
130
+ return overflow === void 0 ? { rows: decoded } : { nextCursor: { ts: toTs(overflow[columns.ts]) }, rows: decoded };
131
+ }
132
+ };
133
+ };
134
+
135
+ export { DEFAULT_LOG_COLUMNS as D, LOG_LEVEL_ORDER as L, DEFAULT_LOG_LIMIT as a, createPipelineLogReader as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.30",
3
+ "version": "1.0.0-alpha.32",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,6 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
+ "@lunora/bindings": "1.0.0-alpha.9",
49
50
  "@lunora/errors": "1.0.0-alpha.6"
50
51
  },
51
52
  "engines": {