@rei-standard/amsg-server 2.6.0-next.2 → 2.6.0-next.21

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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +599 -24
  3. package/dist/adapters/d1.d.ts +416 -0
  4. package/dist/adapters/factory.d.ts +39 -0
  5. package/dist/adapters/interface.d.ts +306 -0
  6. package/dist/adapters/neon.d.ts +88 -0
  7. package/dist/adapters/pg-shared.d.ts +161 -0
  8. package/dist/adapters/pg.d.ts +84 -0
  9. package/dist/adapters/schema.d.ts +57 -0
  10. package/dist/adapters/schema.sqlite.d.ts +49 -0
  11. package/dist/chunk-7NMQFTDJ.cjs +148 -0
  12. package/dist/chunk-E7OWP3VL.cjs +135 -0
  13. package/dist/chunk-GN44PST5.mjs +148 -0
  14. package/dist/chunk-KOJQVOYS.mjs +5847 -0
  15. package/dist/chunk-QHVID3V2.cjs +5847 -0
  16. package/dist/chunk-ZGGF4GMA.mjs +135 -0
  17. package/dist/cloudflare/single-user-worker.d.ts +37 -0
  18. package/dist/cloudflare.cjs +39 -2
  19. package/dist/cloudflare.d.ts +10 -2
  20. package/dist/cloudflare.mjs +40 -3
  21. package/dist/handlers/cancel-message.d.ts +3 -0
  22. package/dist/handlers/capabilities.d.ts +4 -0
  23. package/dist/handlers/client-state.d.ts +7 -0
  24. package/dist/handlers/get-message.d.ts +3 -0
  25. package/dist/handlers/get-user-key.d.ts +3 -0
  26. package/dist/handlers/init-tenant.d.ts +23 -0
  27. package/dist/handlers/llm-credentials.d.ts +5 -0
  28. package/dist/handlers/messages.d.ts +3 -0
  29. package/dist/handlers/outbox.d.ts +7 -0
  30. package/dist/handlers/push-subscription.d.ts +5 -0
  31. package/dist/handlers/schedule-message.d.ts +3 -0
  32. package/dist/handlers/send-notifications.d.ts +3 -0
  33. package/dist/handlers/single-user-init.d.ts +13 -0
  34. package/dist/handlers/update-message.d.ts +3 -0
  35. package/dist/handlers/vapid-public-key.d.ts +19 -0
  36. package/dist/index.cjs +133 -44
  37. package/dist/index.d.cts +131 -766
  38. package/dist/index.d.ts +131 -766
  39. package/dist/index.mjs +125 -36
  40. package/dist/lib/agentic-fire.d.ts +108 -0
  41. package/dist/lib/client-state-store.d.ts +80 -0
  42. package/dist/lib/constant-time.d.ts +14 -0
  43. package/dist/lib/db-errors.d.ts +10 -0
  44. package/dist/lib/encryption.d.ts +48 -0
  45. package/dist/lib/errors.d.ts +215 -0
  46. package/dist/lib/llm-credentials-store.d.ts +147 -0
  47. package/dist/lib/llm.d.ts +1 -0
  48. package/dist/lib/message-processor.d.ts +73 -0
  49. package/dist/lib/outbox-store.d.ts +95 -0
  50. package/dist/lib/push-subscription-store.d.ts +94 -0
  51. package/dist/lib/recurrence.d.ts +51 -0
  52. package/dist/lib/request.d.ts +182 -0
  53. package/dist/lib/result-emitter.d.ts +54 -0
  54. package/dist/lib/run-tick.d.ts +115 -0
  55. package/dist/lib/schema-version.d.ts +50 -0
  56. package/dist/lib/state-accessors.d.ts +70 -0
  57. package/dist/lib/state-chunks.d.ts +55 -0
  58. package/dist/lib/task-projection.d.ts +92 -0
  59. package/dist/lib/validation.d.ts +97 -0
  60. package/dist/lib/version.d.ts +7 -0
  61. package/dist/lib/webcrypto-utils.d.ts +1 -0
  62. package/dist/lib/webpush-webcrypto.d.ts +78 -0
  63. package/dist/{neon-BFUS25UX.cjs → neon-KP2CPA57.cjs} +110 -10
  64. package/dist/{neon-CU5N3CSW.mjs → neon-ZIMHALYI.mjs} +106 -6
  65. package/dist/{pg-QO6NKTGL.mjs → pg-QGC2XHUT.mjs} +94 -5
  66. package/dist/{pg-IIH3M4OM.cjs → pg-RZPQGXR3.cjs} +98 -9
  67. package/dist/single-user.d.ts +95 -0
  68. package/dist/tenant/blob-store.d.ts +30 -0
  69. package/dist/tenant/context.d.ts +47 -0
  70. package/dist/tenant/single-user-context.d.ts +29 -0
  71. package/dist/tenant/token.d.ts +26 -0
  72. package/package.json +3 -3
  73. package/dist/chunk-5ENJGVZX.mjs +0 -72
  74. package/dist/chunk-GN5QTMCL.cjs +0 -1870
  75. package/dist/chunk-PVNLB6TF.mjs +0 -1870
  76. package/dist/chunk-RGECD4OH.cjs +0 -72
  77. package/dist/cloudflare-B-E51VjP.d.cts +0 -2848
  78. package/dist/cloudflare-B-E51VjP.d.ts +0 -2848
  79. package/dist/cloudflare.d.cts +0 -2
  80. package/dist/neon-CJl66EGy.d.cts +0 -251
  81. package/dist/neon-DYvGnCzx.d.ts +0 -251
  82. package/dist/pg-07S-u_H4.d.cts +0 -246
  83. package/dist/pg-Du-pN_UT.d.ts +0 -246
  84. package/dist/schema-C8OnYk6j.d.cts +0 -74
  85. package/dist/schema-C8OnYk6j.d.ts +0 -74
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Validate ISO 8601 date string.
3
+ * @param {string} dateString
4
+ * @returns {boolean}
5
+ */
6
+ export function isValidISO8601(dateString: string): boolean;
7
+ /**
8
+ * Validate UUID format.
9
+ * @param {string} uuid
10
+ * @returns {boolean}
11
+ */
12
+ export function isValidUUID(uuid: string): boolean;
13
+ /**
14
+ * Validate UUID v4 format.
15
+ * @param {string} uuid
16
+ * @returns {boolean}
17
+ */
18
+ export function isValidUUIDv4(uuid: string): boolean;
19
+ /**
20
+ * Validate the optional `splitPattern` field (amsg-server scheduled tasks
21
+ * only; amsg-instant 0.8.0 dropped its request-level `splitPattern`).
22
+ * Accepts `string`, `string[]`, or absent/null. Returns an error message
23
+ * string, or null when valid.
24
+ *
25
+ * Limits (per-item length ≤ 200, array ≤ 10 items, must compile via
26
+ * `new RegExp(item)`) are an **input-size guard**, NOT a ReDoS defense —
27
+ * a 6-character pattern like `(a+)+$` is enough to trigger catastrophic
28
+ * backtracking. The real backstop is Worker / runtime CPU limits + the
29
+ * fact that splitPattern is stored under the user's own encrypted task
30
+ * and matched against output from the user's own LLM API key, so the
31
+ * blast radius is self-inflicted only (no cross-tenant attack surface).
32
+ *
33
+ * @param {unknown} value
34
+ * @returns {string | null}
35
+ */
36
+ export function validateSplitPattern(value: unknown): string | null;
37
+ /**
38
+ * Validate an OpenAI-style messages array. 形状规则统一在
39
+ * `@rei-standard/amsg-shared` 的 `validateLlmMessagesShape`(amsg-instant
40
+ * 的 `validateMessagesArray` 走同一实现,两包接受的形状不会漂移)——
41
+ * 因此 agentic 会话回放(assistant 带 `tool_calls` 时 content 可空、
42
+ * `role:'tool'` 消息要求 `tool_call_id`)在这里同样合法。本函数只把
43
+ * 结构化错误码映射成本包既有的英文错误文案,返回形状不变。
44
+ *
45
+ * @param {unknown} messages
46
+ * @returns {string | null} Error message, or null if valid.
47
+ */
48
+ export function validateLlmMessagesArray(messages: unknown): string | null;
49
+ /**
50
+ * 落库前量一次任务正文。合法返回 null,超限返回可以直接放进 400 响应体的
51
+ * error 对象——`POST /schedule-message` 与 `PUT /update-message` 共用同一个错
52
+ * 误码和同一套 details,下游判断读 `details.bytes` / `details.maxBytes`,不用
53
+ * 去解析那句人话。
54
+ *
55
+ * 按 UTF-8 字节算而不是字符数:D1 限的是字节,加密前也是先转 UTF-8,一段全中
56
+ * 文的正文字符数只有字节数的三分之一,用 `.length` 量等于把上限放大三倍。
57
+ *
58
+ * @param {string} serializedPayload - 将要交给 encryptForStorage 的那个字符串。
59
+ * @returns {{ code: string, message: string, details: { bytes: number, maxBytes: number } } | null}
60
+ */
61
+ export function validateTaskPayloadSize(serializedPayload: string): {
62
+ code: string;
63
+ message: string;
64
+ details: {
65
+ bytes: number;
66
+ maxBytes: number;
67
+ };
68
+ } | null;
69
+ /**
70
+ * 任务正文的 UTF-8 字节数(与 {@link validateTaskPayloadSize} 同一把尺子)。
71
+ *
72
+ * `PUT /update-message` 拿它跟改动前的正文比大小:上限是后加的,比它更早建出来
73
+ * 的大任务不该因此连排期都改不了,只要这次改动没把它变得更大就放行。
74
+ *
75
+ * @param {string} serializedPayload
76
+ * @returns {number}
77
+ */
78
+ export function taskPayloadByteLength(serializedPayload: string): number;
79
+ /**
80
+ * Validate the schedule-message request payload.
81
+ *
82
+ * @param {Object} payload
83
+ * @returns {{ valid: boolean, errorCode?: string, errorMessage?: string, details?: Object }}
84
+ */
85
+ export function validateScheduleMessagePayload(payload: any): {
86
+ valid: boolean;
87
+ errorCode?: string;
88
+ errorMessage?: string;
89
+ details?: any;
90
+ };
91
+ /**
92
+ * 一条任务正文(`JSON.stringify` 的结果)的明文上限,UTF-8 字节数。
93
+ * 客户端想在提交前自己预算就读这个数(从 `@rei-standard/amsg-server` 导出)。
94
+ */
95
+ export const MAX_TASK_PAYLOAD_BYTES: number;
96
+ import { isValidTimeZoneId } from './recurrence.js';
97
+ export { isValidTimeZoneId, isValidUrl, validateAvatarUrl };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 构建期注入的包版本。tsup 用 define 把 __AMSG_SERVER_VERSION__ 替换成
3
+ * package.json 的 version(见 tsup.config.js),发布产物里是真实版本号;
4
+ * 直接跑 src(node --test / 本地调试)没有这个替换,typeof 守卫落到
5
+ * '0.0.0-dev'。
6
+ */
7
+ export const SERVER_VERSION: any;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 组 payload 前做预算用:先量骨架有多大,剩下多少字节才是能塞附加数据的额度。
3
+ *
4
+ * @example
5
+ * // hook 里组 push:把库要补的信封字段先扣掉
6
+ * const { remainingBytes } = measurePushPayload(
7
+ * JSON.stringify({ ...basePush, message: '' }),
8
+ * { reserveEnvelope: true }
9
+ * );
10
+ * const excerpt = remainingBytes > 0 ? text.slice(0, remainingBytes) : '';
11
+ *
12
+ * @param {string} payload - 待发送的 payload 字符串(通常是 JSON.stringify 的结果)。
13
+ * @param {{ reserveEnvelope?: boolean }} [options]
14
+ * `reserveEnvelope: true` → 额度里扣掉 {@link PUSH_ENVELOPE_RESERVED_BYTES},
15
+ * 也就是「库补完字段之后还装得下」的口径。fire-time hook 组 payload 时用这个。
16
+ * 默认 false = 量这个字符串本身对上限的占用。
17
+ * @returns {{ bytes: number, maxBytes: number, remainingBytes: number, withinLimit: boolean, envelopeReservedBytes: number }}
18
+ * `bytes` 是 payload 的 UTF-8 字节数;`maxBytes` 是本次口径下的上限;
19
+ * `remainingBytes` 是还能加多少字节(已超限时为负);`withinLimit` 为 false
20
+ * 时(不留信封的口径下)sendWebPush 会抛 `PUSH_PAYLOAD_TOO_LARGE`。
21
+ */
22
+ export function measurePushPayload(payload: string, options?: {
23
+ reserveEnvelope?: boolean;
24
+ }): {
25
+ bytes: number;
26
+ maxBytes: number;
27
+ remainingBytes: number;
28
+ withinLimit: boolean;
29
+ envelopeReservedBytes: number;
30
+ };
31
+ /**
32
+ * Send a single Web Push notification. 委托 shared 的 `sendWebPush`(参数、
33
+ * 返回值与之完全一致),只在加密前多做一步 payload 大小护栏——超限的
34
+ * payload 发出去只会被推送服务 413 掉,用户什么也收不到。加密之前就拦下
35
+ * 来,让调用方拿到一个说得清的错误(宿主可以退回一条短消息 + 引用键)。
36
+ *
37
+ * @param {Object} args - 同 shared `sendWebPush`:{ subscription, payload, vapid, ttl, fetch }。
38
+ * @returns {Promise<{ statusCode: number, body: string, headers: Headers }>}
39
+ * @throws {Error} err.code = 'PUSH_PAYLOAD_TOO_LARGE' when the payload
40
+ * exceeds MAX_PUSH_PAYLOAD_BYTES (nothing is sent);
41
+ * err.code = 'PUSH_SEND_FAILED' on push-service error.
42
+ */
43
+ export function sendWebPush(args: any): Promise<{
44
+ statusCode: number;
45
+ body: string;
46
+ headers: Headers;
47
+ }>;
48
+ /**
49
+ * web-push-compatible sender backed by the shared Web Crypto implementation.
50
+ * message-processor calls `ctx.webpush.sendNotification(subscription, payloadString)`,
51
+ * so we only need that one method. VAPID keys are baked in at construction.
52
+ *
53
+ * @param {{ email: string, publicKey: string, privateKey: string }} [vapid]
54
+ * @param {{ ttl?: number }} [options] - Push TTL in seconds; defaults to 4 weeks
55
+ * (matches the web-push backend) so scheduled pushes outlive an offline device.
56
+ * @returns {{ sendNotification: (subscription: Object, payload: string) => Promise<any> }}
57
+ */
58
+ export function createWebCryptoWebPush(vapid?: {
59
+ email: string;
60
+ publicKey: string;
61
+ privateKey: string;
62
+ }, { ttl }?: {
63
+ ttl?: number;
64
+ }): {
65
+ sendNotification: (subscription: any, payload: string) => Promise<any>;
66
+ };
67
+ /** 推送服务对加密后 body 的上限(字节)。 */
68
+ export const WEB_PUSH_MAX_BODY_BYTES: 4096;
69
+ /** aes128gcm 固定开销:header 86 + 填充分隔符 1 + GCM tag 16。 */
70
+ export const WEB_PUSH_ENCRYPTION_OVERHEAD_BYTES: number;
71
+ /** 一条 push 的明文(JSON 字符串)上限,UTF-8 字节数。 */
72
+ export const MAX_PUSH_PAYLOAD_BYTES: number;
73
+ /**
74
+ * 库在 hook 交还 payload 之后还要补的那批字段占的字节数上界。
75
+ * 组 payload 时按 `MAX_PUSH_PAYLOAD_BYTES - PUSH_ENVELOPE_RESERVED_BYTES`
76
+ * 预算,或者直接用 `measurePushPayload(s, { reserveEnvelope: true })`。
77
+ */
78
+ export const PUSH_ENVELOPE_RESERVED_BYTES: 384;
@@ -3,7 +3,25 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkRGECD4OHcjs = require('./chunk-RGECD4OH.cjs');
6
+
7
+
8
+
9
+
10
+
11
+
12
+ var _chunkE7OWP3VLcjs = require('./chunk-E7OWP3VL.cjs');
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+ var _chunk7NMQFTDJcjs = require('./chunk-7NMQFTDJ.cjs');
7
25
 
8
26
  // src/server/adapters/neon.js
9
27
  var _serverless = require('@neondatabase/serverless');
@@ -13,7 +31,13 @@ var NeonAdapter = class {
13
31
  this._connectionString = connectionString;
14
32
  this._sql = null;
15
33
  }
16
- /** @private */
34
+ /**
35
+ * neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
36
+ * pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
37
+ * Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
38
+ *
39
+ * @private
40
+ */
17
41
  _getSql() {
18
42
  if (!this._sql) {
19
43
  this._sql = _serverless.neon.call(void 0, this._connectionString);
@@ -22,9 +46,14 @@ var NeonAdapter = class {
22
46
  }
23
47
  async initSchema() {
24
48
  const sql = this._getSql();
25
- await sql.query(_chunkRGECD4OHcjs.TABLE_SQL);
49
+ await sql.query(_chunk7NMQFTDJcjs.TABLE_SQL);
50
+ await sql.query(_chunk7NMQFTDJcjs.PUSH_SUBSCRIPTION_TABLE_SQL);
51
+ await sql.query(_chunk7NMQFTDJcjs.LLM_CREDENTIALS_TABLE_SQL);
52
+ for (const migration of _chunk7NMQFTDJcjs.MIGRATIONS) {
53
+ await sql.query(migration.sql);
54
+ }
26
55
  const indexResults = [];
27
- for (const index of _chunkRGECD4OHcjs.INDEXES) {
56
+ for (const index of _chunk7NMQFTDJcjs.INDEXES) {
28
57
  try {
29
58
  await sql.query(index.sql);
30
59
  indexResults.push({
@@ -50,11 +79,11 @@ var NeonAdapter = class {
50
79
  `Critical index creation failed (${failedNames}). Please remove duplicate UUID rows and run initSchema again.`
51
80
  );
52
81
  }
53
- const tableCheck = await sql.query(_chunkRGECD4OHcjs.VERIFY_TABLE_SQL);
82
+ const tableCheck = await sql.query(_chunk7NMQFTDJcjs.VERIFY_TABLE_SQL);
54
83
  if (tableCheck.length === 0) {
55
84
  throw new Error("Table creation verification failed");
56
85
  }
57
- const columns = await sql.query(_chunkRGECD4OHcjs.COLUMNS_SQL);
86
+ const columns = await sql.query(_chunk7NMQFTDJcjs.COLUMNS_SQL);
58
87
  return {
59
88
  columnsCreated: columns.length,
60
89
  indexesCreated: indexResults.filter((r) => r.status === "success").length,
@@ -68,6 +97,8 @@ var NeonAdapter = class {
68
97
  async dropSchema() {
69
98
  const sql = this._getSql();
70
99
  await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
100
+ await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
101
+ await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
71
102
  }
72
103
  async createTask(params) {
73
104
  const sql = this._getSql();
@@ -83,7 +114,7 @@ var NeonAdapter = class {
83
114
  async getTaskByUuid(uuid, userId) {
84
115
  const sql = this._getSql();
85
116
  const rows = await sql.query(
86
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
117
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
87
118
  FROM scheduled_messages
88
119
  WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
89
120
  LIMIT 1`,
@@ -94,7 +125,7 @@ var NeonAdapter = class {
94
125
  async getTaskByUuidOnly(uuid) {
95
126
  const sql = this._getSql();
96
127
  const rows = await sql.query(
97
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
128
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
98
129
  FROM scheduled_messages
99
130
  WHERE uuid = $1 AND status = 'pending'
100
131
  LIMIT 1`,
@@ -102,12 +133,30 @@ var NeonAdapter = class {
102
133
  );
103
134
  return rows[0] || null;
104
135
  }
136
+ /**
137
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
138
+ * 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
139
+ *
140
+ * @param {string} uuid
141
+ * @returns {Promise<{ status: string }|null>}
142
+ */
143
+ async getTaskStatusByUuidOnly(uuid) {
144
+ const sql = this._getSql();
145
+ const rows = await sql.query(
146
+ `SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
147
+ [uuid]
148
+ );
149
+ return rows[0] ? { status: rows[0].status } : null;
150
+ }
105
151
  async updateTaskById(taskId, updates) {
106
152
  const sql = this._getSql();
107
153
  const sets = [];
108
154
  const values = [];
109
155
  let idx = 1;
110
156
  for (const [key, value] of Object.entries(updates)) {
157
+ if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
158
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
159
+ }
111
160
  sets.push(`${key} = $${idx}`);
112
161
  values.push(value);
113
162
  idx++;
@@ -127,6 +176,9 @@ var NeonAdapter = class {
127
176
  let idx = 2;
128
177
  if (extraFields) {
129
178
  for (const [key, value] of Object.entries(extraFields)) {
179
+ if (!_chunk7NMQFTDJcjs.UPDATABLE_COLUMNS.has(key)) {
180
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
181
+ }
130
182
  sets.push(`${key} = $${idx}`);
131
183
  values.push(value);
132
184
  idx++;
@@ -157,14 +209,27 @@ var NeonAdapter = class {
157
209
  async getPendingTasks(limit = 50) {
158
210
  const sql = this._getSql();
159
211
  return sql.query(
160
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
212
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DELIVERY_COLUMNS}
161
213
  FROM scheduled_messages
162
214
  WHERE status = 'pending' AND next_send_at <= NOW()
215
+ AND (lease_until IS NULL OR lease_until <= NOW())
216
+ AND (retry_after IS NULL OR retry_after <= NOW())
163
217
  ORDER BY next_send_at ASC
164
218
  LIMIT $1`,
165
219
  [limit]
166
220
  );
167
221
  }
222
+ // 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
223
+ // 语义说明也在那里)。
224
+ async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
225
+ const sql = this._getSql();
226
+ return _chunkE7OWP3VLcjs.claimTask.call(void 0, (text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
227
+ }
228
+ // 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
229
+ async renewTaskLease(taskId, leaseUntil) {
230
+ const sql = this._getSql();
231
+ return _chunkE7OWP3VLcjs.renewTaskLease.call(void 0, (text, params) => sql.query(text, params), taskId, leaseUntil);
232
+ }
168
233
  async listTasks(userId, opts = {}) {
169
234
  const sql = this._getSql();
170
235
  const { status = "all", limit = 20, offset = 0 } = opts;
@@ -184,7 +249,7 @@ var NeonAdapter = class {
184
249
  const total = parseInt(countRows[0].count, 10);
185
250
  const taskParams = [...params, limit, offset];
186
251
  const tasks = await sql.query(
187
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
252
+ `SELECT ${_chunk7NMQFTDJcjs.TASK_DETAIL_COLUMNS}
188
253
  FROM scheduled_messages
189
254
  WHERE ${where}
190
255
  ORDER BY next_send_at ASC
@@ -213,6 +278,41 @@ var NeonAdapter = class {
213
278
  );
214
279
  return rows.length > 0 ? rows[0].status : null;
215
280
  }
281
+ // 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
282
+ async getTaskStatusInfo(uuid, userId) {
283
+ const sql = this._getSql();
284
+ return _chunkE7OWP3VLcjs.getTaskStatusInfo.call(void 0, (text, params) => sql.query(text, params), uuid, userId);
285
+ }
286
+ // ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
287
+ async getPushSubscription(userId) {
288
+ const sql = this._getSql();
289
+ return _chunkE7OWP3VLcjs.getPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
290
+ }
291
+ async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
292
+ const sql = this._getSql();
293
+ return _chunkE7OWP3VLcjs.upsertPushSubscription.call(void 0, (text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
294
+ }
295
+ async deletePushSubscription(userId) {
296
+ const sql = this._getSql();
297
+ return _chunkE7OWP3VLcjs.deletePushSubscription.call(void 0, (text, params) => sql.query(text, params), userId);
298
+ }
299
+ // ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
300
+ async upsertLlmCredentials(userId, entries) {
301
+ const sql = this._getSql();
302
+ return _chunkE7OWP3VLcjs.upsertLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
303
+ }
304
+ async getLlmCredentials(userId, credIds) {
305
+ const sql = this._getSql();
306
+ return _chunkE7OWP3VLcjs.getLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
307
+ }
308
+ async listLlmCredentials(userId) {
309
+ const sql = this._getSql();
310
+ return _chunkE7OWP3VLcjs.listLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId);
311
+ }
312
+ async deleteLlmCredentials(userId, credIds = null) {
313
+ const sql = this._getSql();
314
+ return _chunkE7OWP3VLcjs.deleteLlmCredentials.call(void 0, (text, params) => sql.query(text, params), userId, credIds);
315
+ }
216
316
  };
217
317
 
218
318
 
@@ -1,9 +1,27 @@
1
+ import {
2
+ claimTask,
3
+ deleteLlmCredentials,
4
+ deletePushSubscription,
5
+ getLlmCredentials,
6
+ getPushSubscription,
7
+ getTaskStatusInfo,
8
+ listLlmCredentials,
9
+ renewTaskLease,
10
+ upsertLlmCredentials,
11
+ upsertPushSubscription
12
+ } from "./chunk-ZGGF4GMA.mjs";
1
13
  import {
2
14
  COLUMNS_SQL,
3
15
  INDEXES,
16
+ LLM_CREDENTIALS_TABLE_SQL,
17
+ MIGRATIONS,
18
+ PUSH_SUBSCRIPTION_TABLE_SQL,
4
19
  TABLE_SQL,
20
+ TASK_DELIVERY_COLUMNS,
21
+ TASK_DETAIL_COLUMNS,
22
+ UPDATABLE_COLUMNS,
5
23
  VERIFY_TABLE_SQL
6
- } from "./chunk-5ENJGVZX.mjs";
24
+ } from "./chunk-GN44PST5.mjs";
7
25
 
8
26
  // src/server/adapters/neon.js
9
27
  import { neon } from "@neondatabase/serverless";
@@ -13,7 +31,13 @@ var NeonAdapter = class {
13
31
  this._connectionString = connectionString;
14
32
  this._sql = null;
15
33
  }
16
- /** @private */
34
+ /**
35
+ * neon() 的 HTTP 驱动:一次查询一个 fetch,两次查询之间不留连接。所以它没有
36
+ * pg 那种「空闲连接被服务端掐断」的问题——连接层面的错误只会让当次调用的
37
+ * Promise 失败,落在调用方的 try/catch 里,不会变成进程级未捕获异常。
38
+ *
39
+ * @private
40
+ */
17
41
  _getSql() {
18
42
  if (!this._sql) {
19
43
  this._sql = neon(this._connectionString);
@@ -23,6 +47,11 @@ var NeonAdapter = class {
23
47
  async initSchema() {
24
48
  const sql = this._getSql();
25
49
  await sql.query(TABLE_SQL);
50
+ await sql.query(PUSH_SUBSCRIPTION_TABLE_SQL);
51
+ await sql.query(LLM_CREDENTIALS_TABLE_SQL);
52
+ for (const migration of MIGRATIONS) {
53
+ await sql.query(migration.sql);
54
+ }
26
55
  const indexResults = [];
27
56
  for (const index of INDEXES) {
28
57
  try {
@@ -68,6 +97,8 @@ var NeonAdapter = class {
68
97
  async dropSchema() {
69
98
  const sql = this._getSql();
70
99
  await sql.query("DROP TABLE IF EXISTS scheduled_messages CASCADE");
100
+ await sql.query("DROP TABLE IF EXISTS push_subscriptions CASCADE");
101
+ await sql.query("DROP TABLE IF EXISTS llm_credentials CASCADE");
71
102
  }
72
103
  async createTask(params) {
73
104
  const sql = this._getSql();
@@ -83,7 +114,7 @@ var NeonAdapter = class {
83
114
  async getTaskByUuid(uuid, userId) {
84
115
  const sql = this._getSql();
85
116
  const rows = await sql.query(
86
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
117
+ `SELECT ${TASK_DETAIL_COLUMNS}
87
118
  FROM scheduled_messages
88
119
  WHERE uuid = $1 AND user_id = $2 AND status = 'pending'
89
120
  LIMIT 1`,
@@ -94,7 +125,7 @@ var NeonAdapter = class {
94
125
  async getTaskByUuidOnly(uuid) {
95
126
  const sql = this._getSql();
96
127
  const rows = await sql.query(
97
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
128
+ `SELECT ${TASK_DELIVERY_COLUMNS}
98
129
  FROM scheduled_messages
99
130
  WHERE uuid = $1 AND status = 'pending'
100
131
  LIMIT 1`,
@@ -102,12 +133,30 @@ var NeonAdapter = class {
102
133
  );
103
134
  return rows[0] || null;
104
135
  }
136
+ /**
137
+ * 这条 uuid 现在是什么状态——不限用户,也不限状态(上面那个只看 pending
138
+ * 行)。`runTask` 用它把「这条已经跑完了」和「压根没这条」分开回报。
139
+ *
140
+ * @param {string} uuid
141
+ * @returns {Promise<{ status: string }|null>}
142
+ */
143
+ async getTaskStatusByUuidOnly(uuid) {
144
+ const sql = this._getSql();
145
+ const rows = await sql.query(
146
+ `SELECT status FROM scheduled_messages WHERE uuid = $1 LIMIT 1`,
147
+ [uuid]
148
+ );
149
+ return rows[0] ? { status: rows[0].status } : null;
150
+ }
105
151
  async updateTaskById(taskId, updates) {
106
152
  const sql = this._getSql();
107
153
  const sets = [];
108
154
  const values = [];
109
155
  let idx = 1;
110
156
  for (const [key, value] of Object.entries(updates)) {
157
+ if (!UPDATABLE_COLUMNS.has(key)) {
158
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
159
+ }
111
160
  sets.push(`${key} = $${idx}`);
112
161
  values.push(value);
113
162
  idx++;
@@ -127,6 +176,9 @@ var NeonAdapter = class {
127
176
  let idx = 2;
128
177
  if (extraFields) {
129
178
  for (const [key, value] of Object.entries(extraFields)) {
179
+ if (!UPDATABLE_COLUMNS.has(key)) {
180
+ throw new Error(`[amsg-server neon] rejected unknown update column: ${key}`);
181
+ }
130
182
  sets.push(`${key} = $${idx}`);
131
183
  values.push(value);
132
184
  idx++;
@@ -157,14 +209,27 @@ var NeonAdapter = class {
157
209
  async getPendingTasks(limit = 50) {
158
210
  const sql = this._getSql();
159
211
  return sql.query(
160
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count
212
+ `SELECT ${TASK_DELIVERY_COLUMNS}
161
213
  FROM scheduled_messages
162
214
  WHERE status = 'pending' AND next_send_at <= NOW()
215
+ AND (lease_until IS NULL OR lease_until <= NOW())
216
+ AND (retry_after IS NULL OR retry_after <= NOW())
163
217
  ORDER BY next_send_at ASC
164
218
  LIMIT $1`,
165
219
  [limit]
166
220
  );
167
221
  }
222
+ // 领取一条到点的任务。SQL 与并发语义在 pg-shared.js(pg / neon 共用一份,
223
+ // 语义说明也在那里)。
224
+ async claimTask(taskId, expectedNextSendAt, leaseUntil, serializeGroup = null) {
225
+ const sql = this._getSql();
226
+ return claimTask((text, params) => sql.query(text, params), taskId, expectedNextSendAt, leaseUntil, serializeGroup);
227
+ }
228
+ // 投递期间的租约续期(run-tick 心跳;语义见 pg-shared.js)。
229
+ async renewTaskLease(taskId, leaseUntil) {
230
+ const sql = this._getSql();
231
+ return renewTaskLease((text, params) => sql.query(text, params), taskId, leaseUntil);
232
+ }
168
233
  async listTasks(userId, opts = {}) {
169
234
  const sql = this._getSql();
170
235
  const { status = "all", limit = 20, offset = 0 } = opts;
@@ -184,7 +249,7 @@ var NeonAdapter = class {
184
249
  const total = parseInt(countRows[0].count, 10);
185
250
  const taskParams = [...params, limit, offset];
186
251
  const tasks = await sql.query(
187
- `SELECT id, user_id, uuid, encrypted_payload, message_type, next_send_at, status, retry_count, created_at, updated_at
252
+ `SELECT ${TASK_DETAIL_COLUMNS}
188
253
  FROM scheduled_messages
189
254
  WHERE ${where}
190
255
  ORDER BY next_send_at ASC
@@ -213,6 +278,41 @@ var NeonAdapter = class {
213
278
  );
214
279
  return rows.length > 0 ? rows[0].status : null;
215
280
  }
281
+ // 状态 + 失败摘要(GET /message 对已失败的行透出原因;语义见 pg-shared.js)。
282
+ async getTaskStatusInfo(uuid, userId) {
283
+ const sql = this._getSql();
284
+ return getTaskStatusInfo((text, params) => sql.query(text, params), uuid, userId);
285
+ }
286
+ // ── push_subscriptions(实现见 pg-shared.js,pg / neon 共用一份)─────────
287
+ async getPushSubscription(userId) {
288
+ const sql = this._getSql();
289
+ return getPushSubscription((text, params) => sql.query(text, params), userId);
290
+ }
291
+ async upsertPushSubscription(userId, encryptedSubscription, updatedAt) {
292
+ const sql = this._getSql();
293
+ return upsertPushSubscription((text, params) => sql.query(text, params), userId, encryptedSubscription, updatedAt);
294
+ }
295
+ async deletePushSubscription(userId) {
296
+ const sql = this._getSql();
297
+ return deletePushSubscription((text, params) => sql.query(text, params), userId);
298
+ }
299
+ // ── llm_credentials(实现见 pg-shared.js,pg / neon 共用一份)───────────
300
+ async upsertLlmCredentials(userId, entries) {
301
+ const sql = this._getSql();
302
+ return upsertLlmCredentials((text, params) => sql.query(text, params), userId, entries, (/* @__PURE__ */ new Date()).toISOString());
303
+ }
304
+ async getLlmCredentials(userId, credIds) {
305
+ const sql = this._getSql();
306
+ return getLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
307
+ }
308
+ async listLlmCredentials(userId) {
309
+ const sql = this._getSql();
310
+ return listLlmCredentials((text, params) => sql.query(text, params), userId);
311
+ }
312
+ async deleteLlmCredentials(userId, credIds = null) {
313
+ const sql = this._getSql();
314
+ return deleteLlmCredentials((text, params) => sql.query(text, params), userId, credIds);
315
+ }
216
316
  };
217
317
  export {
218
318
  NeonAdapter