@lunora/do 0.0.0 → 1.0.0-alpha.1
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/LICENSE.md +105 -0
- package/README.md +115 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +5599 -0
- package/dist/index.d.ts +5599 -0
- package/dist/index.mjs +35 -0
- package/dist/packem_shared/ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs +313 -0
- package/dist/packem_shared/AUTH_METRICS_BUCKET_MS-CiHHYeJi.mjs +84 -0
- package/dist/packem_shared/ConflictError-C0STs6bU.mjs +13 -0
- package/dist/packem_shared/CountRlsUnsupportedError-28ZvvwKS.mjs +133 -0
- package/dist/packem_shared/DATA_MIGRATION_STATE_TABLE-PTtTiQ7U.mjs +237 -0
- package/dist/packem_shared/LogBuffer-B_Ezju_N.mjs +37 -0
- package/dist/packem_shared/NotFoundError-CMuMZt81.mjs +10 -0
- package/dist/packem_shared/ROOT_DO_SIZE_WARN_BYTES-DQkmGiCS.mjs +4009 -0
- package/dist/packem_shared/ReactiveCache-ByVzgH3d.mjs +259 -0
- package/dist/packem_shared/SCAN_DEP-DLJF8dsj.mjs +19 -0
- package/dist/packem_shared/SESSION_DO_TTL_DEFAULT-ilPZsVwu.mjs +180 -0
- package/dist/packem_shared/SHARD_REGISTRY_DO_NAME-BsAbi5Mn.mjs +146 -0
- package/dist/packem_shared/aggregateTableName-CxNqY1Sl.mjs +64 -0
- package/dist/packem_shared/applyCdcChanges-Ctdmxmrv.mjs +103 -0
- package/dist/packem_shared/applyOnDelete-CMif2RKw.mjs +165 -0
- package/dist/packem_shared/armRestore-BJk53Ro8.mjs +55 -0
- package/dist/packem_shared/assertFlatPredicate-DyVYReuT.mjs +160 -0
- package/dist/packem_shared/assertReadonly-dDcFE1YZ.mjs +29 -0
- package/dist/packem_shared/assertValidClientId-CBZ1zC96.mjs +1745 -0
- package/dist/packem_shared/backfillAggregateIndexes-BF5eL7kW.mjs +80 -0
- package/dist/packem_shared/buildSecurityAudit-CCAvoFlr.mjs +1 -0
- package/dist/packem_shared/buildSeekWhere-lVsNXSLy.mjs +84 -0
- package/dist/packem_shared/clearCapturedMail-CPpgl-dX.mjs +104 -0
- package/dist/packem_shared/compileWhereSql-CXrhFA3G.mjs +127 -0
- package/dist/packem_shared/createSystemReader-8CzSZP9V.mjs +80 -0
- package/dist/packem_shared/ctx-db-idempotency-DkC9rP91.mjs +35 -0
- package/dist/packem_shared/do-exec-5eQy5cEi.mjs +12 -0
- package/dist/packem_shared/do-sql-BCHCWtrD.mjs +87 -0
- package/dist/packem_shared/encodePartitionKey-C6blLR5K.mjs +1 -0
- package/dist/packem_shared/ensureFunctionMetricsTables-UDNVD7FS.mjs +248 -0
- package/dist/packem_shared/exportShardRows-DZEhUeyI.mjs +156 -0
- package/dist/packem_shared/ftsTableName-BLEMawrp.mjs +38 -0
- package/dist/packem_shared/guardWriter-u3UlnCH5.mjs +128 -0
- package/dist/packem_shared/matchesStaticWhere-CFk6adSu.mjs +54 -0
- package/dist/packem_shared/rank-CrkEIpF4.mjs +102 -0
- package/dist/packem_shared/renderSql-D6eUcn2N.mjs +16 -0
- package/dist/packem_shared/runShardMigrations-C3bn5r93.mjs +103 -0
- package/dist/packem_shared/runTriggers-5N6_Fx0A.mjs +20 -0
- package/dist/packem_shared/security-audit-CucgBice.mjs +158 -0
- package/dist/packem_shared/serveRelationFanout-Clr1a05L.mjs +24 -0
- package/package.json +41 -17
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const serializeSqlValue = (value) => {
|
|
2
|
+
if (typeof value === "boolean") {
|
|
3
|
+
return value ? 1 : 0;
|
|
4
|
+
}
|
|
5
|
+
if (value === null || typeof value === "string" || typeof value === "number") {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === "bigint") {
|
|
9
|
+
return value.toString();
|
|
10
|
+
}
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const compareStrings = (a, b) => {
|
|
15
|
+
if (a < b) {
|
|
16
|
+
return -1;
|
|
17
|
+
}
|
|
18
|
+
return a > b ? 1 : 0;
|
|
19
|
+
};
|
|
20
|
+
const NOT_RESOLVABLE = /* @__PURE__ */ Symbol("not-resolvable");
|
|
21
|
+
const resolvePartitionValue = (value) => {
|
|
22
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
23
|
+
const operatorKeys = Object.keys(value);
|
|
24
|
+
if (operatorKeys.length === 1 && operatorKeys[0] === "eq") {
|
|
25
|
+
return value.eq;
|
|
26
|
+
}
|
|
27
|
+
return NOT_RESOLVABLE;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
const encodePartitionKey = (partitionBy, source) => {
|
|
32
|
+
if (partitionBy.length === 0) {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
const ordered = {};
|
|
36
|
+
for (const field of [...partitionBy].toSorted(compareStrings)) {
|
|
37
|
+
ordered[field] = source[field] ?? null;
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(ordered);
|
|
40
|
+
};
|
|
41
|
+
const RANK_TIEBREAK = "__id__";
|
|
42
|
+
const sortColumnName = (i) => `__sort_k${String(i)}__`;
|
|
43
|
+
const matchesRankStaticWhere = (document, predicate) => {
|
|
44
|
+
for (const [field, expected] of Object.entries(predicate)) {
|
|
45
|
+
const actual = document[field];
|
|
46
|
+
if (expected !== null && typeof expected === "object" && !Array.isArray(expected)) {
|
|
47
|
+
const operatorKeys = Object.keys(expected);
|
|
48
|
+
if (operatorKeys.length === 1 && operatorKeys[0] === "eq") {
|
|
49
|
+
if (actual !== expected.eq) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (actual !== expected) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
const resolveRankPartition = (index, where) => {
|
|
63
|
+
const partitionBy = index.partitionBy ?? [];
|
|
64
|
+
const requested = where ?? {};
|
|
65
|
+
for (const key of Object.keys(requested)) {
|
|
66
|
+
if (key === "AND" || key === "OR" || key === "NOT") {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const resolved = {};
|
|
71
|
+
for (const field of partitionBy) {
|
|
72
|
+
if (field in requested) {
|
|
73
|
+
const value = resolvePartitionValue(requested[field]);
|
|
74
|
+
if (value === NOT_RESOLVABLE) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
resolved[field] = value;
|
|
78
|
+
} else if (index.where && field in index.where) {
|
|
79
|
+
resolved[field] = index.where[field];
|
|
80
|
+
} else {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return resolved;
|
|
85
|
+
};
|
|
86
|
+
const rankTableName = (table, indexName) => `${table}__rank_${indexName}`;
|
|
87
|
+
const rankKeyFromDocument = (index, document_) => {
|
|
88
|
+
return {
|
|
89
|
+
partitionKey: encodePartitionKey(index.partitionBy ?? [], document_),
|
|
90
|
+
rowId: document_["_id"],
|
|
91
|
+
// Serialize each sort value the same way `syncRankIndexEntry` writes the
|
|
92
|
+
// stored `__sort_k<i>__` column, so a peer shard's comparison matches
|
|
93
|
+
// byte-for-byte. This also makes the values JSON-safe for the
|
|
94
|
+
// cross-shard RPC wire — `serializeSqlValue` turns bigint/Date/object
|
|
95
|
+
// into a string|number|null, where raw bigint would crash JSON.stringify
|
|
96
|
+
// and a raw Date would serialize to a different shape than the store.
|
|
97
|
+
// eslint-disable-next-line unicorn/no-null -- mirrors syncRankIndexEntry: a missing field serializes via `?? null` so the stored and wire bytes agree
|
|
98
|
+
sortValues: index.sortBy.map((key) => serializeSqlValue(document_[key.field] ?? null))
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export { RANK_TIEBREAK as R, resolveRankPartition as a, serializeSqlValue as b, rankKeyFromDocument as c, encodePartitionKey as e, matchesRankStaticWhere as m, rankTableName as r, sortColumnName as s };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { MySqlDialect } from 'drizzle-orm/mysql-core';
|
|
3
|
+
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { SQLiteSyncDialect } from 'drizzle-orm/sqlite-core';
|
|
5
|
+
|
|
6
|
+
const PG_DIALECT = new PgDialect();
|
|
7
|
+
const MYSQL_DIALECT = new MySqlDialect();
|
|
8
|
+
const SQLITE_DIALECT = new SQLiteSyncDialect();
|
|
9
|
+
const DIALECTS = { mysql: MYSQL_DIALECT, postgres: PG_DIALECT, sqlite: SQLITE_DIALECT };
|
|
10
|
+
const renderSql = (engine, query) => {
|
|
11
|
+
const { params, sql: text } = DIALECTS[engine].sqlToQuery(query);
|
|
12
|
+
return { params, sql: text };
|
|
13
|
+
};
|
|
14
|
+
const param = (value) => sql`${value}`;
|
|
15
|
+
|
|
16
|
+
export { param, renderSql };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { aggregateTableName } from './aggregateTableName-CxNqY1Sl.mjs';
|
|
3
|
+
import { migrateCdcLog, migrateCdcMeta } from './applyCdcChanges-Ctdmxmrv.mjs';
|
|
4
|
+
import { m as migrateIdempotency } from './ctx-db-idempotency-DkC9rP91.mjs';
|
|
5
|
+
import { r as runDrizzle } from './do-exec-5eQy5cEi.mjs';
|
|
6
|
+
import { D as DOC_COLUMN, j as jsonPathSql, c as createIndexSql, t as tableColumns, i as isFtsAvailable, A as AGG_KEY, a as AGG_VALUE, b as AGG_COUNT } from './do-sql-BCHCWtrD.mjs';
|
|
7
|
+
import { s as sortColumnName, r as rankTableName } from './rank-CrkEIpF4.mjs';
|
|
8
|
+
import { ftsTableName } from './ftsTableName-BLEMawrp.mjs';
|
|
9
|
+
|
|
10
|
+
const migrateSecondaryIndexes = (sql$1, tableName, definition) => {
|
|
11
|
+
for (const index of definition.indexes) {
|
|
12
|
+
const indexName = `${tableName}_${index.name}`;
|
|
13
|
+
const expressions = sql.join(
|
|
14
|
+
index.fields.map((field) => jsonPathSql(field)),
|
|
15
|
+
sql`, `
|
|
16
|
+
);
|
|
17
|
+
runDrizzle(sql$1, createIndexSql(indexName, tableName, expressions, index.unique ?? false));
|
|
18
|
+
}
|
|
19
|
+
for (const [field, column] of tableColumns(definition)) {
|
|
20
|
+
if (!column.unique) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const indexName = `${tableName}_unique_${field}`;
|
|
24
|
+
runDrizzle(sql$1, createIndexSql(indexName, tableName, jsonPathSql(field), true));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const migrateSearchIndexes = (sql$1, tableName, definition) => {
|
|
28
|
+
if (!definition.searchIndexes || definition.searchIndexes.length === 0 || !isFtsAvailable(sql$1)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const index of definition.searchIndexes) {
|
|
32
|
+
const ftName = ftsTableName(tableName, index.name);
|
|
33
|
+
runDrizzle(
|
|
34
|
+
sql$1,
|
|
35
|
+
sql`CREATE VIRTUAL TABLE IF NOT EXISTS ${sql.identifier(ftName)} USING fts5(${sql.identifier("__text__")}, ${sql.identifier("__id__")} UNINDEXED)`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const migrateAggregateIndexes = (sql$1, tableName, definition) => {
|
|
40
|
+
if (!definition.aggregateIndexes) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const index of definition.aggregateIndexes) {
|
|
44
|
+
const aggTable = aggregateTableName(tableName, index.name);
|
|
45
|
+
runDrizzle(
|
|
46
|
+
sql$1,
|
|
47
|
+
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(aggTable)} (${AGG_KEY} TEXT PRIMARY KEY, ${AGG_VALUE} REAL, ${AGG_COUNT} INTEGER NOT NULL DEFAULT 0)`
|
|
48
|
+
);
|
|
49
|
+
const columns = runDrizzle(sql$1, sql`PRAGMA table_info(${sql.identifier(aggTable)})`).toArray();
|
|
50
|
+
if (!columns.some((column) => column.name === "__count__")) {
|
|
51
|
+
runDrizzle(sql$1, sql`ALTER TABLE ${sql.identifier(aggTable)} ADD COLUMN ${AGG_COUNT} INTEGER NOT NULL DEFAULT 0`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const migrateRankIndexes = (sql$1, tableName, definition) => {
|
|
56
|
+
if (!definition.rankIndexes) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const index of definition.rankIndexes) {
|
|
60
|
+
const rankTable = rankTableName(tableName, index.name);
|
|
61
|
+
const sortColumns = index.sortBy.map((_, i) => sortColumnName(i));
|
|
62
|
+
const columnDdls = sortColumns.map((column) => sql`${sql.identifier(column)} BLOB`);
|
|
63
|
+
const columnPart = columnDdls.length > 0 ? sql`, ${sql.join(columnDdls, sql`, `)}` : sql``;
|
|
64
|
+
runDrizzle(
|
|
65
|
+
sql$1,
|
|
66
|
+
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(rankTable)} (${sql.identifier("__id__")} TEXT PRIMARY KEY, ${sql.identifier("__partition__")} TEXT NOT NULL${columnPart})`
|
|
67
|
+
);
|
|
68
|
+
const orderedColumns = [sql`${sql.identifier("__partition__")} ASC`];
|
|
69
|
+
for (const [i, column] of sortColumns.entries()) {
|
|
70
|
+
const direction = index.sortBy[i]?.direction;
|
|
71
|
+
orderedColumns.push(sql`${sql.identifier(column)} ${sql.raw(direction === "desc" ? "DESC" : "ASC")}`);
|
|
72
|
+
}
|
|
73
|
+
orderedColumns.push(sql`${sql.identifier("__id__")} ASC`);
|
|
74
|
+
const btreeName = `${tableName}__rank_${index.name}__btree`;
|
|
75
|
+
runDrizzle(sql$1, createIndexSql(btreeName, rankTable, sql.join(orderedColumns, sql`, `), false));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const runShardMigrations = (sql$1, schema, options = {}) => {
|
|
79
|
+
for (const [tableName, definition] of Object.entries(schema.tables)) {
|
|
80
|
+
if (definition.shardMode?.kind === "global") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
runDrizzle(
|
|
84
|
+
sql$1,
|
|
85
|
+
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier(tableName)} (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
_creationTime REAL NOT NULL,
|
|
88
|
+
${sql.identifier(DOC_COLUMN)} TEXT NOT NULL
|
|
89
|
+
)`
|
|
90
|
+
);
|
|
91
|
+
migrateSecondaryIndexes(sql$1, tableName, definition);
|
|
92
|
+
migrateSearchIndexes(sql$1, tableName, definition);
|
|
93
|
+
migrateAggregateIndexes(sql$1, tableName, definition);
|
|
94
|
+
migrateRankIndexes(sql$1, tableName, definition);
|
|
95
|
+
}
|
|
96
|
+
if (options.cdc) {
|
|
97
|
+
migrateCdcLog(sql$1);
|
|
98
|
+
migrateCdcMeta(sql$1);
|
|
99
|
+
}
|
|
100
|
+
migrateIdempotency(sql$1);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export { runShardMigrations };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const runTriggers = async (options) => {
|
|
2
|
+
const definitions = options.schema.tables[options.tableName]?.triggerMap;
|
|
3
|
+
if (!definitions) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
for (const definition of Object.values(definitions)) {
|
|
7
|
+
if (definition.timing === options.timing && definition.op === options.op) {
|
|
8
|
+
await definition.handler(options.ctx, options.event);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const hasTrigger = (schema, tableName, op) => {
|
|
13
|
+
const definitions = schema.tables[tableName]?.triggerMap;
|
|
14
|
+
if (!definitions) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return Object.values(definitions).some((definition) => definition.op === op);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { hasTrigger, runTriggers };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const SECRET_NAME_PATTERN = /key|secret|token|password|passwd|credential|private|auth|bearer|session|cookie|salt|signature|dsn|webhook/iu;
|
|
2
|
+
const LUNORA_INTERNAL_VARS = /* @__PURE__ */ new Set(["LUNORA_ADMIN_TOKEN", "LUNORA_WS_BEARER"]);
|
|
3
|
+
const WORKER_URL_VARS = ["CF_PAGES_URL", "WORKER_URL", "LUNORA_WORKER_URL"];
|
|
4
|
+
const ENVIRONMENT_VARS = ["CF_ENV", "ENVIRONMENT", "WORKER_ENV", "NODE_ENV"];
|
|
5
|
+
const redact = (value) => {
|
|
6
|
+
if (value.length <= 4) {
|
|
7
|
+
return "••••";
|
|
8
|
+
}
|
|
9
|
+
return `${value.slice(0, 4)}${"•".repeat(Math.min(8, value.length - 4))}`;
|
|
10
|
+
};
|
|
11
|
+
const looksSecret = (name) => SECRET_NAME_PATTERN.test(name);
|
|
12
|
+
const bindingType = (value) => {
|
|
13
|
+
const has = (key) => typeof value[key] === "function";
|
|
14
|
+
if (has("idFromName") || has("idFromString")) {
|
|
15
|
+
return "durable-object";
|
|
16
|
+
}
|
|
17
|
+
if (has("createMultipartUpload") || has("put") && has("get") && has("list") && has("head")) {
|
|
18
|
+
return "r2";
|
|
19
|
+
}
|
|
20
|
+
if (has("getWithMetadata")) {
|
|
21
|
+
return "kv";
|
|
22
|
+
}
|
|
23
|
+
if (has("prepare") && has("dump")) {
|
|
24
|
+
return "d1";
|
|
25
|
+
}
|
|
26
|
+
if (has("send") && has("sendBatch")) {
|
|
27
|
+
return "queue";
|
|
28
|
+
}
|
|
29
|
+
if (has("fetch") && !has("get")) {
|
|
30
|
+
return "service";
|
|
31
|
+
}
|
|
32
|
+
return "object";
|
|
33
|
+
};
|
|
34
|
+
const readDeployInfo = (env) => {
|
|
35
|
+
const deploy = {};
|
|
36
|
+
for (const key of WORKER_URL_VARS) {
|
|
37
|
+
if (typeof env[key] === "string" && env[key] !== "") {
|
|
38
|
+
deploy.workerUrl = env[key];
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const key of ENVIRONMENT_VARS) {
|
|
43
|
+
if (typeof env[key] === "string" && env[key] !== "") {
|
|
44
|
+
deploy.environment = env[key];
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const versionMetadata = env["CF_VERSION_METADATA"];
|
|
49
|
+
if (versionMetadata !== null && typeof versionMetadata === "object") {
|
|
50
|
+
const meta = versionMetadata;
|
|
51
|
+
if (typeof meta["id"] === "string") {
|
|
52
|
+
deploy.deploymentId = meta["id"];
|
|
53
|
+
}
|
|
54
|
+
if (typeof meta["tag"] === "string") {
|
|
55
|
+
deploy.versionTag = meta["tag"];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return deploy;
|
|
59
|
+
};
|
|
60
|
+
const DEV_ENVIRONMENT_PATTERN = /^(?:dev(?:elopment)?|local(?:host)?|test)$/iu;
|
|
61
|
+
const isDevEnvironment = (rawEnv) => {
|
|
62
|
+
const env = rawEnv ?? {};
|
|
63
|
+
for (const key of ENVIRONMENT_VARS) {
|
|
64
|
+
const value = env[key];
|
|
65
|
+
if (typeof value === "string" && DEV_ENVIRONMENT_PATTERN.test(value)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
const buildSettings = (rawEnv) => {
|
|
72
|
+
const env = rawEnv ?? {};
|
|
73
|
+
const settings = [];
|
|
74
|
+
for (const [name, value] of Object.entries(env)) {
|
|
75
|
+
if (LUNORA_INTERNAL_VARS.has(name)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
const secret = looksSecret(name);
|
|
80
|
+
settings.push({
|
|
81
|
+
kind: secret ? "secret" : "var",
|
|
82
|
+
name,
|
|
83
|
+
// Both vars and secrets are masked — a "var" may still hold a
|
|
84
|
+
// sensitive value, and the studio never needs the raw text.
|
|
85
|
+
value: redact(value)
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
90
|
+
settings.push({ kind: "var", name, value: String(value) });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (value !== null && typeof value === "object") {
|
|
94
|
+
settings.push({ bindingType: bindingType(value), kind: "binding", name, value: null });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
settings.push({ kind: "var", name, value: null });
|
|
98
|
+
}
|
|
99
|
+
settings.sort((a, b) => a.name.localeCompare(b.name));
|
|
100
|
+
return { deploy: readDeployInfo(env), settings };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const MIN_ADMIN_TOKEN_LENGTH = 24;
|
|
104
|
+
const MIN_AUTH_SECRET_LENGTH = 32;
|
|
105
|
+
const LEVEL_ORDER = { error: 0, info: 2, warning: 1 };
|
|
106
|
+
const DISABLED_ENV_VALUES = /* @__PURE__ */ new Set(["0", "disabled", "false", "no", "off"]);
|
|
107
|
+
const ENABLED_ENV_VALUES = /* @__PURE__ */ new Set(["1", "enabled", "on", "true", "yes"]);
|
|
108
|
+
const readFlag = (value) => typeof value === "string" ? value.trim().toLowerCase() : void 0;
|
|
109
|
+
const auditAuthSecret = (env) => {
|
|
110
|
+
const authSecret = env["AUTH_SECRET"] ?? env["BETTER_AUTH_SECRET"];
|
|
111
|
+
const length = typeof authSecret === "string" ? authSecret.trim().length : 0;
|
|
112
|
+
if (length > 0 && length < MIN_AUTH_SECRET_LENGTH) {
|
|
113
|
+
return [{ detail: { length, min: MIN_AUTH_SECRET_LENGTH }, kind: "auth-secret-weak", level: "warning" }];
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
};
|
|
117
|
+
const auditCors = (env) => {
|
|
118
|
+
const allowedOrigins = readFlag(env["LUNORA_ALLOWED_ORIGINS"]);
|
|
119
|
+
const corsCredentials = ENABLED_ENV_VALUES.has(readFlag(env["LUNORA_CORS_ALLOW_CREDENTIALS"]) ?? "");
|
|
120
|
+
const hasWildcard = allowedOrigins?.split(",").some((entry) => entry.trim() === "*") ?? false;
|
|
121
|
+
return hasWildcard && corsCredentials ? [{ kind: "cors-wildcard-credentials", level: "error" }] : [];
|
|
122
|
+
};
|
|
123
|
+
const auditSecurityLayers = (env, dev) => {
|
|
124
|
+
if (dev) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const findings = [];
|
|
128
|
+
if (DISABLED_ENV_VALUES.has(readFlag(env["LUNORA_SECURITY_HEADERS"]) ?? "")) {
|
|
129
|
+
findings.push({ kind: "security-headers-disabled", level: "warning" });
|
|
130
|
+
}
|
|
131
|
+
if (DISABLED_ENV_VALUES.has(readFlag(env["LUNORA_SECURITY_CSRF"]) ?? "")) {
|
|
132
|
+
findings.push({ kind: "csrf-disabled", level: "warning" });
|
|
133
|
+
}
|
|
134
|
+
if (readFlag(env["BETTER_AUTH_URL"])?.startsWith("http://") === true) {
|
|
135
|
+
findings.push({ kind: "cookies-insecure", level: "warning" });
|
|
136
|
+
}
|
|
137
|
+
return findings;
|
|
138
|
+
};
|
|
139
|
+
const buildSecurityAudit = (rawEnv) => {
|
|
140
|
+
const env = rawEnv ?? {};
|
|
141
|
+
const findings = [];
|
|
142
|
+
const adminToken = env["LUNORA_ADMIN_TOKEN"];
|
|
143
|
+
if (typeof adminToken === "string" && adminToken.length > 0 && adminToken.length < MIN_ADMIN_TOKEN_LENGTH) {
|
|
144
|
+
findings.push({ detail: { length: adminToken.length, min: MIN_ADMIN_TOKEN_LENGTH }, kind: "admin-token-weak", level: "warning" });
|
|
145
|
+
}
|
|
146
|
+
const wsBearer = env["LUNORA_WS_BEARER"];
|
|
147
|
+
const dev = isDevEnvironment(env);
|
|
148
|
+
if (typeof wsBearer !== "string" || wsBearer === "") {
|
|
149
|
+
findings.push({ kind: "ws-gate-open", level: dev ? "info" : "error" });
|
|
150
|
+
}
|
|
151
|
+
if (dev) {
|
|
152
|
+
findings.push({ kind: "dev-args-unredacted", level: "warning" });
|
|
153
|
+
}
|
|
154
|
+
findings.push(...auditAuthSecret(env), ...auditCors(env), ...auditSecurityLayers(env, dev));
|
|
155
|
+
return { findings: findings.toSorted((a, b) => LEVEL_ORDER[a.level] - LEVEL_ORDER[b.level]) };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export { MIN_ADMIN_TOKEN_LENGTH as M, MIN_AUTH_SECRET_LENGTH as a, buildSecurityAudit as b, buildSettings as c, isDevEnvironment as i };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTION_PREFIX-Dzdqq5J2.mjs';
|
|
2
|
+
|
|
3
|
+
const serveRelationFanout = async (schema, database, functionPath, args) => {
|
|
4
|
+
const table = typeof args["table"] === "string" ? args["table"] : "";
|
|
5
|
+
const definition = schema.tables[table];
|
|
6
|
+
if (!definition) {
|
|
7
|
+
throw Object.assign(new Error(`${RELATION_FUNCTION_PREFIX} unknown table "${table}"`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
|
|
8
|
+
}
|
|
9
|
+
if (definition.shardMode?.kind === "global") {
|
|
10
|
+
throw Object.assign(new Error(`${RELATION_FUNCTION_PREFIX} table "${table}" is global, not shard-local`), {
|
|
11
|
+
code: "BAD_REQUEST",
|
|
12
|
+
name: "LunoraError",
|
|
13
|
+
status: 400
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const where = args["where"] ?? void 0;
|
|
17
|
+
if (functionPath === `${RELATION_FUNCTION_PREFIX}count`) {
|
|
18
|
+
return database.count(table, where);
|
|
19
|
+
}
|
|
20
|
+
const result = await database.findMany(table, { orderBy: args["orderBy"], where, with: args["with"] });
|
|
21
|
+
return result.page;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { serveRelationFanout };
|
package/package.json
CHANGED
|
@@ -1,31 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/do",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
4
|
"description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare",
|
|
7
|
+
"durable-objects",
|
|
8
|
+
"hibernation",
|
|
9
|
+
"lunora",
|
|
10
|
+
"shard",
|
|
11
|
+
"sqlite",
|
|
12
|
+
"websocket",
|
|
13
|
+
"workers"
|
|
14
|
+
],
|
|
6
15
|
"homepage": "https://lunora.sh",
|
|
16
|
+
"bugs": "https://github.com/anolilab/lunora/issues",
|
|
17
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Daniel Bannert",
|
|
20
|
+
"email": "d.bannert@anolilab.de"
|
|
21
|
+
},
|
|
7
22
|
"repository": {
|
|
8
23
|
"type": "git",
|
|
9
24
|
"url": "git+https://github.com/anolilab/lunora.git",
|
|
10
25
|
"directory": "packages/do"
|
|
11
26
|
},
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"cloudflare",
|
|
18
|
-
"workers",
|
|
19
|
-
"durable-objects",
|
|
20
|
-
"sqlite",
|
|
21
|
-
"websocket",
|
|
22
|
-
"hibernation",
|
|
23
|
-
"shard"
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE.md",
|
|
31
|
+
"__assets__"
|
|
24
32
|
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"main": "./dist/index.mjs",
|
|
36
|
+
"module": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"import": "./dist/index.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
25
45
|
"publishConfig": {
|
|
26
46
|
"access": "public"
|
|
27
47
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@visulima/redact": "3.0.0-alpha.14",
|
|
50
|
+
"drizzle-orm": "^0.45.2"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": "^22.15.0 || >=24.11.0"
|
|
54
|
+
}
|
|
31
55
|
}
|