@lunora/do 1.0.0-alpha.35 → 1.0.0-alpha.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -2757,6 +2757,7 @@ declare const FLAGS_FUNCTION_PREFIX = "__lunora_flags__:";
2757
2757
  */
2758
2758
  declare const ADMIN_FUNCTIONS: {
2759
2759
  readonly applyCdc: "__lunora_admin__:applyCdc";
2760
+ readonly assignIssue: "__lunora_admin__:assignIssue";
2760
2761
  readonly cdcSync: "__lunora_admin__:cdcSync";
2761
2762
  readonly clearCapturedMail: "__lunora_admin__:clearCapturedMail";
2762
2763
  readonly clearQueueMessages: "__lunora_admin__:clearQueueMessages";
@@ -2786,6 +2787,7 @@ declare const ADMIN_FUNCTIONS: {
2786
2787
  readonly getSettings: "__lunora_admin__:getSettings";
2787
2788
  readonly getTraces: "__lunora_admin__:getTraces";
2788
2789
  readonly getWorkflowInstanceStatus: "__lunora_admin__:getWorkflowInstanceStatus";
2790
+ readonly ignoreIssue: "__lunora_admin__:ignoreIssue";
2789
2791
  readonly importShard: "__lunora_admin__:importShard";
2790
2792
  readonly listFlags: "__lunora_admin__:listFlags";
2791
2793
  readonly listQueues: "__lunora_admin__:listQueues";
@@ -2802,12 +2804,14 @@ declare const ADMIN_FUNCTIONS: {
2802
2804
  readonly recordMail: "__lunora_admin__:recordMail";
2803
2805
  readonly recordQueueMessage: "__lunora_admin__:recordQueueMessage";
2804
2806
  readonly replayQueueMessage: "__lunora_admin__:replayQueueMessage";
2807
+ readonly resolveIssue: "__lunora_admin__:resolveIssue";
2805
2808
  readonly rlsPolicies: "__lunora_admin__:rlsPolicies";
2806
2809
  readonly runAs: "__lunora_admin__:runAs";
2807
2810
  readonly runMigration: "__lunora_admin__:runMigration";
2808
2811
  readonly runSql: "__lunora_admin__:runSql";
2809
2812
  readonly sendQueueMessage: "__lunora_admin__:sendQueueMessage";
2810
2813
  readonly sendTestMail: "__lunora_admin__:sendTestMail";
2814
+ readonly setIssueSeverity: "__lunora_admin__:setIssueSeverity";
2811
2815
  readonly storageOrphans: "__lunora_admin__:storageOrphans";
2812
2816
  readonly storageReferences: "__lunora_admin__:storageReferences";
2813
2817
  readonly storageRules: "__lunora_admin__:storageRules";
@@ -5921,6 +5925,29 @@ declare abstract class ShardDO {
5921
5925
  * mirroring `handlePitrAdminOp`.
5922
5926
  */
5923
5927
  private handleExtraAdminOp;
5928
+ /**
5929
+ * Serve the four Issue-triage admin writes — `resolveIssue` / `ignoreIssue`
5930
+ * (a status change), `assignIssue` (set/clear an owner), `setIssueSeverity`
5931
+ * (tag/clear severity). Each upserts one row in the reserved
5932
+ * `__lunora_issue_state__` side table keyed by the Issue's fingerprint
5933
+ * `hash`, then re-derives at read time in {@link readErrorIssues}. Returns
5934
+ * `undefined` for any path it doesn't own so `handleExtraAdminOp` falls
5935
+ * through. Admin-gated by `handleAdminRpc`'s caller (the `LUNORA_ADMIN_TOKEN`
5936
+ * bearer); a bad/missing `hash` (or a bad status/severity value) is a 400.
5937
+ *
5938
+ * The write lands through raw SQL the change-tracker can't observe, so it
5939
+ * marks {@link ISSUE_STATE_TABLE} changed and flushes — the live Issues
5940
+ * subscription is an admin-wildcard memo that re-runs whenever a flush finds
5941
+ * a changed table, so the triage shows up without an unrelated write.
5942
+ */
5943
+ private handleIssueTriageOp;
5944
+ /**
5945
+ * Map an Issue-triage admin path to the `IssueStatePatch` it applies, or
5946
+ * `undefined` when the path isn't a triage write. `assignIssue`/
5947
+ * `setIssueSeverity` accept an explicit `null` to CLEAR the field (unassign /
5948
+ * untag); a missing or malformed value is a 400 rather than a silent no-op.
5949
+ */
5950
+ private parseIssueTriagePatch;
5924
5951
  /**
5925
5952
  * Record one app-level auth attempt for the auth-failure SLO (PLAN3 §2.3).
5926
5953
  * The worker calls this fire-and-forget (via `waitUntil`) after a top-level
package/dist/index.d.ts CHANGED
@@ -2757,6 +2757,7 @@ declare const FLAGS_FUNCTION_PREFIX = "__lunora_flags__:";
2757
2757
  */
2758
2758
  declare const ADMIN_FUNCTIONS: {
2759
2759
  readonly applyCdc: "__lunora_admin__:applyCdc";
2760
+ readonly assignIssue: "__lunora_admin__:assignIssue";
2760
2761
  readonly cdcSync: "__lunora_admin__:cdcSync";
2761
2762
  readonly clearCapturedMail: "__lunora_admin__:clearCapturedMail";
2762
2763
  readonly clearQueueMessages: "__lunora_admin__:clearQueueMessages";
@@ -2786,6 +2787,7 @@ declare const ADMIN_FUNCTIONS: {
2786
2787
  readonly getSettings: "__lunora_admin__:getSettings";
2787
2788
  readonly getTraces: "__lunora_admin__:getTraces";
2788
2789
  readonly getWorkflowInstanceStatus: "__lunora_admin__:getWorkflowInstanceStatus";
2790
+ readonly ignoreIssue: "__lunora_admin__:ignoreIssue";
2789
2791
  readonly importShard: "__lunora_admin__:importShard";
2790
2792
  readonly listFlags: "__lunora_admin__:listFlags";
2791
2793
  readonly listQueues: "__lunora_admin__:listQueues";
@@ -2802,12 +2804,14 @@ declare const ADMIN_FUNCTIONS: {
2802
2804
  readonly recordMail: "__lunora_admin__:recordMail";
2803
2805
  readonly recordQueueMessage: "__lunora_admin__:recordQueueMessage";
2804
2806
  readonly replayQueueMessage: "__lunora_admin__:replayQueueMessage";
2807
+ readonly resolveIssue: "__lunora_admin__:resolveIssue";
2805
2808
  readonly rlsPolicies: "__lunora_admin__:rlsPolicies";
2806
2809
  readonly runAs: "__lunora_admin__:runAs";
2807
2810
  readonly runMigration: "__lunora_admin__:runMigration";
2808
2811
  readonly runSql: "__lunora_admin__:runSql";
2809
2812
  readonly sendQueueMessage: "__lunora_admin__:sendQueueMessage";
2810
2813
  readonly sendTestMail: "__lunora_admin__:sendTestMail";
2814
+ readonly setIssueSeverity: "__lunora_admin__:setIssueSeverity";
2811
2815
  readonly storageOrphans: "__lunora_admin__:storageOrphans";
2812
2816
  readonly storageReferences: "__lunora_admin__:storageReferences";
2813
2817
  readonly storageRules: "__lunora_admin__:storageRules";
@@ -5921,6 +5925,29 @@ declare abstract class ShardDO {
5921
5925
  * mirroring `handlePitrAdminOp`.
5922
5926
  */
5923
5927
  private handleExtraAdminOp;
5928
+ /**
5929
+ * Serve the four Issue-triage admin writes — `resolveIssue` / `ignoreIssue`
5930
+ * (a status change), `assignIssue` (set/clear an owner), `setIssueSeverity`
5931
+ * (tag/clear severity). Each upserts one row in the reserved
5932
+ * `__lunora_issue_state__` side table keyed by the Issue's fingerprint
5933
+ * `hash`, then re-derives at read time in {@link readErrorIssues}. Returns
5934
+ * `undefined` for any path it doesn't own so `handleExtraAdminOp` falls
5935
+ * through. Admin-gated by `handleAdminRpc`'s caller (the `LUNORA_ADMIN_TOKEN`
5936
+ * bearer); a bad/missing `hash` (or a bad status/severity value) is a 400.
5937
+ *
5938
+ * The write lands through raw SQL the change-tracker can't observe, so it
5939
+ * marks {@link ISSUE_STATE_TABLE} changed and flushes — the live Issues
5940
+ * subscription is an admin-wildcard memo that re-runs whenever a flush finds
5941
+ * a changed table, so the triage shows up without an unrelated write.
5942
+ */
5943
+ private handleIssueTriageOp;
5944
+ /**
5945
+ * Map an Issue-triage admin path to the `IssueStatePatch` it applies, or
5946
+ * `undefined` when the path isn't a triage write. `assignIssue`/
5947
+ * `setIssueSeverity` accept an explicit `null` to CLEAR the field (unassign /
5948
+ * untag); a missing or malformed value is a 400 rather than a silent no-op.
5949
+ */
5950
+ private parseIssueTriagePatch;
5924
5951
  /**
5925
5952
  * Record one app-level auth attempt for the auth-failure SLO (PLAN3 §2.3).
5926
5953
  * The worker calls this fire-and-forget (via `waitUntil`) after a top-level
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ export { diffExternalSource } from './packem_shared/diffExternalSource-CovHfdyo.
12
12
  export { materializeExternalRows, materializeExternalRowsIncremental, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-DlQWMlw_.mjs';
13
13
  export { isSoftDeleted, isSourceDue, liftSourceId, pullExternalSourceIncrementalTick, pullExternalSourceTick } from './packem_shared/isSoftDeleted-YLKR6JYw.mjs';
14
14
  export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric } from './packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
15
- export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-CqVmUVNs.mjs';
15
+ export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-DVk02KpP.mjs';
16
16
  export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
17
17
  export { MAIL_RETENTION, MAIL_TABLE, clearCapturedMail, ensureMailTable, readCapturedMail, recordCapturedMail } from './packem_shared/MAIL_RETENTION-CPpgl-dX.mjs';
18
18
  export { default as NotFoundError } from './packem_shared/NotFoundError-C70b9hLw.mjs';
@@ -20,14 +20,14 @@ export { armRestore, readBookmark } from './packem_shared/armRestore-4Px61hHS.mj
20
20
  export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-WQY8m62C.mjs';
21
21
  export { RANK_TIEBREAK, encodePartitionKey, matchesRankStaticWhere, rankTableName, resolveRankPartition, sortColumnName } from './packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs';
22
22
  export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-DnSvbjil.mjs';
23
- export { serveRelationFanout } from './packem_shared/serveRelationFanout-Yiyx2OQB.mjs';
23
+ export { serveRelationFanout } from './packem_shared/serveRelationFanout-FdrPflv1.mjs';
24
24
  export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
25
25
  export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-BXSq3S70.mjs';
26
26
  export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs';
27
27
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
28
28
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
29
29
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs';
30
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-QSOI2hpe.mjs';
30
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CBlw_ir3.mjs';
31
31
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
32
32
  export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-iFAA8FbD.mjs';
33
33
  export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
@@ -7,6 +7,7 @@ const RELATION_FUNCTION_PREFIX = "__lunora_relation__:";
7
7
  const FLAGS_FUNCTION_PREFIX = "__lunora_flags__:";
8
8
  const ADMIN_FUNCTIONS = {
9
9
  applyCdc: "__lunora_admin__:applyCdc",
10
+ assignIssue: "__lunora_admin__:assignIssue",
10
11
  cdcSync: "__lunora_admin__:cdcSync",
11
12
  clearCapturedMail: "__lunora_admin__:clearCapturedMail",
12
13
  clearQueueMessages: "__lunora_admin__:clearQueueMessages",
@@ -37,6 +38,7 @@ const ADMIN_FUNCTIONS = {
37
38
  getTraces: "__lunora_admin__:getTraces",
38
39
  // eslint-disable-next-line no-secrets/no-secrets -- reserved admin RPC path constant, not a credential
39
40
  getWorkflowInstanceStatus: "__lunora_admin__:getWorkflowInstanceStatus",
41
+ ignoreIssue: "__lunora_admin__:ignoreIssue",
40
42
  importShard: "__lunora_admin__:importShard",
41
43
  listFlags: "__lunora_admin__:listFlags",
42
44
  listQueues: "__lunora_admin__:listQueues",
@@ -53,12 +55,14 @@ const ADMIN_FUNCTIONS = {
53
55
  recordMail: "__lunora_admin__:recordMail",
54
56
  recordQueueMessage: "__lunora_admin__:recordQueueMessage",
55
57
  replayQueueMessage: "__lunora_admin__:replayQueueMessage",
58
+ resolveIssue: "__lunora_admin__:resolveIssue",
56
59
  rlsPolicies: "__lunora_admin__:rlsPolicies",
57
60
  runAs: "__lunora_admin__:runAs",
58
61
  runMigration: "__lunora_admin__:runMigration",
59
62
  runSql: "__lunora_admin__:runSql",
60
63
  sendQueueMessage: "__lunora_admin__:sendQueueMessage",
61
64
  sendTestMail: "__lunora_admin__:sendTestMail",
65
+ setIssueSeverity: "__lunora_admin__:setIssueSeverity",
62
66
  storageOrphans: "__lunora_admin__:storageOrphans",
63
67
  storageReferences: "__lunora_admin__:storageReferences",
64
68
  storageRules: "__lunora_admin__:storageRules",
@@ -9,7 +9,7 @@ import { n as normalizeLogFields, r as resolveTraceAnchor, a as createTracer, c
9
9
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
10
10
  import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
11
11
  import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
12
- import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-CqVmUVNs.mjs';
12
+ import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-DVk02KpP.mjs';
13
13
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
14
14
  import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
15
15
  import { stableStringify } from './stableStringify-mC40mZts.mjs';
@@ -93,12 +93,12 @@ const verifyWsAdminToken = async (secret, token, now = Date.now()) => {
93
93
 
94
94
  const AUDIT_LOG_TABLE = "__lunora_audit__";
95
95
  const AUDIT_LOG_RETENTION = 1e3;
96
- const runSql$3 = (sql, query, ...params) => {
96
+ const runSql$4 = (sql, query, ...params) => {
97
97
  const runner = sql.exec;
98
98
  return runner.call(sql, query, ...params);
99
99
  };
100
100
  const ensureAuditTable = (sql) => {
101
- runSql$3(
101
+ runSql$4(
102
102
  sql,
103
103
  `CREATE TABLE IF NOT EXISTS "${AUDIT_LOG_TABLE}" (
104
104
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -112,7 +112,7 @@ const ensureAuditTable = (sql) => {
112
112
  };
113
113
  const appendAuditEntry = (sql, entry) => {
114
114
  ensureAuditTable(sql);
115
- runSql$3(
115
+ runSql$4(
116
116
  sql,
117
117
  `INSERT INTO "${AUDIT_LOG_TABLE}" (ts, op, "table", id, detail) VALUES (?, ?, ?, ?, ?)`,
118
118
  entry.ts,
@@ -124,13 +124,13 @@ const appendAuditEntry = (sql, entry) => {
124
124
  // eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct value for an op with no associated table/id/detail.
125
125
  entry.detail === void 0 ? null : JSON.stringify(entry.detail)
126
126
  );
127
- runSql$3(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
127
+ runSql$4(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
128
128
  };
129
129
  const readAuditLog = (sql, options = {}) => {
130
130
  ensureAuditTable(sql);
131
131
  const sinceSeq = options.sinceSeq ?? 0;
132
132
  const limit = Math.max(1, Math.min(options.limit ?? AUDIT_LOG_RETENTION, 1e4));
133
- const rows = runSql$3(
133
+ const rows = runSql$4(
134
134
  sql,
135
135
  `SELECT seq, ts, op, "table", id, detail FROM "${AUDIT_LOG_TABLE}" WHERE seq > ? ORDER BY seq DESC LIMIT ?`,
136
136
  sinceSeq,
@@ -183,6 +183,99 @@ const buildBatchEntryRequest = (batchRequest, entry) => {
183
183
  });
184
184
  };
185
185
 
186
+ const ISSUE_STATE_TABLE = "__lunora_issue_state__";
187
+ const HASH_QUERY_BATCH = 100;
188
+ const ISSUE_STATUSES = ["ignored", "open", "resolved"];
189
+ const ISSUE_SEVERITIES = ["critical", "high", "low", "medium"];
190
+ const runSql$3 = (sql, query, ...parameters) => {
191
+ const runner = sql.exec;
192
+ return runner.call(sql, query, ...parameters);
193
+ };
194
+ const SQL_NULL = null;
195
+ const orNull$1 = (value) => value ?? SQL_NULL;
196
+ const ensureIssueStateTable = (sql) => {
197
+ runSql$3(
198
+ sql,
199
+ `CREATE TABLE IF NOT EXISTS "${ISSUE_STATE_TABLE}" (
200
+ hash TEXT PRIMARY KEY,
201
+ status TEXT NOT NULL DEFAULT 'open',
202
+ assignee TEXT,
203
+ severity TEXT,
204
+ updated_at REAL NOT NULL,
205
+ updated_by TEXT
206
+ )`
207
+ );
208
+ };
209
+ const hydrate = (row) => {
210
+ return {
211
+ ...row.assignee === null ? {} : { assignee: row.assignee },
212
+ hash: row.hash,
213
+ ...row.severity === null ? {} : { severity: row.severity },
214
+ status: row.status,
215
+ updatedAt: row.updated_at,
216
+ ...row.updated_by === null ? {} : { updatedBy: row.updated_by }
217
+ };
218
+ };
219
+ const readIssueStates = (sql, hashes) => {
220
+ const states = /* @__PURE__ */ new Map();
221
+ if (hashes.length === 0) {
222
+ return states;
223
+ }
224
+ ensureIssueStateTable(sql);
225
+ for (let start = 0; start < hashes.length; start += HASH_QUERY_BATCH) {
226
+ const batch = hashes.slice(start, start + HASH_QUERY_BATCH);
227
+ const placeholders = batch.map(() => "?").join(", ");
228
+ const rows = runSql$3(
229
+ sql,
230
+ `SELECT hash, status, assignee, severity, updated_at, updated_by FROM "${ISSUE_STATE_TABLE}" WHERE hash IN (${placeholders})`,
231
+ ...batch
232
+ ).toArray();
233
+ for (const row of rows) {
234
+ states.set(row.hash, hydrate(row));
235
+ }
236
+ }
237
+ return states;
238
+ };
239
+ const upsertIssueState = (sql, hash, patch, updatedAt, updatedBy) => {
240
+ ensureIssueStateTable(sql);
241
+ const status = orNull$1(patch.status);
242
+ const assignee = orNull$1(patch.assignee);
243
+ const clearAssignee = patch.assignee === null ? 1 : 0;
244
+ const severity = orNull$1(patch.severity);
245
+ const clearSeverity = patch.severity === null ? 1 : 0;
246
+ const actor = orNull$1(updatedBy);
247
+ runSql$3(
248
+ sql,
249
+ `INSERT INTO "${ISSUE_STATE_TABLE}" (hash, status, assignee, severity, updated_at, updated_by)
250
+ VALUES (?, COALESCE(?, 'open'), ?, ?, ?, ?)
251
+ ON CONFLICT(hash) DO UPDATE SET
252
+ status = COALESCE(?, status),
253
+ assignee = CASE WHEN ? = 1 THEN NULL ELSE COALESCE(?, assignee) END,
254
+ severity = CASE WHEN ? = 1 THEN NULL ELSE COALESCE(?, severity) END,
255
+ updated_at = ?,
256
+ updated_by = ?`,
257
+ hash,
258
+ status,
259
+ assignee,
260
+ severity,
261
+ updatedAt,
262
+ actor,
263
+ status,
264
+ clearAssignee,
265
+ assignee,
266
+ clearSeverity,
267
+ severity,
268
+ updatedAt,
269
+ actor
270
+ );
271
+ const [row] = runSql$3(
272
+ sql,
273
+ `SELECT hash, status, assignee, severity, updated_at, updated_by FROM "${ISSUE_STATE_TABLE}" WHERE hash = ?`,
274
+ hash
275
+ ).toArray();
276
+ return row === void 0 ? { hash, status: status ?? "open", updatedAt, ...updatedBy === void 0 ? {} : { updatedBy } } : hydrate(row);
277
+ };
278
+
186
279
  const DEFAULT_CAPACITY$1 = 256;
187
280
  const seriesKey = (event) => `${event.kind}\0${event.name}\0${stableStringify(event.attributes ?? {})}`;
188
281
  class MetricBuffer {
@@ -1452,6 +1545,23 @@ const readRequestLog = (sql, options = {}) => {
1452
1545
  return base;
1453
1546
  });
1454
1547
  };
1548
+ const applyIssueStates = (sql, issues) => {
1549
+ const states = readIssueStates(sql, [...issues.keys()]);
1550
+ for (const issue of issues.values()) {
1551
+ const state = states.get(issue.hash);
1552
+ if (state === void 0) {
1553
+ continue;
1554
+ }
1555
+ issue.stateUpdatedAt = state.updatedAt;
1556
+ if (state.assignee !== void 0) {
1557
+ issue.assignee = state.assignee;
1558
+ }
1559
+ if (state.severity !== void 0) {
1560
+ issue.severity = state.severity;
1561
+ }
1562
+ issue.status = state.status === "resolved" && issue.lastSeen > state.updatedAt ? "open" : state.status;
1563
+ }
1564
+ };
1455
1565
  const readErrorIssues = (sql, options = {}) => {
1456
1566
  ensureRequestLogTable(sql);
1457
1567
  const limit = Math.max(1, Math.min(options.limit ?? REQUEST_LOG_RETENTION, 1e4));
@@ -1483,7 +1593,7 @@ const readErrorIssues = (sql, options = {}) => {
1483
1593
  const { culprit, hash, title } = fingerprintError({ functionPath: row.function_path, message });
1484
1594
  const existing = issues.get(hash);
1485
1595
  if (existing === void 0) {
1486
- issues.set(hash, { count: 1, culprit, firstSeen: row.ts, hash, lastSeen: row.ts, sampleMessage: message, title });
1596
+ issues.set(hash, { count: 1, culprit, firstSeen: row.ts, hash, lastSeen: row.ts, sampleMessage: message, status: "open", title });
1487
1597
  sampleTs.set(hash, row.ts);
1488
1598
  continue;
1489
1599
  }
@@ -1496,7 +1606,10 @@ const readErrorIssues = (sql, options = {}) => {
1496
1606
  existing.title = title;
1497
1607
  }
1498
1608
  }
1499
- return [...issues.values()].toSorted((a, b) => b.lastSeen - a.lastSeen);
1609
+ applyIssueStates(sql, issues);
1610
+ const folded = [...issues.values()];
1611
+ const filtered = options.status === void 0 ? folded : folded.filter((issue) => issue.status === options.status);
1612
+ return filtered.toSorted((a, b) => b.lastSeen - a.lastSeen);
1500
1613
  };
1501
1614
 
1502
1615
  const runSocketPool = async (items, processOne, concurrency = 8) => {
@@ -1761,6 +1874,36 @@ const parseWriteRowArgs = (args) => {
1761
1874
  }
1762
1875
  return { doc: record, id, op, table };
1763
1876
  };
1877
+ const isIssueStatus = (value) => typeof value === "string" && ISSUE_STATUSES.includes(value);
1878
+ const isIssueSeverity = (value) => typeof value === "string" && ISSUE_SEVERITIES.includes(value);
1879
+ const parseIssueHash = (args) => {
1880
+ const hash = typeof args["hash"] === "string" ? args["hash"].trim() : "";
1881
+ if (hash === "") {
1882
+ throw new LunoraError("BAD_REQUEST", "issue triage: `hash` is required");
1883
+ }
1884
+ return hash;
1885
+ };
1886
+ const CLEAR = null;
1887
+ const parseAssigneeArgument = (args) => {
1888
+ const raw = args["assignee"];
1889
+ if (raw === null) {
1890
+ return CLEAR;
1891
+ }
1892
+ if (typeof raw === "string" && raw.trim() !== "") {
1893
+ return raw;
1894
+ }
1895
+ throw new LunoraError("BAD_REQUEST", "assignIssue: `assignee` must be a non-empty string (assign) or null (unassign)");
1896
+ };
1897
+ const parseSeverityArgument = (args) => {
1898
+ const raw = args["severity"];
1899
+ if (raw === null) {
1900
+ return CLEAR;
1901
+ }
1902
+ if (isIssueSeverity(raw)) {
1903
+ return raw;
1904
+ }
1905
+ throw new LunoraError("BAD_REQUEST", "setIssueSeverity: `severity` must be one of critical|high|medium|low, or null to clear");
1906
+ };
1764
1907
  const parseCreateWorkflowInstanceArgs = (args) => {
1765
1908
  const exportName = typeof args["exportName"] === "string" ? args["exportName"].trim() : "";
1766
1909
  if (exportName === "") {
@@ -4955,8 +5098,63 @@ class ShardDO {
4955
5098
  if (functionPath === ADMIN_FUNCTIONS.listFlags) {
4956
5099
  return this.handleListFlags(args);
4957
5100
  }
5101
+ const triaged = await this.handleIssueTriageOp(functionPath, args);
5102
+ if (triaged !== void 0) {
5103
+ return triaged;
5104
+ }
4958
5105
  return this.handlePitrAdminOp(functionPath, args);
4959
5106
  }
5107
+ /**
5108
+ * Serve the four Issue-triage admin writes — `resolveIssue` / `ignoreIssue`
5109
+ * (a status change), `assignIssue` (set/clear an owner), `setIssueSeverity`
5110
+ * (tag/clear severity). Each upserts one row in the reserved
5111
+ * `__lunora_issue_state__` side table keyed by the Issue's fingerprint
5112
+ * `hash`, then re-derives at read time in {@link readErrorIssues}. Returns
5113
+ * `undefined` for any path it doesn't own so `handleExtraAdminOp` falls
5114
+ * through. Admin-gated by `handleAdminRpc`'s caller (the `LUNORA_ADMIN_TOKEN`
5115
+ * bearer); a bad/missing `hash` (or a bad status/severity value) is a 400.
5116
+ *
5117
+ * The write lands through raw SQL the change-tracker can't observe, so it
5118
+ * marks {@link ISSUE_STATE_TABLE} changed and flushes — the live Issues
5119
+ * subscription is an admin-wildcard memo that re-runs whenever a flush finds
5120
+ * a changed table, so the triage shows up without an unrelated write.
5121
+ */
5122
+ async handleIssueTriageOp(functionPath, args) {
5123
+ const patch = this.parseIssueTriagePatch(functionPath, args);
5124
+ if (patch === void 0) {
5125
+ return void 0;
5126
+ }
5127
+ const hash = parseIssueHash(args);
5128
+ const updatedBy = typeof args["updatedBy"] === "string" ? args["updatedBy"] : void 0;
5129
+ const sql = this.state.storage.sql;
5130
+ const state = upsertIssueState(sql, hash, patch, Date.now(), updatedBy);
5131
+ this.recordChangedTable(ISSUE_STATE_TABLE);
5132
+ await this.flushChangedTables();
5133
+ this.recordAudit(functionPath.slice(ADMIN_FUNCTION_PREFIX.length), { detail: { ...patch, hash } });
5134
+ return jsonResponse({ result: { state } }, 200);
5135
+ }
5136
+ /**
5137
+ * Map an Issue-triage admin path to the `IssueStatePatch` it applies, or
5138
+ * `undefined` when the path isn't a triage write. `assignIssue`/
5139
+ * `setIssueSeverity` accept an explicit `null` to CLEAR the field (unassign /
5140
+ * untag); a missing or malformed value is a 400 rather than a silent no-op.
5141
+ */
5142
+ // eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `parse*`/handler seams
5143
+ parseIssueTriagePatch(functionPath, args) {
5144
+ if (functionPath === ADMIN_FUNCTIONS.resolveIssue) {
5145
+ return { status: "resolved" };
5146
+ }
5147
+ if (functionPath === ADMIN_FUNCTIONS.ignoreIssue) {
5148
+ return { status: "ignored" };
5149
+ }
5150
+ if (functionPath === ADMIN_FUNCTIONS.assignIssue) {
5151
+ return { assignee: parseAssigneeArgument(args), status: "open" };
5152
+ }
5153
+ if (functionPath === ADMIN_FUNCTIONS.setIssueSeverity) {
5154
+ return { severity: parseSeverityArgument(args) };
5155
+ }
5156
+ return void 0;
5157
+ }
4960
5158
  /**
4961
5159
  * Record one app-level auth attempt for the auth-failure SLO (PLAN3 §2.3).
4962
5160
  * The worker calls this fire-and-forget (via `waitUntil`) after a top-level
@@ -5729,6 +5927,7 @@ class ShardDO {
5729
5927
  functionPathPrefix: typeof args["functionPathPrefix"] === "string" ? args["functionPathPrefix"] : void 0,
5730
5928
  limit: typeof args["limit"] === "number" ? args["limit"] : void 0,
5731
5929
  shardKey: typeof args["shardKey"] === "string" ? args["shardKey"] : void 0,
5930
+ status: isIssueStatus(args["status"]) ? args["status"] : void 0,
5732
5931
  userId: typeof args["userId"] === "string" ? args["userId"] : void 0
5733
5932
  })
5734
5933
  };
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-CqVmUVNs.mjs';
2
+ import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-DVk02KpP.mjs';
3
3
 
4
4
  const serveRelationFanout = async (schema, database, functionPath, args) => {
5
5
  const table = typeof args["table"] === "string" ? args["table"] : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.35",
3
+ "version": "1.0.0-alpha.37",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",