@adep/runtime 0.1.0 → 0.1.2

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.
@@ -1,3 +1,19 @@
1
+ /**
2
+ * ULID 生成器(DB-006 owned 表主键)。
3
+ *
4
+ * 128 位 = 48 位毫秒时间戳 + 80 位随机,Crockford Base32 编码,26 字符。
5
+ * 性质(Gherkin Scenario:owned 表行主键为 ULID 且本地可生成):
6
+ * - **时间有序**:高位是毫秒时间戳,新生成的 ULID 字典序严格 ≥ 旧值;
7
+ * - **单调**:同一毫秒内对随机段 +1 递增,保证同毫秒生成不冲突;
8
+ * - **离线可生成**:纯本地随机,无中心协调(对比 UUID v4 需中心分配)。
9
+ *
10
+ * 纯函数模块,无运行时依赖;离线/本地节点可用相同逻辑复现(M8 同步客户端)。
11
+ *
12
+ * FN-023 起随机源走 `crypto.getRandomValues`(浏览器 / Node19+ / Bun 全 realm 同形),
13
+ * 不再 import `node:crypto`——`@adep/runtime` 的 builder 模块要进浏览器 bundle
14
+ * (离线模拟运行时复用 `createDbCapability`),node-only import 会让打包直接失败。
15
+ * 退化分支(非安全上下文无 crypto)仅覆盖模拟/开发场景,主键唯一性由单调段兜底。
16
+ */
1
17
  /** ULID 合法字符集(用于校验:`^[0-9A-HJKMNP-TV-Z]{26}$`)。 */
2
18
  export declare const ULID_PATTERN: RegExp;
3
19
  /**
package/dist/index.d.ts CHANGED
@@ -32,4 +32,9 @@ export type { StorageDriver, StoredFileMeta, FileVisibility } from './storage/dr
32
32
  export { StorageError, STORAGE_CODES } from './storage/driver';
33
33
  export { signDownloadUrl, verifyDownloadSignature } from './storage/signature';
34
34
  export type { DownloadSignerConfig } from './storage/signature';
35
+ export { hmacSha256, hmacSha256Hex, sha256, constantTimeEqual } from './storage/hmac-sha256';
36
+ export { SimSqlEngine, SimDbError } from './sim/sql-engine';
37
+ export type { SimTable, EngineStorage } from './sim/sql-engine';
38
+ export { SimRealtime, createSimRealtimeCapability, REALTIME_CODES } from './sim/realtime';
39
+ export type { SimRealtimeSubscription } from './sim/realtime';
35
40
  export { resolveFunctionSource, parseSnapshot } from './shared/function-source';
package/dist/index.js CHANGED
@@ -35,6 +35,20 @@ var DB_RPC = {
35
35
  /** 执行一条链:`chain, args=[ChainRequest]`。 */
36
36
  chain: "chain"
37
37
  };
38
+ var REALTIME_CAPABILITY_CODES = {
39
+ invalidMethod: "REALTIME_INVALID_METHOD",
40
+ unsupportedArg: "REALTIME_UNSUPPORTED_ARG",
41
+ subscriptionNotFound: "REALTIME_SUBSCRIPTION_NOT_FOUND",
42
+ tooManySubscriptions: "REALTIME_TOO_MANY_SUBSCRIPTIONS",
43
+ /** 线上独有:订阅授权策略(RT-002 `SubscriptionPolicy`)拒绝该 channel。 */
44
+ channelDenied: "REALTIME_CHANNEL_DENIED"
45
+ };
46
+ var REALTIME_CAPABILITY_METHODS = [
47
+ "publish",
48
+ "subscribe",
49
+ "receive",
50
+ "unsubscribe"
51
+ ];
38
52
 
39
53
  // packages/runtime/src/functions/runtime/worker-executor.ts
40
54
  import { readFileSync } from "node:fs";
@@ -301,9 +315,6 @@ var WorkerFunctionExecutor = class {
301
315
  }
302
316
  };
303
317
 
304
- // packages/runtime/src/database/sdk/cloud.ts
305
- import { randomUUID } from "node:crypto";
306
-
307
318
  // packages/runtime/src/database/builder/dialect.ts
308
319
  var sqliteDialect = {
309
320
  name: "sqlite",
@@ -485,7 +496,6 @@ async function readChanges(driver, table, query = {}) {
485
496
  }
486
497
 
487
498
  // packages/runtime/src/database/builder/ulid.ts
488
- import { randomFillSync } from "node:crypto";
489
499
  var ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
490
500
  var TIME_LEN = 10;
491
501
  var RANDOM_LEN = 16;
@@ -526,6 +536,16 @@ function incrBase32(prev) {
526
536
  }
527
537
  return chars.join("");
528
538
  }
539
+ function randomFill(bytes) {
540
+ const source = globalThis.crypto;
541
+ if (source !== void 0 && typeof source.getRandomValues === "function") {
542
+ return source.getRandomValues(bytes);
543
+ }
544
+ for (let i = 0; i < bytes.length; i += 1) {
545
+ bytes[i] = Math.floor(Math.random() * 256);
546
+ }
547
+ return bytes;
548
+ }
529
549
  function ulid(now = Date.now()) {
530
550
  const time = encodeTime(now);
531
551
  let random;
@@ -534,7 +554,7 @@ function ulid(now = Date.now()) {
534
554
  } else {
535
555
  lastTime = now;
536
556
  const bytes = new Uint8Array(10);
537
- randomFillSync(bytes);
557
+ randomFill(bytes);
538
558
  random = encodeRandom(bytes);
539
559
  }
540
560
  lastRandom = random;
@@ -982,7 +1002,7 @@ function createDbCapability(driver, options = {}) {
982
1002
  case DB_RPC.begin: {
983
1003
  if (activeTxs.size > 0) throw unsafeOperation("\u4E0D\u5141\u8BB8\u5D4C\u5957\u4E8B\u52A1");
984
1004
  await driver.run("BEGIN");
985
- const txId = randomUUID();
1005
+ const txId = crypto.randomUUID();
986
1006
  activeTxs.set(txId, true);
987
1007
  return txId;
988
1008
  }
@@ -1059,14 +1079,182 @@ function assertSafeStoragePath(path) {
1059
1079
  return path;
1060
1080
  }
1061
1081
 
1082
+ // packages/runtime/src/storage/hmac-sha256.ts
1083
+ var K = new Uint32Array([
1084
+ 1116352408,
1085
+ 1899447441,
1086
+ 3049323471,
1087
+ 3921009573,
1088
+ 961987163,
1089
+ 1508970993,
1090
+ 2453635748,
1091
+ 2870763221,
1092
+ 3624381080,
1093
+ 310598401,
1094
+ 607225278,
1095
+ 1426881987,
1096
+ 1925078388,
1097
+ 2162078206,
1098
+ 2614888103,
1099
+ 3248222580,
1100
+ 3835390401,
1101
+ 4022224774,
1102
+ 264347078,
1103
+ 604807628,
1104
+ 770255983,
1105
+ 1249150122,
1106
+ 1555081692,
1107
+ 1996064986,
1108
+ 2554220882,
1109
+ 2821834349,
1110
+ 2952996808,
1111
+ 3210313671,
1112
+ 3336571891,
1113
+ 3584528711,
1114
+ 113926993,
1115
+ 338241895,
1116
+ 666307205,
1117
+ 773529912,
1118
+ 1294757372,
1119
+ 1396182291,
1120
+ 1695183700,
1121
+ 1986661051,
1122
+ 2177026350,
1123
+ 2456956037,
1124
+ 2730485921,
1125
+ 2820302411,
1126
+ 3259730800,
1127
+ 3345764771,
1128
+ 3516065817,
1129
+ 3600352804,
1130
+ 4094571909,
1131
+ 275423344,
1132
+ 430227734,
1133
+ 506948616,
1134
+ 659060556,
1135
+ 883997877,
1136
+ 958139571,
1137
+ 1322822218,
1138
+ 1537002063,
1139
+ 1747873779,
1140
+ 1955562222,
1141
+ 2024104815,
1142
+ 2227730452,
1143
+ 2361852424,
1144
+ 2428436474,
1145
+ 2756734187,
1146
+ 3204031479,
1147
+ 3329325298
1148
+ ]);
1149
+ var rotr = (x, n) => x >>> n | x << 32 - n;
1150
+ function compress(h, block, w) {
1151
+ for (let i = 0; i < 16; i += 1) {
1152
+ const j = i * 4;
1153
+ w[i] = (block[j] ?? 0) << 24 | (block[j + 1] ?? 0) << 16 | (block[j + 2] ?? 0) << 8 | (block[j + 3] ?? 0);
1154
+ }
1155
+ for (let i = 16; i < 64; i += 1) {
1156
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
1157
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
1158
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 | 0;
1159
+ }
1160
+ let [a, b, c, d, e, f, g, hh] = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]];
1161
+ for (let i = 0; i < 64; i += 1) {
1162
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1163
+ const ch = e & f ^ ~e & g;
1164
+ const t1 = hh + S1 + ch + K[i] + w[i] | 0;
1165
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
1166
+ const maj = a & b ^ a & c ^ b & c;
1167
+ const t2 = S0 + maj | 0;
1168
+ hh = g;
1169
+ g = f;
1170
+ f = e;
1171
+ e = d + t1 | 0;
1172
+ d = c;
1173
+ c = b;
1174
+ b = a;
1175
+ a = t1 + t2 | 0;
1176
+ }
1177
+ h[0] = h[0] + a | 0;
1178
+ h[1] = h[1] + b | 0;
1179
+ h[2] = h[2] + c | 0;
1180
+ h[3] = h[3] + d | 0;
1181
+ h[4] = h[4] + e | 0;
1182
+ h[5] = h[5] + f | 0;
1183
+ h[6] = h[6] + g | 0;
1184
+ h[7] = h[7] + hh | 0;
1185
+ }
1186
+ function sha256(data) {
1187
+ const h = new Uint32Array([
1188
+ 1779033703,
1189
+ 3144134277,
1190
+ 1013904242,
1191
+ 2773480762,
1192
+ 1359893119,
1193
+ 2600822924,
1194
+ 528734635,
1195
+ 1541459225
1196
+ ]);
1197
+ const bitLength = data.length * 8;
1198
+ const paddedLength = Math.ceil((data.length + 9) / 64) * 64;
1199
+ const padded = new Uint8Array(paddedLength);
1200
+ padded.set(data);
1201
+ padded[data.length] = 128;
1202
+ const view = new DataView(padded.buffer);
1203
+ view.setUint32(paddedLength - 4, bitLength >>> 0, false);
1204
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296), false);
1205
+ const w = new Uint32Array(64);
1206
+ for (let offset = 0; offset < paddedLength; offset += 64) {
1207
+ compress(h, padded.subarray(offset, offset + 64), w);
1208
+ }
1209
+ const out = new Uint8Array(32);
1210
+ const outView = new DataView(out.buffer);
1211
+ for (let i = 0; i < 8; i += 1) outView.setUint32(i * 4, h[i], false);
1212
+ return out;
1213
+ }
1214
+ function toBytes(input) {
1215
+ if (typeof input !== "string") return input;
1216
+ return new TextEncoder().encode(input);
1217
+ }
1218
+ function hmacSha256(key, message) {
1219
+ const blockSize = 64;
1220
+ let keyBytes = toBytes(key);
1221
+ if (keyBytes.length > blockSize) keyBytes = sha256(keyBytes);
1222
+ const padded = new Uint8Array(blockSize);
1223
+ padded.set(keyBytes);
1224
+ const inner = new Uint8Array(blockSize);
1225
+ const outer = new Uint8Array(blockSize);
1226
+ for (let i = 0; i < blockSize; i += 1) {
1227
+ inner[i] = padded[i] ^ 54;
1228
+ outer[i] = padded[i] ^ 92;
1229
+ }
1230
+ const innerInput = new Uint8Array(blockSize + toBytes(message).length);
1231
+ innerInput.set(inner);
1232
+ innerInput.set(toBytes(message), blockSize);
1233
+ const innerHash = sha256(innerInput);
1234
+ const outerInput = new Uint8Array(blockSize + 32);
1235
+ outerInput.set(outer);
1236
+ outerInput.set(innerHash, blockSize);
1237
+ return sha256(outerInput);
1238
+ }
1239
+ function hmacSha256Hex(key, message) {
1240
+ return [...hmacSha256(key, message)].map((b) => b.toString(16).padStart(2, "0")).join("");
1241
+ }
1242
+ function constantTimeEqual(a, b) {
1243
+ if (a.length !== b.length) return false;
1244
+ let diff = 0;
1245
+ for (let i = 0; i < a.length; i += 1) {
1246
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1247
+ }
1248
+ return diff === 0;
1249
+ }
1250
+
1062
1251
  // packages/runtime/src/storage/signature.ts
1063
- import { createHmac, timingSafeEqual } from "node:crypto";
1064
1252
  var DEFAULT_SIGN_TTL_SECONDS = 15 * 60;
1065
1253
  var SIGN_EXPIRES_KEY = "x-expires";
1066
1254
  var SIGN_SIGNATURE_KEY = "x-signature";
1067
1255
  function sign(secret, projectId, path, expires) {
1068
1256
  const payload = `${projectId}|${path}|${expires}`;
1069
- return createHmac("sha256", secret).update(payload).digest("hex");
1257
+ return hmacSha256Hex(secret, payload);
1070
1258
  }
1071
1259
  function signDownloadUrl(config, projectId, path, ttlSeconds = DEFAULT_SIGN_TTL_SECONDS) {
1072
1260
  const expires = Math.floor(Date.now() / 1e3) + ttlSeconds;
@@ -1085,9 +1273,7 @@ function verifyDownloadSignature(config, projectId, path, query, nowSeconds = Ma
1085
1273
  if (!Number.isFinite(expires)) return { ok: false, reason: "malformed" };
1086
1274
  if (nowSeconds > expires) return { ok: false, reason: "expired" };
1087
1275
  const expected = sign(config.secret, projectId, path, expires);
1088
- const actual = Buffer.from(signatureRaw, "utf8");
1089
- const candidate = Buffer.from(expected, "utf8");
1090
- if (actual.length !== candidate.length || !timingSafeEqual(actual, candidate)) {
1276
+ if (!constantTimeEqual(signatureRaw, expected)) {
1091
1277
  return { ok: false, reason: "invalid" };
1092
1278
  }
1093
1279
  return { ok: true };
@@ -1359,6 +1545,546 @@ function createLocalStorageDriver(options) {
1359
1545
  };
1360
1546
  }
1361
1547
 
1548
+ // packages/runtime/src/sim/sql-engine.ts
1549
+ var SimDbError = class extends Error {
1550
+ constructor(code, message) {
1551
+ super(message);
1552
+ this.code = code;
1553
+ this.name = "SimDbError";
1554
+ }
1555
+ };
1556
+ var ParamCursor = class {
1557
+ constructor(params) {
1558
+ this.params = params;
1559
+ }
1560
+ index = 0;
1561
+ take() {
1562
+ const v = this.params[this.index];
1563
+ this.index += 1;
1564
+ return v ?? null;
1565
+ }
1566
+ };
1567
+ function ident(raw) {
1568
+ return raw.trim().replace(/^"|"$/g, "").trim();
1569
+ }
1570
+ function toNumber(raw) {
1571
+ const t = raw.trim();
1572
+ if (t === "?") return null;
1573
+ const n = Number(t);
1574
+ return Number.isNaN(n) ? null : n;
1575
+ }
1576
+ function stripQuotes(raw) {
1577
+ const t = raw.trim();
1578
+ if (/^'.*'$/.test(t)) return t.slice(1, -1);
1579
+ if (/^".*"$/.test(t)) return t.slice(1, -1);
1580
+ return t;
1581
+ }
1582
+ function parseCreateTable(ddl) {
1583
+ const m = /^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s*\(([\s\S]*)\)$/i.exec(
1584
+ ddl.trim()
1585
+ );
1586
+ if (m === null) return null;
1587
+ const name = ident(m[1]);
1588
+ const body = m[3];
1589
+ const columns = [];
1590
+ for (const part of body.split(",")) {
1591
+ const tokens = part.trim().split(/\s+/);
1592
+ const colName = ident(tokens[0] ?? "");
1593
+ const type = (tokens[1] ?? "TEXT").toUpperCase();
1594
+ const primaryKey = tokens.includes("PRIMARY") && tokens.includes("KEY");
1595
+ const autoincrement = primaryKey && tokens.includes("AUTOINCREMENT");
1596
+ columns.push({ name: colName, type, primaryKey, autoincrement });
1597
+ }
1598
+ return { name, columns };
1599
+ }
1600
+ function parseWhereClauses(whereRaw, cursor) {
1601
+ const clauses = [];
1602
+ for (const part of whereRaw.split(/\s+AND\s+/i)) {
1603
+ const t = part.trim();
1604
+ if (t.length === 0) continue;
1605
+ if (/\bIS\s+NULL\b/i.test(t)) {
1606
+ const col2 = ident(t.split(/\s+IS\s+NULL\b/i)[0] ?? "");
1607
+ clauses.push({ column: col2, operator: "IS NULL", value: null, list: false });
1608
+ continue;
1609
+ }
1610
+ if (/\bIS\s+NOT\s+NULL\b/i.test(t)) {
1611
+ const col2 = ident(t.split(/\s+IS\s+NOT\s+NULL\b/i)[0] ?? "");
1612
+ clauses.push({ column: col2, operator: "IS NOT NULL", value: null, list: false });
1613
+ continue;
1614
+ }
1615
+ const opMatch = /^\s*((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s*(=|!=|>=|<=|>|<|like|in|not\s+in)\s*(.+)$/i.exec(
1616
+ t
1617
+ );
1618
+ if (opMatch === null) continue;
1619
+ const col = ident(opMatch[1] ?? "");
1620
+ const op = (opMatch[3] ?? opMatch[2] ?? "").toLowerCase();
1621
+ const rhs = (opMatch[4] ?? "").trim();
1622
+ if (op === "in" || op === "not in") {
1623
+ const inner = rhs.replace(/^\(|\)$/g, "");
1624
+ const items = splitListItems(inner);
1625
+ const list = items.map((item) => {
1626
+ if (item.trim() === "?") return cursor.take();
1627
+ return stripQuotes(item);
1628
+ });
1629
+ clauses.push({ column: col, operator: op, value: list, list: true });
1630
+ continue;
1631
+ }
1632
+ let value;
1633
+ if (rhs === "?") {
1634
+ value = cursor.take();
1635
+ } else {
1636
+ value = stripQuotes(rhs);
1637
+ }
1638
+ clauses.push({ column: col, operator: op, value, list: false });
1639
+ }
1640
+ return clauses;
1641
+ }
1642
+ function matchWhere(clauses, row) {
1643
+ return clauses.every((c) => matchValue(row[c.column], c));
1644
+ }
1645
+ function splitListItems(inner) {
1646
+ const items = [];
1647
+ let depth = 0;
1648
+ let buffer = "";
1649
+ for (let i = 0; i < inner.length; i += 1) {
1650
+ const ch = inner[i];
1651
+ if (ch === "(") depth += 1;
1652
+ else if (ch === ")") depth -= 1;
1653
+ if (ch === "," && depth === 0) {
1654
+ items.push(buffer);
1655
+ buffer = "";
1656
+ } else {
1657
+ buffer += ch;
1658
+ }
1659
+ }
1660
+ items.push(buffer);
1661
+ return items;
1662
+ }
1663
+ function matchValue(value, clause) {
1664
+ const op = clause.operator;
1665
+ if (op === "IS NULL") return value === null || value === void 0;
1666
+ if (op === "IS NOT NULL") return value !== null && value !== void 0;
1667
+ if (op === "in") return clause.value.some((item) => value === item);
1668
+ if (op === "not in") return !clause.value.some((item) => value === item);
1669
+ if (op === "like") {
1670
+ if (typeof value !== "string") return false;
1671
+ const pattern = String(clause.value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
1672
+ return new RegExp(`^${pattern}$`, "s").test(value);
1673
+ }
1674
+ const left = value;
1675
+ const right = clause.value;
1676
+ switch (op) {
1677
+ case "=":
1678
+ return left !== null && right !== null && left === right;
1679
+ case "!=":
1680
+ return left !== null && right !== null && left !== right;
1681
+ case ">":
1682
+ return left !== null && right !== null && left > right;
1683
+ case ">=":
1684
+ return left !== null && right !== null && left >= right;
1685
+ case "<":
1686
+ return left !== null && right !== null && left < right;
1687
+ case "<=":
1688
+ return left !== null && right !== null && left <= right;
1689
+ default:
1690
+ return false;
1691
+ }
1692
+ }
1693
+ function assertReadOnly(sql) {
1694
+ const stripped = sql.replace(/'[^']*(?:''[^']*)*'/g, "").replace(/"[^"]*(?:""[^"]*)*"/g, "").replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1695
+ if (stripped.includes(";")) throw new SimDbError("DB_UNSAFE_OP", "\u4EC5\u5141\u8BB8\u5355\u6761\u8BED\u53E5\uFF0C\u7981\u6B62\u591A\u8BED\u53E5\u5806\u53E0");
1696
+ const head = stripped.trim().replace(/^\(+/, "").trim().toUpperCase();
1697
+ if (!head.startsWith("SELECT") && !head.startsWith("WITH")) {
1698
+ throw new SimDbError("DB_UNSAFE_OP", "cloud.db.query \u4EC5\u5141\u8BB8\u53EA\u8BFB\u67E5\u8BE2\uFF08SELECT / WITH\uFF09");
1699
+ }
1700
+ if (/ATTACH|DETACH/.test(stripped.toUpperCase())) {
1701
+ throw new SimDbError("DB_UNSAFE_OP", "\u7981\u6B62\u8DE8\u5E93\u64CD\u4F5C\uFF08ATTACH / DETACH\uFF09");
1702
+ }
1703
+ if (/SQLITE_\w+/i.test(stripped) && !/SQLITE_MASTER\b/i.test(stripped)) {
1704
+ throw new SimDbError("DB_UNSAFE_OP", "\u7981\u6B62\u8BBF\u95EE\u7CFB\u7EDF\u8868\uFF08sqlite_*\uFF09");
1705
+ }
1706
+ }
1707
+ var SimSqlEngine = class {
1708
+ tables = {};
1709
+ storage;
1710
+ memoryOnly;
1711
+ savepointDepth = 0;
1712
+ txSnapshot = null;
1713
+ constructor(options = {}) {
1714
+ this.storage = options.storage ?? { load: async () => null, save: async () => void 0 };
1715
+ this.memoryOnly = options.memoryOnly ?? false;
1716
+ }
1717
+ async load() {
1718
+ if (this.memoryOnly) return;
1719
+ const persisted = await this.storage.load();
1720
+ if (persisted !== null) this.tables = persisted;
1721
+ }
1722
+ async persist() {
1723
+ if (this.memoryOnly) return;
1724
+ await this.storage.save(this.tables);
1725
+ }
1726
+ ensureTable(name) {
1727
+ let table = this.tables[name];
1728
+ if (table === void 0) {
1729
+ table = { columns: [], rows: [], nextAutoincrement: 1 };
1730
+ this.tables[name] = table;
1731
+ }
1732
+ return table;
1733
+ }
1734
+ applyAutoincrement(table, row) {
1735
+ const pk = table.columns.find((c) => c.primaryKey && c.autoincrement);
1736
+ if (pk === void 0) return;
1737
+ const val = row[pk.name];
1738
+ if (val !== null && val !== void 0) {
1739
+ const n = Number(val);
1740
+ if (Number.isInteger(n) && n >= table.nextAutoincrement) table.nextAutoincrement = n + 1;
1741
+ return;
1742
+ }
1743
+ row[pk.name] = table.nextAutoincrement;
1744
+ table.nextAutoincrement += 1;
1745
+ }
1746
+ catalog() {
1747
+ const out = [];
1748
+ for (const [name] of Object.entries(this.tables)) {
1749
+ out.push({ type: "table", name, tbl_name: name });
1750
+ }
1751
+ return out;
1752
+ }
1753
+ async run(sql, params = []) {
1754
+ const stmt = sql.trim();
1755
+ if (/^BEGIN\s*$/i.test(stmt)) {
1756
+ this.txSnapshot = structuredClone(this.tables);
1757
+ return { changes: 0 };
1758
+ }
1759
+ if (/^COMMIT\s*$/i.test(stmt)) {
1760
+ this.txSnapshot = null;
1761
+ await this.persist();
1762
+ return { changes: 0 };
1763
+ }
1764
+ if (/^ROLLBACK\s*$/i.test(stmt)) {
1765
+ if (this.txSnapshot !== null) {
1766
+ this.tables = this.txSnapshot;
1767
+ this.txSnapshot = null;
1768
+ }
1769
+ await this.persist();
1770
+ return { changes: 0 };
1771
+ }
1772
+ if (/^SAVEPOINT\s+/i.test(stmt)) {
1773
+ this.savepointDepth += 1;
1774
+ return { changes: 0 };
1775
+ }
1776
+ if (/^RELEASE\s+SAVEPOINT\s+/i.test(stmt)) {
1777
+ this.savepointDepth = Math.max(0, this.savepointDepth - 1);
1778
+ return { changes: 0 };
1779
+ }
1780
+ if (/^ROLLBACK\s+TO\s+SAVEPOINT\s+/i.test(stmt)) return { changes: 0 };
1781
+ if (/^CREATE\s+TABLE/i.test(stmt)) {
1782
+ const parsed = parseCreateTable(stmt);
1783
+ if (parsed === null) throw new SimDbError("DB_UNSAFE_OP", `\u65E0\u6CD5\u89E3\u6790\u5EFA\u8868\u8BED\u53E5`);
1784
+ if (this.tables[parsed.name] === void 0) {
1785
+ this.tables[parsed.name] = { columns: parsed.columns, rows: [], nextAutoincrement: 1 };
1786
+ await this.persist();
1787
+ }
1788
+ return { changes: 0 };
1789
+ }
1790
+ if (/^INSERT\s+INTO/i.test(stmt)) return this.execInsert(stmt, params);
1791
+ if (/^UPDATE\s+/i.test(stmt)) return this.execUpdate(stmt, params);
1792
+ if (/^DELETE\s+FROM/i.test(stmt)) return this.execDelete(stmt, params);
1793
+ throw new SimDbError("DB_UNSAFE_OP", `sim SQL \u5F15\u64CE\u4E0D\u652F\u6301\u8BE5\u8BED\u53E5`);
1794
+ }
1795
+ execInsert(sql, params) {
1796
+ const match = /^INSERT\s+INTO\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s*\(([^)]*)\)\s*VALUES\s*([\s\S]+)$/i.exec(
1797
+ sql
1798
+ );
1799
+ if (match === null) throw new SimDbError("DB_UNSAFE_OP", "\u65E0\u6CD5\u89E3\u6790 INSERT");
1800
+ const tableName = ident(match[1]);
1801
+ const cols = match[3].split(",").map(ident);
1802
+ const valueBody = match[4].trim();
1803
+ const cursor = new ParamCursor(params);
1804
+ const table = this.ensureTable(tableName);
1805
+ let inserted = 0;
1806
+ for (const group of splitListItems(valueBody)) {
1807
+ const inner = group.trim().replace(/^\(|\)$/g, "");
1808
+ const values = splitListItems(inner).map(
1809
+ (item) => item.trim() === "?" ? cursor.take() : stripQuotes(item)
1810
+ );
1811
+ const row = {};
1812
+ cols.forEach((col, i) => {
1813
+ row[col] = values[i] ?? null;
1814
+ });
1815
+ this.applyAutoincrement(table, row);
1816
+ table.rows.push(row);
1817
+ inserted += 1;
1818
+ }
1819
+ void this.persist();
1820
+ return { changes: inserted };
1821
+ }
1822
+ execUpdate(sql, params) {
1823
+ const match = /^UPDATE\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+SET\s+([\s\S]+?)\s+WHERE\s+([\s\S]+)$/i.exec(
1824
+ sql
1825
+ );
1826
+ if (match === null) throw new SimDbError("DB_UNSAFE_OP", "UPDATE \u5FC5\u987B\u5E26 WHERE");
1827
+ const tableName = ident(match[1]);
1828
+ const setRaw = match[3];
1829
+ const whereRaw = match[4];
1830
+ const cursor = new ParamCursor(params);
1831
+ const sets = splitListItems(setRaw).map((part) => {
1832
+ const [col, , mark] = part.trim().split(/\s+/);
1833
+ return { col: ident(col ?? ""), mark: mark ?? "?" };
1834
+ });
1835
+ const table = this.ensureTable(tableName);
1836
+ const boundSets = sets.map((set) => ({
1837
+ col: set.col,
1838
+ value: set.mark === "?" ? cursor.take() : stripQuotes(set.mark)
1839
+ }));
1840
+ const whereClauses = parseWhereClauses(whereRaw, cursor);
1841
+ let changes = 0;
1842
+ for (const row of table.rows) {
1843
+ if (!matchWhere(whereClauses, row)) continue;
1844
+ for (const set of boundSets) {
1845
+ row[set.col] = set.value;
1846
+ }
1847
+ changes += 1;
1848
+ }
1849
+ void this.persist();
1850
+ return { changes };
1851
+ }
1852
+ execDelete(sql, params) {
1853
+ const match = /^DELETE\s+FROM\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+WHERE\s+([\s\S]+)$/i.exec(sql);
1854
+ if (match === null) throw new SimDbError("DB_UNSAFE_OP", "DELETE \u5FC5\u987B\u5E26 WHERE");
1855
+ const tableName = ident(match[1]);
1856
+ const whereRaw = match[3];
1857
+ const cursor = new ParamCursor(params);
1858
+ const table = this.ensureTable(tableName);
1859
+ const whereClauses = parseWhereClauses(whereRaw, cursor);
1860
+ const keep = [];
1861
+ let changes = 0;
1862
+ for (const row of table.rows) {
1863
+ if (!matchWhere(whereClauses, row)) keep.push(row);
1864
+ else changes += 1;
1865
+ }
1866
+ table.rows = keep;
1867
+ void this.persist();
1868
+ return { changes };
1869
+ }
1870
+ select(sql, params) {
1871
+ const m = /^SELECT\s+([\s\S]+?)\s+FROM\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))([\s\S]*)$/i.exec(
1872
+ sql.trim()
1873
+ );
1874
+ if (m === null) throw new SimDbError("DB_UNSAFE_OP", "\u65E0\u6CD5\u89E3\u6790 SELECT");
1875
+ const selectRaw = m[1].trim();
1876
+ const tableName = ident(m[2]);
1877
+ const tail = m[4] ?? "";
1878
+ const cursor = new ParamCursor(params);
1879
+ if (tableName.toLowerCase() === "sqlite_master") {
1880
+ return project(this.catalog(), selectRaw);
1881
+ }
1882
+ const table = this.ensureTable(tableName);
1883
+ const whereMatch = /\bWHERE\b/i.exec(tail);
1884
+ const orderMatch = /\bORDER\s+BY\b/i.exec(tail);
1885
+ const limitMatch = /\bLIMIT\b/i.exec(tail);
1886
+ const offsetMatch = /\bOFFSET\b/i.exec(tail);
1887
+ const whereRaw = whereMatch === null ? "" : tail.slice(
1888
+ whereMatch.index + whereMatch[0].length,
1889
+ indexAfter(whereMatch.index, [orderMatch, limitMatch, offsetMatch], tail)
1890
+ );
1891
+ let rows = table.rows;
1892
+ if (whereRaw.trim().length > 0) {
1893
+ const clauses = parseWhereClauses(whereRaw, cursor);
1894
+ rows = rows.filter((row) => matchWhere(clauses, row));
1895
+ }
1896
+ if (orderMatch !== null) {
1897
+ const orderRaw = tail.slice(
1898
+ orderMatch.index + orderMatch[0].length,
1899
+ indexAfter(orderMatch.index, [limitMatch, offsetMatch], tail)
1900
+ );
1901
+ const oc = /^\s*((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+(asc|desc)/i.exec(orderRaw);
1902
+ if (oc !== null) {
1903
+ const col = ident(oc[1]);
1904
+ const dir = oc[3].toLowerCase();
1905
+ rows = [...rows].toSorted((a, b) => {
1906
+ const av = a[col];
1907
+ const bv = b[col];
1908
+ const cmp = av === bv ? 0 : av === null ? -1 : bv === null ? 1 : av > bv ? 1 : -1;
1909
+ return dir === "desc" ? -cmp : cmp;
1910
+ });
1911
+ }
1912
+ }
1913
+ if (limitMatch !== null) {
1914
+ const limRaw = tail.slice(
1915
+ limitMatch.index + limitMatch[0].length,
1916
+ indexAfter(limitMatch.index, [offsetMatch], tail)
1917
+ );
1918
+ const lim = limRaw.trim() === "?" ? cursor.take() : toNumber(limRaw);
1919
+ if (typeof lim === "number") rows = rows.slice(0, lim);
1920
+ }
1921
+ if (offsetMatch !== null) {
1922
+ const offRaw = tail.slice(
1923
+ offsetMatch.index + offsetMatch[0].length,
1924
+ indexAfter(offsetMatch.index, [limitMatch], tail)
1925
+ );
1926
+ const off = offRaw.trim() === "?" ? cursor.take() : toNumber(offRaw);
1927
+ if (typeof off === "number") rows = rows.slice(off);
1928
+ }
1929
+ return project(rows, selectRaw);
1930
+ }
1931
+ async all(sql, params = []) {
1932
+ assertReadOnly(sql);
1933
+ return this.select(sql, params);
1934
+ }
1935
+ async get(sql, params = []) {
1936
+ assertReadOnly(sql);
1937
+ return this.select(sql, params)[0] ?? null;
1938
+ }
1939
+ async close() {
1940
+ void this.savepointDepth;
1941
+ }
1942
+ };
1943
+ function indexAfter(start, matches, tail) {
1944
+ const candidates = matches.filter((x) => x !== null && x.index > start).map((x) => x.index);
1945
+ const end = candidates.length === 0 ? -1 : Math.min(...candidates);
1946
+ return end === -1 ? tail.length : end;
1947
+ }
1948
+ function project(rows, selectRaw) {
1949
+ if (/^count\s*\(\s*\*/i.test(selectRaw) || /^count\s*\(\s*1\)/i.test(selectRaw)) {
1950
+ const alias = selectRaw.match(/\bAS\s+([A-Za-z_][A-Za-z0-9_]*)/i)?.[1] ?? "n";
1951
+ return [{ [alias]: rows.length }];
1952
+ }
1953
+ const constMatch = /^(\d+)\s+AS\s+([A-Za-z_][A-Za-z0-9_]*)/i.exec(selectRaw);
1954
+ if (constMatch !== null) {
1955
+ return [{ [constMatch[2]]: Number(constMatch[1]) }];
1956
+ }
1957
+ if (selectRaw.trim() === "*") return rows;
1958
+ const cols = selectRaw.split(",").map((s) => ident(s.trim()));
1959
+ return rows.map((row) => {
1960
+ const out = {};
1961
+ for (const col of cols) out[col] = row[col];
1962
+ return out;
1963
+ });
1964
+ }
1965
+
1966
+ // packages/runtime/src/sim/realtime.ts
1967
+ var SimRealtime = class {
1968
+ seq = 0;
1969
+ subscribers = /* @__PURE__ */ new Map();
1970
+ /** 向某 channel 广播一条消息;返回被投递的连接数(单进程内)。 */
1971
+ publish(channel, data, publisher) {
1972
+ this.seq += 1;
1973
+ const message = {
1974
+ channel,
1975
+ seq: this.seq,
1976
+ data,
1977
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1978
+ ...publisher === void 0 ? {} : { publisher }
1979
+ };
1980
+ const set = this.subscribers.get(channel);
1981
+ if (set === void 0) return { delivered: 0 };
1982
+ let delivered = 0;
1983
+ for (const listener of set) {
1984
+ listener(message);
1985
+ delivered += 1;
1986
+ }
1987
+ return { delivered };
1988
+ }
1989
+ /** 订阅某 channel;返回退订函数。 */
1990
+ subscribe(channel, listener) {
1991
+ let set = this.subscribers.get(channel);
1992
+ if (set === void 0) {
1993
+ set = /* @__PURE__ */ new Set();
1994
+ this.subscribers.set(channel, set);
1995
+ }
1996
+ set.add(listener);
1997
+ return () => {
1998
+ set?.delete(listener);
1999
+ };
2000
+ }
2001
+ /** 当前有订阅者的 channel 数(调试用)。 */
2002
+ channelCount() {
2003
+ return this.subscribers.size;
2004
+ }
2005
+ };
2006
+ var MAX_BUFFERED_MESSAGES = 64;
2007
+ var MAX_SUBSCRIPTIONS = 64;
2008
+ var REALTIME_CODES = REALTIME_CAPABILITY_CODES;
2009
+ function realtimeError(code, message) {
2010
+ return new Error(`[${code}] ${message}`);
2011
+ }
2012
+ function createSimRealtimeCapability() {
2013
+ const sim = new SimRealtime();
2014
+ const buffers = /* @__PURE__ */ new Map();
2015
+ const disposers = /* @__PURE__ */ new Map();
2016
+ let seq = 0;
2017
+ const handler = async (method, args) => {
2018
+ switch (method) {
2019
+ case "publish": {
2020
+ if (args.length > 2) {
2021
+ throw realtimeError(
2022
+ REALTIME_CODES.unsupportedArg,
2023
+ "\u672C\u5730\u5355\u8FDB\u7A0B\u5185\u5B58\u5E7F\u64AD\u65E0\u8FDE\u63A5\u8EAB\u4EFD\uFF0C\u4E0D\u652F\u6301 publish \u7684 except \u5B9E\u53C2"
2024
+ );
2025
+ }
2026
+ const [channel, data] = args;
2027
+ return sim.publish(channel, data);
2028
+ }
2029
+ case "subscribe": {
2030
+ if (buffers.size >= MAX_SUBSCRIPTIONS) {
2031
+ throw realtimeError(
2032
+ REALTIME_CODES.tooManySubscriptions,
2033
+ `\u672C\u5730\u8BA2\u9605\u6570\u5DF2\u8FBE\u4E0A\u9650 ${String(MAX_SUBSCRIPTIONS)}\uFF0C\u8BF7\u5148 unsubscribe`
2034
+ );
2035
+ }
2036
+ const [channel] = args;
2037
+ seq += 1;
2038
+ const id = `sub-${String(seq)}`;
2039
+ const buffer = [];
2040
+ const off = sim.subscribe(channel, (msg) => {
2041
+ buffer.push(msg);
2042
+ if (buffer.length > MAX_BUFFERED_MESSAGES) buffer.shift();
2043
+ });
2044
+ buffers.set(id, buffer);
2045
+ disposers.set(id, off);
2046
+ return { subscription: id, channel };
2047
+ }
2048
+ case "receive": {
2049
+ const buffer = buffers.get(args[0]);
2050
+ if (buffer === void 0) {
2051
+ throw realtimeError(
2052
+ REALTIME_CODES.subscriptionNotFound,
2053
+ `\u672C\u5730\u8BA2\u9605 "${String(args[0])}" \u4E0D\u5B58\u5728\u6216\u5DF2\u9000\u8BA2`
2054
+ );
2055
+ }
2056
+ return buffer.splice(0, buffer.length);
2057
+ }
2058
+ case "unsubscribe": {
2059
+ const id = args[0];
2060
+ const off = disposers.get(id);
2061
+ if (off === void 0) {
2062
+ throw realtimeError(
2063
+ REALTIME_CODES.subscriptionNotFound,
2064
+ `\u672C\u5730\u8BA2\u9605 "${id}" \u4E0D\u5B58\u5728\u6216\u5DF2\u9000\u8BA2`
2065
+ );
2066
+ }
2067
+ off();
2068
+ disposers.delete(id);
2069
+ buffers.delete(id);
2070
+ return { removed: true };
2071
+ }
2072
+ default:
2073
+ throw realtimeError(
2074
+ REALTIME_CODES.invalidMethod,
2075
+ `\u672A\u77E5\u7684 cloud.realtime \u65B9\u6CD5 "${method}"\uFF08\u53EF\u7528\uFF1A${REALTIME_CAPABILITY_METHODS.join(" / ")}\uFF09`
2076
+ );
2077
+ }
2078
+ };
2079
+ return {
2080
+ bundle: {
2081
+ capabilities: [{ name: "realtime", value: { [RPC_CAPABILITY_KEY]: true } }],
2082
+ rpcHandlers: { realtime: handler }
2083
+ },
2084
+ realtime: sim
2085
+ };
2086
+ }
2087
+
1362
2088
  // packages/runtime/src/shared/function-source.ts
1363
2089
  async function resolveFunctionSource(db, fn) {
1364
2090
  const readDraft = async () => {
@@ -1381,17 +2107,26 @@ export {
1381
2107
  DEFAULT_TIMEOUT_MS,
1382
2108
  ExecutorError,
1383
2109
  OOM_CODE,
2110
+ REALTIME_CODES,
1384
2111
  RPC_CAPABILITY_KEY,
1385
2112
  STORAGE_CODES,
2113
+ SimDbError,
2114
+ SimRealtime,
2115
+ SimSqlEngine,
1386
2116
  StorageError,
1387
2117
  TIMEOUT_CODE,
1388
2118
  WorkerFunctionExecutor,
2119
+ constantTimeEqual,
1389
2120
  createCloudDb,
1390
2121
  createDbCapability,
1391
2122
  createLocalStorageDriver,
2123
+ createSimRealtimeCapability,
1392
2124
  createStorageCapability,
2125
+ hmacSha256,
2126
+ hmacSha256Hex,
1393
2127
  parseSnapshot,
1394
2128
  resolveFunctionSource,
2129
+ sha256,
1395
2130
  signDownloadUrl,
1396
2131
  verifyDownloadSignature
1397
2132
  };
@@ -25,3 +25,19 @@ export declare const DB_RPC: {
25
25
  /** 执行一条链:`chain, args=[ChainRequest]`。 */
26
26
  readonly chain: "chain";
27
27
  };
28
+ /**
29
+ * `cloud.realtime` 的跨 realm 错误码(FN-020):与 `STORAGE_*` / `DB_*` 同族,装在 message 前缀里
30
+ * (worker 只回传 `error.message`)。本地 sim(CLI-007)与线上提供方**共用这一份**——
31
+ * 两侧各写一份时,函数里 `catch` 的错误码分支会在上线后静默走空。
32
+ */
33
+ export declare const REALTIME_CAPABILITY_CODES: {
34
+ readonly invalidMethod: "REALTIME_INVALID_METHOD";
35
+ readonly unsupportedArg: "REALTIME_UNSUPPORTED_ARG";
36
+ readonly subscriptionNotFound: "REALTIME_SUBSCRIPTION_NOT_FOUND";
37
+ readonly tooManySubscriptions: "REALTIME_TOO_MANY_SUBSCRIPTIONS";
38
+ /** 线上独有:订阅授权策略(RT-002 `SubscriptionPolicy`)拒绝该 channel。 */
39
+ readonly channelDenied: "REALTIME_CHANNEL_DENIED";
40
+ };
41
+ /** 命令通道可派发的 realtime 方法名(未知方法一律 fail loud)。 */
42
+ export declare const REALTIME_CAPABILITY_METHODS: readonly ["publish", "subscribe", "receive", "unsubscribe"];
43
+ export type RealtimeCapabilityMethod = (typeof REALTIME_CAPABILITY_METHODS)[number];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * SimRealtime(CLI-004)+ `cloud.realtime` 能力装配(CLI-007):单进程内存广播。
3
+ *
4
+ * 以 `RealtimeChannelMessage`(`@adep/types`)为对外形状;单实例语义与 PRD §3.2.9
5
+ * 硬约束 5 一致:**不支持跨进程 / 跨实例广播**。这是模拟运行时的**能力边界**之一。
6
+ *
7
+ * `createSimRealtimeCapability` 把本类装成 `CapabilityBundle`(与 sim db / storage 同款形状:
8
+ * 能力值以 `RPC_CAPABILITY_KEY` 占位、实现在宿主侧 `rpcHandlers.realtime`),因此函数里
9
+ * `await ctx.cloud.realtime.publish(...)` 在 `adep dev` 下可达。本地不建 WS 握手(无连接身份),
10
+ * 函数侧订阅采用**拉取式缓冲**:`subscribe` 注册一条进程内监听并把命中消息留在宿主缓冲里,
11
+ * 函数用 `receive` 取回,`unsubscribe` 退订。测试 / CLI 也可直接调用类上的 `publish` / `subscribe`。
12
+ *
13
+ * **FN-023 移植说明**:本文件自 `packages/cli/src/sim/realtime.ts` 复制进 runtime(CLI 域不在
14
+ * 该单 touches,CLI 侧暂留自有副本)。两份副本的语义一致性由 `shared/sim-contract.ts` 的
15
+ * realtime 期望表双向钉死(CLI 测试 + 浏览器驱动测试各跑一遍);CLI 换口消费本模块留给
16
+ * CLI 域后续单。浏览器侧(`@adep/web-container`)在本装配之上叠加「回网切真实 WS」的
17
+ * 传输切换,语义层(广播 / 缓冲 / 错误码)不动。
18
+ */
19
+ import type { CapabilityBundle, RealtimeChannelMessage } from '@adep/types';
20
+ export declare class SimRealtime {
21
+ private seq;
22
+ private readonly subscribers;
23
+ /** 向某 channel 广播一条消息;返回被投递的连接数(单进程内)。 */
24
+ publish(channel: string, data: unknown, publisher?: string): {
25
+ delivered: number;
26
+ };
27
+ /** 订阅某 channel;返回退订函数。 */
28
+ subscribe(channel: string, listener: (msg: RealtimeChannelMessage) => void): () => void;
29
+ /** 当前有订阅者的 channel 数(调试用)。 */
30
+ channelCount(): number;
31
+ }
32
+ /**
33
+ * 错误码自 FN-020 起取自 `@adep/runtime/shared/capability-keys`(本地与线上共用同一份)。
34
+ * 保留 `REALTIME_CODES` 这个名字与导出,既有 sim 测试与调用点无需改动。
35
+ */
36
+ export declare const REALTIME_CODES: {
37
+ readonly invalidMethod: "REALTIME_INVALID_METHOD";
38
+ readonly unsupportedArg: "REALTIME_UNSUPPORTED_ARG";
39
+ readonly subscriptionNotFound: "REALTIME_SUBSCRIPTION_NOT_FOUND";
40
+ readonly tooManySubscriptions: "REALTIME_TOO_MANY_SUBSCRIPTIONS";
41
+ readonly channelDenied: "REALTIME_CHANNEL_DENIED";
42
+ };
43
+ /** `subscribe` 的回执(函数侧只拿到订阅号,拿不到监听器 / 总线内部对象)。 */
44
+ export interface SimRealtimeSubscription {
45
+ subscription: string;
46
+ channel: string;
47
+ }
48
+ /**
49
+ * 装配 `cloud.realtime` 能力 bundle(`capabilities[].name = 'realtime'` + `rpcHandlers.realtime`),
50
+ * 并暴露其背后的进程内广播实例(宿主可直接 `subscribe` 观察函数广播)。
51
+ *
52
+ * 方法面(全部经命令通道转发回宿主,返回 Promise):
53
+ * - `publish(channel, data)` → `{ delivered }`:向 channel 内存广播,返回命中订阅者数;
54
+ * - `subscribe(channel)` → `{ subscription, channel }`:注册进程内监听,命中消息留在宿主缓冲;
55
+ * - `receive(subscription)` → `RealtimeChannelMessage[]`:取回并清空缓冲(非阻塞);
56
+ * - `unsubscribe(subscription)` → `{ removed: true }`:退订并丢弃缓冲。
57
+ *
58
+ * 本地**没有**线上的连接身份,故 PRD §2.12.3 的 `except`(不回给发送者)无从实现——
59
+ * 传第三个实参即 fail loud 报错,不静默改变语义。
60
+ */
61
+ export declare function createSimRealtimeCapability(): {
62
+ bundle: CapabilityBundle;
63
+ realtime: SimRealtime;
64
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 本地模拟 SQL 引擎(CLI-004):进程内、零外部依赖的 MINI-SQL。
3
+ *
4
+ * 设计动因:`adep dev` 要把 `cloud.db` 能力装进函数执行环境,而 `createDbCapability`
5
+ * (线上 Provider,只读复用)消费一个 `ProjectDriver`(run/all/get/close + engine)。
6
+ * 本引擎实现该驱动所需的「有界 SQL 子集」,恰好覆盖线上 `createCloudDb` 编译器产出的语句形态。
7
+ *
8
+ * **参数化一致性核心**:线上 `compileUpdate`/`compileDelete`/`compileInsert` 的 params 数组顺序
9
+ * 与 SQL 文本里 `?` 占位符的**出现顺序**严格一致(编译器边拼文本边 push 参数)。本引擎统一采用
10
+ * 「从左到右扫描文本、按顺序消费 params」的方式,因此无论 WHERE / SET / VALUES 谁先谁后,
11
+ * 解析顺序都与 params 数组天然对齐——删掉了按子句手工记账的出错面。
12
+ *
13
+ * 覆盖语句:
14
+ * - DDL:`CREATE TABLE [IF NOT EXISTS] name (col TYPE [PRIMARY KEY] [AUTOINCREMENT] [, ...])`;
15
+ * - SELECT:`SELECT cols|count(*)|常量 FROM t [WHERE ...] [ORDER BY col asc|desc] [LIMIT ?] [OFFSET ?]`;
16
+ * `sqlite_master` 探测(owned 变更流表存在性)由特殊分支返回目录;
17
+ * - INSERT:`INSERT INTO t (cols) VALUES (?,...) [, (?,...)]`;
18
+ * - UPDATE:`UPDATE t SET col = ? [...] WHERE ...`;
19
+ * - DELETE:`DELETE FROM t WHERE ...`;
20
+ * - 事务:`BEGIN` / `COMMIT` / `ROLLBACK` / `SAVEPOINT x` / `RELEASE SAVEPOINT x` / `ROLLBACK TO SAVEPOINT x`;
21
+ * - WHERE:`col op ?`(op ∈ = != > >= < <= like in not in);`= ?(null)` → IS NULL、`!= ?(null)` → IS NOT NULL。
22
+ *
23
+ * 一致性承诺:本引擎只在「持久化落点」与线上 sqlite 不同,SQL 语义(CRUD、where 操作符、
24
+ * orderBy/limit、owned 变更流、事务回滚)逐条对齐线上 builder 编译产物——契约表由
25
+ * `shared/sim-contract.ts` 钉死,sim 与线上 Provider 跑同一张表。
26
+ *
27
+ * **FN-023 移植说明**:本文件自 `packages/cli/src/sim/sql-engine.ts` 复制进 runtime
28
+ * (CLI 域不在该单 touches,CLI 侧暂留自有副本)。两份副本的语义一致性由契约表双向钉死
29
+ * (CLI 测试 + 浏览器驱动测试各跑一遍);CLI 换口消费本模块留给 CLI 域后续单。
30
+ * 移植至此的原因:浏览器侧模拟运行时(`@adep/web-container`)复用 `createDbCapability`
31
+ * 时需要同一份「进程内 SQL 引擎」,而 CLI 包不能进浏览器 bundle(`adep` 命令族 / node 依赖)。
32
+ */
33
+ /** 表列定义(从 CREATE TABLE 语句解析)。 */
34
+ interface SimColumn {
35
+ name: string;
36
+ type: string;
37
+ primaryKey: boolean;
38
+ autoincrement: boolean;
39
+ }
40
+ /** 单表状态:列 + 行。 */
41
+ export interface SimTable {
42
+ columns: SimColumn[];
43
+ rows: Array<Record<string, unknown>>;
44
+ nextAutoincrement: number;
45
+ }
46
+ /** 持久层宿主接口(JSON 文件 / 内存)。 */
47
+ export interface EngineStorage {
48
+ load(): Promise<Record<string, SimTable> | null>;
49
+ save(tables: Record<string, SimTable>): Promise<void>;
50
+ }
51
+ /** 本地模拟 DB 统一错误(code 用平台码,供错误信封一致)。 */
52
+ export declare class SimDbError extends Error {
53
+ readonly code: string;
54
+ constructor(code: string, message: string);
55
+ }
56
+ export declare class SimSqlEngine {
57
+ private tables;
58
+ private storage;
59
+ private memoryOnly;
60
+ private savepointDepth;
61
+ private txSnapshot;
62
+ constructor(options?: {
63
+ storage?: EngineStorage;
64
+ memoryOnly?: boolean;
65
+ });
66
+ load(): Promise<void>;
67
+ private persist;
68
+ private ensureTable;
69
+ private applyAutoincrement;
70
+ private catalog;
71
+ run(sql: string, params?: readonly unknown[]): Promise<{
72
+ changes: number;
73
+ }>;
74
+ private execInsert;
75
+ private execUpdate;
76
+ private execDelete;
77
+ private select;
78
+ all(sql: string, params?: readonly unknown[]): Promise<Array<Record<string, unknown>>>;
79
+ get(sql: string, params?: readonly unknown[]): Promise<Record<string, unknown> | null>;
80
+ close(): Promise<void>;
81
+ }
82
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 纯 TS HMAC-SHA256(FN-023):跨 realm(浏览器 / Node / Bun)同形,零 node 依赖。
3
+ *
4
+ * 动因:`storage/signature.ts` 的签名 URL 算法原走 `node:crypto.createHmac`,浏览器侧
5
+ * 模拟运行时(`@adep/web-container` 的 sim storage)复用 `createStorageCapability` 时
6
+ * 打包直接失败。与其在浏览器再造一份「形状一致但算法不同」的签名,不如把 HMAC 本体
7
+ * 收敛为单一实现——所有 realm 跑同一份代码,签名逐字节可比(对拍测试钉住与
8
+ * `node:crypto` 的一致性,见 `__tests__/signature.test.ts`)。
9
+ *
10
+ * 实现是教科书式 FIPS 180-4 / RFC 2104:无 SIMD 优化、无流式 API——本模块的调用面只有
11
+ * 「一次签一个短 payload(项目 + 路径 + 过期时间)」,常数级正确性优先于大数据吞吐。
12
+ */
13
+ /** SHA-256 摘要(32 字节)。输入一次性给全(本模块的调用面没有流式需求)。 */
14
+ export declare function sha256(data: Uint8Array): Uint8Array;
15
+ /** HMAC-SHA256(RFC 2104):key 超过块长先哈希,短 key 左侧补零。 */
16
+ export declare function hmacSha256(key: string | Uint8Array, message: string | Uint8Array): Uint8Array;
17
+ /** HMAC-SHA256 的小写 hex 摘要(签名 URL 的 `x-signature` 取此形态)。 */
18
+ export declare function hmacSha256Hex(key: string | Uint8Array, message: string | Uint8Array): string;
19
+ /**
20
+ * 恒时字符串比较(替代 `node:crypto.timingSafeEqual`):先比长度、再逐字符累加差值。
21
+ * 签名校验的侧信道面是「签名串本身」,该形态恒时性足够(比较对象是 hex 串,无密钥直接暴露)。
22
+ */
23
+ export declare function constantTimeEqual(a: string, b: string): boolean;
package/package.json CHANGED
@@ -1,44 +1,30 @@
1
1
  {
2
2
  "name": "@adep/runtime",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "AgentDeploy 运行时内核:云函数执行器 + 本地模拟运行时 + cloud.db/cloud.storage 能力装配,零平台重栈依赖(无 elysia/drizzle/better-auth/nuxt/bun:sqlite/postgres/ioredis),供 server / CLI(adep dev) / 浏览器离线工作区三方复用。",
8
- "main": "./src/index.ts",
9
- "types": "./src/index.ts",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.ts",
13
- "default": "./src/index.ts"
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
14
  },
15
15
  "./*": {
16
- "types": "./src/*.ts",
17
- "default": "./src/*.ts"
16
+ "types": "./dist/*.d.ts",
17
+ "default": "./dist/*.js"
18
18
  }
19
19
  },
20
20
  "files": [
21
21
  "dist"
22
22
  ],
23
+ "dependencies": {
24
+ "@adep/types": "0.1.2"
25
+ },
23
26
  "scripts": {
24
27
  "test": "vitest run",
25
28
  "build": "bun run ../../scripts/build-package.ts runtime"
26
- },
27
- "dependencies": {
28
- "@adep/types": "workspace:*"
29
- },
30
- "publishConfig": {
31
- "main": "./dist/index.js",
32
- "types": "./dist/index.d.ts",
33
- "exports": {
34
- ".": {
35
- "types": "./dist/index.d.ts",
36
- "default": "./dist/index.js"
37
- },
38
- "./*": {
39
- "types": "./dist/*.d.ts",
40
- "default": "./dist/*.js"
41
- }
42
- }
43
29
  }
44
- }
30
+ }