@mastra/factory 0.26.0-alpha.1 → 0.26.0-alpha.3

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 (180) hide show
  1. package/README.md +74 -7
  2. package/dist/boards/define-board.d.ts +19 -0
  3. package/dist/boards/define-board.d.ts.map +1 -1
  4. package/dist/boards/define-board.js +41 -9
  5. package/dist/boards/define-board.js.map +1 -1
  6. package/dist/boards/project-boards.d.ts +78 -0
  7. package/dist/boards/project-boards.d.ts.map +1 -0
  8. package/dist/boards/project-boards.js +194 -0
  9. package/dist/boards/project-boards.js.map +1 -0
  10. package/dist/boards/review.js +2 -2
  11. package/dist/boards/review.js.map +1 -1
  12. package/dist/boards/stored-board.d.ts +101 -0
  13. package/dist/boards/stored-board.d.ts.map +1 -0
  14. package/dist/boards/stored-board.js +139 -0
  15. package/dist/boards/stored-board.js.map +1 -0
  16. package/dist/boards/work.d.ts.map +1 -1
  17. package/dist/boards/work.js +6 -3
  18. package/dist/boards/work.js.map +1 -1
  19. package/dist/capabilities/intake.d.ts +6 -0
  20. package/dist/capabilities/intake.d.ts.map +1 -1
  21. package/dist/environment/build.d.ts +1 -1
  22. package/dist/environment/build.d.ts.map +1 -1
  23. package/dist/environment/build.js +2 -0
  24. package/dist/environment/build.js.map +1 -1
  25. package/dist/factory.d.ts +15 -2
  26. package/dist/factory.d.ts.map +1 -1
  27. package/dist/factory.js +51 -12
  28. package/dist/factory.js.map +1 -1
  29. package/dist/integrations/github/integration.d.ts +5 -4
  30. package/dist/integrations/github/integration.d.ts.map +1 -1
  31. package/dist/integrations/github/integration.js +10 -8
  32. package/dist/integrations/github/integration.js.map +1 -1
  33. package/dist/integrations/github/sandbox.d.ts +2 -2
  34. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  35. package/dist/integrations/github/sandbox.js +6 -1
  36. package/dist/integrations/github/sandbox.js.map +1 -1
  37. package/dist/integrations/jira/binding-scope.d.ts +30 -0
  38. package/dist/integrations/jira/binding-scope.d.ts.map +1 -0
  39. package/dist/integrations/jira/binding-scope.js +50 -0
  40. package/dist/integrations/jira/binding-scope.js.map +1 -0
  41. package/dist/integrations/jira/claim.d.ts +12 -0
  42. package/dist/integrations/jira/claim.d.ts.map +1 -1
  43. package/dist/integrations/jira/claim.js +24 -1
  44. package/dist/integrations/jira/claim.js.map +1 -1
  45. package/dist/integrations/jira/intake-jql.d.ts +32 -0
  46. package/dist/integrations/jira/intake-jql.d.ts.map +1 -0
  47. package/dist/integrations/jira/intake-jql.js +57 -0
  48. package/dist/integrations/jira/intake-jql.js.map +1 -0
  49. package/dist/integrations/jira/integration.d.ts +4 -3
  50. package/dist/integrations/jira/integration.d.ts.map +1 -1
  51. package/dist/integrations/jira/integration.js +14 -21
  52. package/dist/integrations/jira/integration.js.map +1 -1
  53. package/dist/integrations/jira/routes.d.ts +3 -0
  54. package/dist/integrations/jira/routes.d.ts.map +1 -1
  55. package/dist/integrations/jira/routes.js +16 -29
  56. package/dist/integrations/jira/routes.js.map +1 -1
  57. package/dist/integrations/jira/rules.d.ts.map +1 -1
  58. package/dist/integrations/jira/rules.js +8 -13
  59. package/dist/integrations/jira/rules.js.map +1 -1
  60. package/dist/integrations/platform/github/integration.d.ts +5 -0
  61. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  62. package/dist/integrations/platform/github/integration.js +7 -0
  63. package/dist/integrations/platform/github/integration.js.map +1 -1
  64. package/dist/integrations/platform/jira/integration.d.ts +7 -1
  65. package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
  66. package/dist/integrations/platform/jira/integration.js +26 -24
  67. package/dist/integrations/platform/jira/integration.js.map +1 -1
  68. package/dist/integrations/platform/jira/routes.d.ts +3 -0
  69. package/dist/integrations/platform/jira/routes.d.ts.map +1 -1
  70. package/dist/integrations/platform/jira/routes.js +17 -28
  71. package/dist/integrations/platform/jira/routes.js.map +1 -1
  72. package/dist/routes/attention-providers.d.ts +2 -1
  73. package/dist/routes/attention-providers.d.ts.map +1 -1
  74. package/dist/routes/attention-providers.js +3 -2
  75. package/dist/routes/attention-providers.js.map +1 -1
  76. package/dist/routes/attention.d.ts +2 -1
  77. package/dist/routes/attention.d.ts.map +1 -1
  78. package/dist/routes/attention.js.map +1 -1
  79. package/dist/routes/boards.d.ts +30 -0
  80. package/dist/routes/boards.d.ts.map +1 -0
  81. package/dist/routes/boards.js +272 -0
  82. package/dist/routes/boards.js.map +1 -0
  83. package/dist/routes/contracts.d.ts +249 -0
  84. package/dist/routes/contracts.d.ts.map +1 -1
  85. package/dist/routes/contracts.js +107 -35
  86. package/dist/routes/contracts.js.map +1 -1
  87. package/dist/routes/intake.d.ts +15 -10
  88. package/dist/routes/intake.d.ts.map +1 -1
  89. package/dist/routes/intake.js +109 -22
  90. package/dist/routes/intake.js.map +1 -1
  91. package/dist/routes/projects.d.ts +3 -0
  92. package/dist/routes/projects.d.ts.map +1 -1
  93. package/dist/routes/projects.js +10 -1
  94. package/dist/routes/projects.js.map +1 -1
  95. package/dist/routes/surface.d.ts +10 -3
  96. package/dist/routes/surface.d.ts.map +1 -1
  97. package/dist/routes/surface.js +13 -1
  98. package/dist/routes/surface.js.map +1 -1
  99. package/dist/routes/tenant-credentials.d.ts +9 -0
  100. package/dist/routes/tenant-credentials.d.ts.map +1 -1
  101. package/dist/routes/tenant-credentials.js +24 -2
  102. package/dist/routes/tenant-credentials.js.map +1 -1
  103. package/dist/routes/work-items.d.ts +3 -1
  104. package/dist/routes/work-items.d.ts.map +1 -1
  105. package/dist/routes/work-items.js +64 -42
  106. package/dist/routes/work-items.js.map +1 -1
  107. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  108. package/dist/rules/dispatch-errors.js +4 -0
  109. package/dist/rules/dispatch-errors.js.map +1 -1
  110. package/dist/rules/dispatcher.d.ts +7 -1
  111. package/dist/rules/dispatcher.d.ts.map +1 -1
  112. package/dist/rules/dispatcher.js +70 -34
  113. package/dist/rules/dispatcher.js.map +1 -1
  114. package/dist/rules/processor.d.ts +3 -1
  115. package/dist/rules/processor.d.ts.map +1 -1
  116. package/dist/rules/processor.js +11 -6
  117. package/dist/rules/processor.js.map +1 -1
  118. package/dist/rules/tools.d.ts +2 -1
  119. package/dist/rules/tools.d.ts.map +1 -1
  120. package/dist/rules/tools.js +2 -1
  121. package/dist/rules/tools.js.map +1 -1
  122. package/dist/rules/transition-service.d.ts +3 -1
  123. package/dist/rules/transition-service.d.ts.map +1 -1
  124. package/dist/rules/transition-service.js +12 -12
  125. package/dist/rules/transition-service.js.map +1 -1
  126. package/dist/sandbox/local-factory-sandbox.d.ts +4 -1
  127. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
  128. package/dist/sandbox/local-factory-sandbox.js +1 -1
  129. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  130. package/dist/storage/domains/audit/actions.d.ts +1 -0
  131. package/dist/storage/domains/audit/actions.d.ts.map +1 -1
  132. package/dist/storage/domains/audit/actions.js +6 -1
  133. package/dist/storage/domains/audit/actions.js.map +1 -1
  134. package/dist/storage/domains/boards/base.d.ts +110 -0
  135. package/dist/storage/domains/boards/base.d.ts.map +1 -0
  136. package/dist/storage/domains/boards/base.js +292 -0
  137. package/dist/storage/domains/boards/base.js.map +1 -0
  138. package/dist/storage/domains/boards/schemas.d.ts +11 -0
  139. package/dist/storage/domains/boards/schemas.d.ts.map +1 -0
  140. package/dist/storage/domains/boards/schemas.js +106 -0
  141. package/dist/storage/domains/boards/schemas.js.map +1 -0
  142. package/dist/storage/domains/credentials/base.d.ts +4 -1
  143. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  144. package/dist/storage/domains/credentials/base.js +5 -2
  145. package/dist/storage/domains/credentials/base.js.map +1 -1
  146. package/dist/storage/domains/intake/base.d.ts +86 -5
  147. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  148. package/dist/storage/domains/intake/base.js +298 -59
  149. package/dist/storage/domains/intake/base.js.map +1 -1
  150. package/dist/storage/domains/projects/base.d.ts +2 -1
  151. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  152. package/dist/storage/domains/projects/base.js +15 -10
  153. package/dist/storage/domains/projects/base.js.map +1 -1
  154. package/dist/storage/domains/work-items/base.d.ts +16 -2
  155. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  156. package/dist/storage/domains/work-items/base.js +40 -11
  157. package/dist/storage/domains/work-items/base.js.map +1 -1
  158. package/dist/tools/read-tools.d.ts +3 -1
  159. package/dist/tools/read-tools.d.ts.map +1 -1
  160. package/dist/tools/read-tools.js +3 -2
  161. package/dist/tools/read-tools.js.map +1 -1
  162. package/dist/tools/session-tools.d.ts +3 -1
  163. package/dist/tools/session-tools.d.ts.map +1 -1
  164. package/dist/tools/session-tools.js +2 -1
  165. package/dist/tools/session-tools.js.map +1 -1
  166. package/dist/tools/sessions-state-processor.d.ts +3 -1
  167. package/dist/tools/sessions-state-processor.d.ts.map +1 -1
  168. package/dist/tools/sessions-state-processor.js +2 -1
  169. package/dist/tools/sessions-state-processor.js.map +1 -1
  170. package/dist/tools/work-item-tools.d.ts +3 -1
  171. package/dist/tools/work-item-tools.d.ts.map +1 -1
  172. package/dist/tools/work-item-tools.js +3 -2
  173. package/dist/tools/work-item-tools.js.map +1 -1
  174. package/dist/workspace.d.ts +2 -0
  175. package/dist/workspace.d.ts.map +1 -1
  176. package/dist/workspace.js +14 -5
  177. package/dist/workspace.js.map +1 -1
  178. package/factory-skills/factory-plan/SKILL.md +1 -1
  179. package/factory-skills/factory-triage/SKILL.md +18 -7
  180. package/package.json +9 -9
@@ -0,0 +1,292 @@
1
+ import { FACTORY_BOARDS_SCHEMA, FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA, FACTORY_BOARD_PHASES_SCHEMA } from "./schemas.js";
2
+ import { randomUUID } from "crypto";
3
+ import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
4
+ // src/storage/domains/boards/base.ts
5
+ var StoredBoardError = class extends Error {
6
+ code;
7
+ name = "StoredBoardError";
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ }
12
+ };
13
+ const BOARDS = "factory_boards";
14
+ const PHASES = "factory_board_phases";
15
+ const VERSIONS = "factory_board_definition_versions";
16
+ const MAX_WRITE_ATTEMPTS = 3;
17
+ function scopeWhere({ orgId, factoryProjectId }) {
18
+ return {
19
+ org_id: orgId,
20
+ factory_project_id: factoryProjectId
21
+ };
22
+ }
23
+ function toPhaseRecord(row) {
24
+ return {
25
+ key: row.phase_key,
26
+ position: row.position,
27
+ name: row.name,
28
+ description: row.description,
29
+ kind: row.kind,
30
+ runner: row.runner,
31
+ nextPhases: row.next_phases,
32
+ autoStart: row.auto_start,
33
+ autoStartWhen: row.auto_start_when,
34
+ exitTool: row.exit_tool
35
+ };
36
+ }
37
+ function toRecord(row, phases) {
38
+ return {
39
+ id: row.id,
40
+ orgId: row.org_id,
41
+ factoryProjectId: row.factory_project_id,
42
+ key: row.board_key,
43
+ name: row.name,
44
+ description: row.description,
45
+ position: row.position,
46
+ revision: row.revision,
47
+ createdBy: row.created_by,
48
+ createdAt: row.created_at,
49
+ updatedBy: row.updated_by,
50
+ updatedAt: row.updated_at,
51
+ phases: phases.sort((left, right) => left.position - right.position).map(toPhaseRecord)
52
+ };
53
+ }
54
+ /** A compare-and-set inside a write lost to a concurrent writer; the write is retried from fresh reads. */
55
+ var ConcurrentBoardWrite = class extends Error {};
56
+ /**
57
+ * Each project's boards and their ordered phases. Every write runs in one transaction that also
58
+ * replaces the project's row in `factory_board_definition_versions`, so caches on other
59
+ * replicas can detect it.
60
+ */
61
+ var StoredBoardsStorage = class extends FactoryStorageDomain {
62
+ constructor() {
63
+ super("boards");
64
+ }
65
+ async init() {
66
+ await this.ensureCollections([
67
+ FACTORY_BOARDS_SCHEMA,
68
+ FACTORY_BOARD_PHASES_SCHEMA,
69
+ FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA
70
+ ]);
71
+ }
72
+ async dangerouslyClearAll() {
73
+ await this.ops.deleteMany(PHASES, {});
74
+ await this.ops.deleteMany(BOARDS, {});
75
+ await this.ops.deleteMany(VERSIONS, {});
76
+ }
77
+ /** The project's current definition version; null before its first board write. */
78
+ async readVersion(scope) {
79
+ return readVersion(this.ops, scope);
80
+ }
81
+ /** The project's boards in order. */
82
+ async list(scope) {
83
+ return readBoards(this.ops, scope);
84
+ }
85
+ /**
86
+ * The version and the boards, read in that order: a write landing between the two reads leaves a
87
+ * version older than the rows, so a cache keyed on it rereads on its next check instead of
88
+ * keeping the stale rows.
89
+ */
90
+ async snapshot(scope) {
91
+ return {
92
+ version: await readVersion(this.ops, scope),
93
+ boards: await this.list(scope)
94
+ };
95
+ }
96
+ async get(scope, key) {
97
+ const row = await this.ops.findOne(BOARDS, {
98
+ ...scopeWhere(scope),
99
+ board_key: key
100
+ });
101
+ if (!row) return null;
102
+ return toRecord(row, await this.ops.findMany(PHASES, {
103
+ ...scopeWhere(scope),
104
+ board_key: key
105
+ }));
106
+ }
107
+ /** Creates a board through the API. */
108
+ async create(scope, input, actor) {
109
+ await ensureVersionRow(this.ops, scope);
110
+ try {
111
+ return await this.#write(scope, async (ops) => {
112
+ const rows = await ops.findMany(BOARDS, scopeWhere(scope));
113
+ if (rows.some((row) => row.board_key === input.key)) throw new StoredBoardError("board_exists", `Board "${input.key}" already exists in this project.`);
114
+ await insertBoard(ops, scope, input, {
115
+ position: nextPosition(rows),
116
+ actor
117
+ });
118
+ return {
119
+ result: void 0,
120
+ changed: true
121
+ };
122
+ }).then(async () => await this.get(scope, input.key));
123
+ } catch (error) {
124
+ if (error instanceof UniqueViolationError && error.collection === BOARDS) throw new StoredBoardError("board_exists", `Board "${input.key}" already exists in this project.`);
125
+ throw error;
126
+ }
127
+ }
128
+ /** Replaces a board's name, description and phases. The board key never changes. */
129
+ async replace(scope, key, input, options) {
130
+ await this.#write(scope, async (ops) => {
131
+ const row = await findBoard(ops, scope, key);
132
+ checkRevision(row, key, options.expectedRevision);
133
+ await writeBoard(ops, scope, row, {
134
+ key,
135
+ ...input
136
+ }, options.actor);
137
+ return {
138
+ result: void 0,
139
+ changed: true
140
+ };
141
+ });
142
+ return await this.get(scope, key);
143
+ }
144
+ /** Deletes a board and its phases. */
145
+ async delete(scope, key, options) {
146
+ await this.#write(scope, async (ops) => {
147
+ const row = await findBoard(ops, scope, key);
148
+ checkRevision(row, key, options.expectedRevision);
149
+ if (await ops.deleteMany(BOARDS, {
150
+ id: row.id,
151
+ revision: row.revision
152
+ }) === 0) throw new ConcurrentBoardWrite();
153
+ await ops.deleteMany(PHASES, {
154
+ ...scopeWhere(scope),
155
+ board_key: key
156
+ });
157
+ return {
158
+ result: void 0,
159
+ changed: true
160
+ };
161
+ });
162
+ }
163
+ /**
164
+ * Runs `fn` in a transaction and replaces the project's version when it changed anything.
165
+ * Lost races (a compare-and-set miss, or a concurrent write that inserted the same board or the
166
+ * project's first version row) retry from fresh reads.
167
+ */
168
+ async #write(scope, fn) {
169
+ for (let attempt = 1;; attempt++) try {
170
+ return await this.storage.withTransaction(async (ops) => {
171
+ const { result, changed } = await fn(ops);
172
+ if (changed) await bumpVersion(ops, scope);
173
+ return result;
174
+ });
175
+ } catch (error) {
176
+ if (!(error instanceof ConcurrentBoardWrite || error instanceof UniqueViolationError) || attempt >= MAX_WRITE_ATTEMPTS) throw error;
177
+ }
178
+ }
179
+ };
180
+ async function readVersion(ops, scope) {
181
+ return (await ops.findOne(VERSIONS, scopeWhere(scope)))?.version ?? null;
182
+ }
183
+ async function readBoards(ops, scope) {
184
+ const [rows, phases] = await Promise.all([ops.findMany(BOARDS, scopeWhere(scope)), ops.findMany(PHASES, scopeWhere(scope))]);
185
+ const byBoard = /* @__PURE__ */ new Map();
186
+ for (const phase of phases) {
187
+ const list = byBoard.get(phase.board_key) ?? [];
188
+ list.push(phase);
189
+ byBoard.set(phase.board_key, list);
190
+ }
191
+ return rows.sort((left, right) => left.position - right.position || left.board_key.localeCompare(right.board_key)).map((row) => toRecord(row, byBoard.get(row.board_key) ?? []));
192
+ }
193
+ /**
194
+ * Inserts the project's version row if it has none. This runs outside the write transaction: on
195
+ * Postgres, two transactions racing to insert it would leave the loser's transaction aborted, so
196
+ * the version bump inside a write must always be an update.
197
+ */
198
+ async function ensureVersionRow(ops, scope) {
199
+ if (await ops.findOne(VERSIONS, scopeWhere(scope))) return;
200
+ try {
201
+ await ops.insertOne(VERSIONS, {
202
+ ...scopeWhere(scope),
203
+ version: randomUUID(),
204
+ updated_at: /* @__PURE__ */ new Date()
205
+ });
206
+ } catch (error) {
207
+ if (!(error instanceof UniqueViolationError)) throw error;
208
+ }
209
+ }
210
+ async function bumpVersion(ops, scope) {
211
+ await ops.upsertOne(VERSIONS, ["org_id", "factory_project_id"], {
212
+ ...scopeWhere(scope),
213
+ version: randomUUID(),
214
+ updated_at: /* @__PURE__ */ new Date()
215
+ });
216
+ }
217
+ async function findBoard(ops, scope, key) {
218
+ const row = await ops.findOne(BOARDS, {
219
+ ...scopeWhere(scope),
220
+ board_key: key
221
+ });
222
+ if (!row) throw new StoredBoardError("board_not_found", `Board "${key}" does not exist in this project.`);
223
+ return row;
224
+ }
225
+ function checkRevision(row, key, expectedRevision) {
226
+ if (row.revision !== expectedRevision) throw new StoredBoardError("revision_mismatch", `Board "${key}" is at revision ${row.revision}, not ${expectedRevision}; reload it and try again.`);
227
+ }
228
+ function nextPosition(rows) {
229
+ return rows.reduce((max, row) => Math.max(max, row.position), -1) + 1;
230
+ }
231
+ /** Sets columns on a board row and raises its revision, unless another writer changed it first. */
232
+ async function updateRow(ops, row, set) {
233
+ if (await ops.updateMany(BOARDS, {
234
+ id: row.id,
235
+ revision: row.revision
236
+ }, {
237
+ ...set,
238
+ revision: row.revision + 1,
239
+ updated_at: /* @__PURE__ */ new Date()
240
+ }) !== 1) throw new ConcurrentBoardWrite();
241
+ }
242
+ async function insertPhases(ops, scope, input) {
243
+ for (const [position, phase] of input.phases.entries()) await ops.insertOne(PHASES, {
244
+ ...scopeWhere(scope),
245
+ board_key: input.key,
246
+ phase_key: phase.key,
247
+ position,
248
+ name: phase.name,
249
+ description: phase.description ?? null,
250
+ kind: phase.kind,
251
+ runner: phase.runner ?? null,
252
+ next_phases: phase.nextPhases ?? null,
253
+ auto_start: phase.autoStart ?? false,
254
+ auto_start_when: phase.autoStartWhen ?? null,
255
+ exit_tool: phase.exitTool ?? null
256
+ });
257
+ }
258
+ async function insertBoard(ops, scope, input, options) {
259
+ const now = /* @__PURE__ */ new Date();
260
+ await ops.insertOne(BOARDS, {
261
+ ...scopeWhere(scope),
262
+ board_key: input.key,
263
+ name: input.name,
264
+ description: input.description ?? null,
265
+ position: options.position,
266
+ revision: 1,
267
+ created_by: options.actor,
268
+ created_at: now,
269
+ updated_by: options.actor,
270
+ updated_at: now
271
+ });
272
+ await insertPhases(ops, scope, input);
273
+ }
274
+ /** Replaces a board's definition and phases in place, keeping its key, position and history. */
275
+ async function writeBoard(ops, scope, row, input, actor) {
276
+ await updateRow(ops, row, {
277
+ name: input.name,
278
+ description: input.description ?? null,
279
+ updated_by: actor
280
+ });
281
+ await ops.deleteMany(PHASES, {
282
+ ...scopeWhere(scope),
283
+ board_key: row.board_key
284
+ });
285
+ await insertPhases(ops, scope, {
286
+ ...input,
287
+ key: row.board_key
288
+ });
289
+ }
290
+ export { StoredBoardError, StoredBoardsStorage };
291
+
292
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.js","names":[],"sources":["../../../../src/storage/domains/boards/base.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { FactoryStorageOps } from '@mastra/core/storage';\n\nimport type { RuleGroup } from '../../../rules/rule-group.js';\nimport {\n FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA,\n FACTORY_BOARD_PHASES_SCHEMA,\n FACTORY_BOARDS_SCHEMA,\n} from './schemas.js';\n\n/** The project a board belongs to. */\nexport interface StoredBoardScope {\n orgId: string;\n factoryProjectId: string;\n}\n\nexport type StoredPhaseKind = 'resting' | 'working' | 'terminal';\n\n/** Who runs a working phase. Factory runs every phase with its code agent. */\nexport interface StoredPhaseRunner {\n skill?: string;\n context?: string;\n instructions?: string;\n}\n\nexport interface StoredPhaseExitTool {\n id: string;\n autoApprove?: boolean;\n autoApproveWhen?: RuleGroup;\n}\n\n/** A phase as it is written. `key` is the stage id cards sit in. */\nexport interface StoredPhaseInput {\n key: string;\n name: string;\n description?: string | null;\n kind: StoredPhaseKind;\n runner?: StoredPhaseRunner | null;\n /** Unset or `null` on a resting or working phase means every other phase; `[]` means none. */\n nextPhases?: string[] | null;\n autoStart?: boolean;\n autoStartWhen?: RuleGroup | null;\n exitTool?: StoredPhaseExitTool | null;\n}\n\n/** A board as it is written. Phase order is significant: the first phase is where cards start. */\nexport interface StoredBoardInput {\n key: string;\n name: string;\n description?: string | null;\n phases: StoredPhaseInput[];\n}\n\nexport interface StoredPhaseRecord {\n key: string;\n position: number;\n name: string;\n description: string | null;\n kind: StoredPhaseKind;\n runner: StoredPhaseRunner | null;\n nextPhases: string[] | null;\n autoStart: boolean;\n autoStartWhen: RuleGroup | null;\n exitTool: StoredPhaseExitTool | null;\n}\n\nexport interface StoredBoardRecord {\n id: string;\n orgId: string;\n factoryProjectId: string;\n key: string;\n name: string;\n description: string | null;\n position: number;\n revision: number;\n createdBy: string;\n createdAt: Date;\n updatedBy: string;\n updatedAt: Date;\n phases: StoredPhaseRecord[];\n}\n\nexport interface StoredBoardSnapshot {\n version: string | null;\n boards: StoredBoardRecord[];\n}\n\nexport type StoredBoardErrorCode = 'board_exists' | 'board_not_found' | 'revision_mismatch';\n\nexport class StoredBoardError extends Error {\n override name = 'StoredBoardError';\n constructor(\n readonly code: StoredBoardErrorCode,\n message: string,\n ) {\n super(message);\n }\n}\n\ninterface BoardRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n board_key: string;\n name: string;\n description: string | null;\n position: number;\n revision: number;\n created_by: string;\n created_at: Date;\n updated_by: string;\n updated_at: Date;\n}\n\ninterface PhaseRow extends Record<string, unknown> {\n id: string;\n org_id: string;\n factory_project_id: string;\n board_key: string;\n phase_key: string;\n position: number;\n name: string;\n description: string | null;\n kind: StoredPhaseKind;\n runner: StoredPhaseRunner | null;\n next_phases: string[] | null;\n auto_start: boolean;\n auto_start_when: RuleGroup | null;\n exit_tool: StoredPhaseExitTool | null;\n}\n\nconst BOARDS = 'factory_boards';\nconst PHASES = 'factory_board_phases';\nconst VERSIONS = 'factory_board_definition_versions';\nconst MAX_WRITE_ATTEMPTS = 3;\n\nfunction scopeWhere({ orgId, factoryProjectId }: StoredBoardScope) {\n return { org_id: orgId, factory_project_id: factoryProjectId };\n}\n\nfunction toPhaseRecord(row: PhaseRow): StoredPhaseRecord {\n return {\n key: row.phase_key,\n position: row.position,\n name: row.name,\n description: row.description,\n kind: row.kind,\n runner: row.runner,\n nextPhases: row.next_phases,\n autoStart: row.auto_start,\n autoStartWhen: row.auto_start_when,\n exitTool: row.exit_tool,\n };\n}\n\nfunction toRecord(row: BoardRow, phases: PhaseRow[]): StoredBoardRecord {\n return {\n id: row.id,\n orgId: row.org_id,\n factoryProjectId: row.factory_project_id,\n key: row.board_key,\n name: row.name,\n description: row.description,\n position: row.position,\n revision: row.revision,\n createdBy: row.created_by,\n createdAt: row.created_at,\n updatedBy: row.updated_by,\n updatedAt: row.updated_at,\n phases: phases.sort((left, right) => left.position - right.position).map(toPhaseRecord),\n };\n}\n\n/** A compare-and-set inside a write lost to a concurrent writer; the write is retried from fresh reads. */\nclass ConcurrentBoardWrite extends Error {}\n\n/**\n * Each project's boards and their ordered phases. Every write runs in one transaction that also\n * replaces the project's row in `factory_board_definition_versions`, so caches on other\n * replicas can detect it.\n */\nexport class StoredBoardsStorage extends FactoryStorageDomain {\n constructor() {\n super('boards');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([\n FACTORY_BOARDS_SCHEMA,\n FACTORY_BOARD_PHASES_SCHEMA,\n FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA,\n ]);\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany(PHASES, {});\n await this.ops.deleteMany(BOARDS, {});\n await this.ops.deleteMany(VERSIONS, {});\n }\n\n /** The project's current definition version; null before its first board write. */\n async readVersion(scope: StoredBoardScope): Promise<string | null> {\n return readVersion(this.ops, scope);\n }\n\n /** The project's boards in order. */\n async list(scope: StoredBoardScope): Promise<StoredBoardRecord[]> {\n return readBoards(this.ops, scope);\n }\n\n /**\n * The version and the boards, read in that order: a write landing between the two reads leaves a\n * version older than the rows, so a cache keyed on it rereads on its next check instead of\n * keeping the stale rows.\n */\n async snapshot(scope: StoredBoardScope): Promise<StoredBoardSnapshot> {\n const version = await readVersion(this.ops, scope);\n return { version, boards: await this.list(scope) };\n }\n\n async get(scope: StoredBoardScope, key: string): Promise<StoredBoardRecord | null> {\n const row = await this.ops.findOne<BoardRow>(BOARDS, { ...scopeWhere(scope), board_key: key });\n if (!row) return null;\n const phases = await this.ops.findMany<PhaseRow>(PHASES, { ...scopeWhere(scope), board_key: key });\n return toRecord(row, phases);\n }\n\n /** Creates a board through the API. */\n async create(scope: StoredBoardScope, input: StoredBoardInput, actor: string): Promise<StoredBoardRecord> {\n await ensureVersionRow(this.ops, scope);\n try {\n return await this.#write(scope, async ops => {\n const rows = await ops.findMany<BoardRow>(BOARDS, scopeWhere(scope));\n if (rows.some(row => row.board_key === input.key)) {\n throw new StoredBoardError('board_exists', `Board \"${input.key}\" already exists in this project.`);\n }\n await insertBoard(ops, scope, input, { position: nextPosition(rows), actor });\n return { result: undefined, changed: true };\n }).then(async () => (await this.get(scope, input.key))!);\n } catch (error) {\n // Only the board key's index means the board exists; a lost race on another table is not that.\n if (error instanceof UniqueViolationError && error.collection === BOARDS) {\n throw new StoredBoardError('board_exists', `Board \"${input.key}\" already exists in this project.`);\n }\n throw error;\n }\n }\n\n /** Replaces a board's name, description and phases. The board key never changes. */\n async replace(\n scope: StoredBoardScope,\n key: string,\n input: Omit<StoredBoardInput, 'key'>,\n options: { expectedRevision: number; actor: string },\n ): Promise<StoredBoardRecord> {\n await this.#write(scope, async ops => {\n const row = await findBoard(ops, scope, key);\n checkRevision(row, key, options.expectedRevision);\n await writeBoard(ops, scope, row, { key, ...input }, options.actor);\n return { result: undefined, changed: true };\n });\n return (await this.get(scope, key))!;\n }\n\n /** Deletes a board and its phases. */\n async delete(scope: StoredBoardScope, key: string, options: { expectedRevision: number }): Promise<void> {\n await this.#write(scope, async ops => {\n const row = await findBoard(ops, scope, key);\n checkRevision(row, key, options.expectedRevision);\n if ((await ops.deleteMany(BOARDS, { id: row.id, revision: row.revision })) === 0)\n throw new ConcurrentBoardWrite();\n await ops.deleteMany(PHASES, { ...scopeWhere(scope), board_key: key });\n return { result: undefined, changed: true };\n });\n }\n\n /**\n * Runs `fn` in a transaction and replaces the project's version when it changed anything.\n * Lost races (a compare-and-set miss, or a concurrent write that inserted the same board or the\n * project's first version row) retry from fresh reads.\n */\n async #write<T>(\n scope: StoredBoardScope,\n fn: (ops: FactoryStorageOps) => Promise<{ result: T; changed: boolean }>,\n ): Promise<T> {\n for (let attempt = 1; ; attempt++) {\n try {\n // Read committed is enough: every row change is a compare-and-set on `revision` and every\n // insert meets the unique index. Serializable isolation would also fail writes to\n // unrelated projects that replicas run side by side.\n return await this.storage.withTransaction(async ops => {\n const { result, changed } = await fn(ops);\n if (changed) await bumpVersion(ops, scope);\n return result;\n });\n } catch (error) {\n const retryable = error instanceof ConcurrentBoardWrite || error instanceof UniqueViolationError;\n if (!retryable || attempt >= MAX_WRITE_ATTEMPTS) throw error;\n }\n }\n }\n}\n\nasync function readVersion(ops: FactoryStorageOps, scope: StoredBoardScope): Promise<string | null> {\n const row = await ops.findOne<{ version: string }>(VERSIONS, scopeWhere(scope));\n return row?.version ?? null;\n}\n\nasync function readBoards(ops: FactoryStorageOps, scope: StoredBoardScope): Promise<StoredBoardRecord[]> {\n const [rows, phases] = await Promise.all([\n ops.findMany<BoardRow>(BOARDS, scopeWhere(scope)),\n ops.findMany<PhaseRow>(PHASES, scopeWhere(scope)),\n ]);\n const byBoard = new Map<string, PhaseRow[]>();\n for (const phase of phases) {\n const list = byBoard.get(phase.board_key) ?? [];\n list.push(phase);\n byBoard.set(phase.board_key, list);\n }\n return rows\n .sort((left, right) => left.position - right.position || left.board_key.localeCompare(right.board_key))\n .map(row => toRecord(row, byBoard.get(row.board_key) ?? []));\n}\n\n/**\n * Inserts the project's version row if it has none. This runs outside the write transaction: on\n * Postgres, two transactions racing to insert it would leave the loser's transaction aborted, so\n * the version bump inside a write must always be an update.\n */\nasync function ensureVersionRow(ops: FactoryStorageOps, scope: StoredBoardScope): Promise<void> {\n if (await ops.findOne(VERSIONS, scopeWhere(scope))) return;\n try {\n await ops.insertOne(VERSIONS, { ...scopeWhere(scope), version: randomUUID(), updated_at: new Date() });\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n }\n}\n\nasync function bumpVersion(ops: FactoryStorageOps, scope: StoredBoardScope): Promise<void> {\n await ops.upsertOne(VERSIONS, ['org_id', 'factory_project_id'], {\n ...scopeWhere(scope),\n version: randomUUID(),\n updated_at: new Date(),\n });\n}\n\nasync function findBoard(ops: FactoryStorageOps, scope: StoredBoardScope, key: string): Promise<BoardRow> {\n const row = await ops.findOne<BoardRow>(BOARDS, { ...scopeWhere(scope), board_key: key });\n if (!row) {\n throw new StoredBoardError('board_not_found', `Board \"${key}\" does not exist in this project.`);\n }\n return row;\n}\n\nfunction checkRevision(row: BoardRow, key: string, expectedRevision: number): void {\n if (row.revision !== expectedRevision) {\n throw new StoredBoardError(\n 'revision_mismatch',\n `Board \"${key}\" is at revision ${row.revision}, not ${expectedRevision}; reload it and try again.`,\n );\n }\n}\n\nfunction nextPosition(rows: BoardRow[]): number {\n return rows.reduce((max, row) => Math.max(max, row.position), -1) + 1;\n}\n\n/** Sets columns on a board row and raises its revision, unless another writer changed it first. */\nasync function updateRow(ops: FactoryStorageOps, row: BoardRow, set: Partial<BoardRow>): Promise<void> {\n const updated = await ops.updateMany(\n BOARDS,\n { id: row.id, revision: row.revision },\n { ...set, revision: row.revision + 1, updated_at: new Date() },\n );\n if (updated !== 1) throw new ConcurrentBoardWrite();\n}\n\nasync function insertPhases(ops: FactoryStorageOps, scope: StoredBoardScope, input: StoredBoardInput): Promise<void> {\n for (const [position, phase] of input.phases.entries()) {\n await ops.insertOne<PhaseRow>(PHASES, {\n ...scopeWhere(scope),\n board_key: input.key,\n phase_key: phase.key,\n position,\n name: phase.name,\n description: phase.description ?? null,\n kind: phase.kind,\n runner: phase.runner ?? null,\n next_phases: phase.nextPhases ?? null,\n auto_start: phase.autoStart ?? false,\n auto_start_when: phase.autoStartWhen ?? null,\n exit_tool: phase.exitTool ?? null,\n });\n }\n}\n\nasync function insertBoard(\n ops: FactoryStorageOps,\n scope: StoredBoardScope,\n input: StoredBoardInput,\n options: { position: number; actor: string },\n): Promise<void> {\n const now = new Date();\n await ops.insertOne<BoardRow>(BOARDS, {\n ...scopeWhere(scope),\n board_key: input.key,\n name: input.name,\n description: input.description ?? null,\n position: options.position,\n revision: 1,\n created_by: options.actor,\n created_at: now,\n updated_by: options.actor,\n updated_at: now,\n });\n await insertPhases(ops, scope, input);\n}\n\n/** Replaces a board's definition and phases in place, keeping its key, position and history. */\nasync function writeBoard(\n ops: FactoryStorageOps,\n scope: StoredBoardScope,\n row: BoardRow,\n input: StoredBoardInput,\n actor: string,\n): Promise<void> {\n await updateRow(ops, row, { name: input.name, description: input.description ?? null, updated_by: actor });\n await ops.deleteMany(PHASES, { ...scopeWhere(scope), board_key: row.board_key });\n await insertPhases(ops, scope, { ...input, key: row.board_key });\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { CollectionSchema } from '@mastra/core/storage';
2
+ /** A project's boards. `board_key` is the id cards reference (`work_items.board`). */
3
+ export declare const FACTORY_BOARDS_SCHEMA: CollectionSchema;
4
+ /** A board's ordered phases. `phase_key` is the stage id cards sit in. */
5
+ export declare const FACTORY_BOARD_PHASES_SCHEMA: CollectionSchema;
6
+ /**
7
+ * One row per project whose `version` is replaced inside every board write, so other replicas
8
+ * can tell their cached boards are stale with one indexed read.
9
+ */
10
+ export declare const FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA: CollectionSchema;
11
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/boards/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,sFAAsF;AACtF,eAAO,MAAM,qBAAqB,EAAE,gBAmBnC,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,2BAA2B,EAAE,gBA0BzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,EAAE,gBAUtD,CAAC"}
@@ -0,0 +1,106 @@
1
+ // src/storage/domains/boards/schemas.ts
2
+ /** A project's boards. `board_key` is the id cards reference (`work_items.board`). */
3
+ const FACTORY_BOARDS_SCHEMA = {
4
+ name: "factory_boards",
5
+ columns: {
6
+ id: { type: "uuid-pk" },
7
+ org_id: { type: "text" },
8
+ factory_project_id: { type: "text" },
9
+ board_key: { type: "text" },
10
+ name: { type: "text" },
11
+ description: {
12
+ type: "text",
13
+ nullable: true
14
+ },
15
+ position: { type: "integer" },
16
+ revision: { type: "integer" },
17
+ created_by: { type: "text" },
18
+ created_at: { type: "timestamp" },
19
+ updated_by: { type: "text" },
20
+ updated_at: { type: "timestamp" }
21
+ },
22
+ uniqueIndexes: [{
23
+ name: "factory_boards_project_key",
24
+ columns: [
25
+ "org_id",
26
+ "factory_project_id",
27
+ "board_key"
28
+ ]
29
+ }],
30
+ indexes: [{
31
+ name: "factory_boards_project",
32
+ columns: ["org_id", "factory_project_id"]
33
+ }]
34
+ };
35
+ /** A board's ordered phases. `phase_key` is the stage id cards sit in. */
36
+ const FACTORY_BOARD_PHASES_SCHEMA = {
37
+ name: "factory_board_phases",
38
+ columns: {
39
+ id: { type: "uuid-pk" },
40
+ org_id: { type: "text" },
41
+ factory_project_id: { type: "text" },
42
+ board_key: { type: "text" },
43
+ phase_key: { type: "text" },
44
+ position: { type: "integer" },
45
+ name: { type: "text" },
46
+ description: {
47
+ type: "text",
48
+ nullable: true
49
+ },
50
+ kind: { type: "text" },
51
+ runner: {
52
+ type: "json",
53
+ nullable: true
54
+ },
55
+ next_phases: {
56
+ type: "json",
57
+ nullable: true
58
+ },
59
+ auto_start: {
60
+ type: "boolean",
61
+ default: false
62
+ },
63
+ auto_start_when: {
64
+ type: "json",
65
+ nullable: true
66
+ },
67
+ exit_tool: {
68
+ type: "json",
69
+ nullable: true
70
+ }
71
+ },
72
+ uniqueIndexes: [{
73
+ name: "factory_board_phases_project_board_phase",
74
+ columns: [
75
+ "org_id",
76
+ "factory_project_id",
77
+ "board_key",
78
+ "phase_key"
79
+ ]
80
+ }],
81
+ indexes: [{
82
+ name: "factory_board_phases_project",
83
+ columns: ["org_id", "factory_project_id"]
84
+ }]
85
+ };
86
+ /**
87
+ * One row per project whose `version` is replaced inside every board write, so other replicas
88
+ * can tell their cached boards are stale with one indexed read.
89
+ */
90
+ const FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA = {
91
+ name: "factory_board_definition_versions",
92
+ columns: {
93
+ id: { type: "uuid-pk" },
94
+ org_id: { type: "text" },
95
+ factory_project_id: { type: "text" },
96
+ version: { type: "text" },
97
+ updated_at: { type: "timestamp" }
98
+ },
99
+ uniqueIndexes: [{
100
+ name: "factory_board_definition_versions_project",
101
+ columns: ["org_id", "factory_project_id"]
102
+ }]
103
+ };
104
+ export { FACTORY_BOARDS_SCHEMA, FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA, FACTORY_BOARD_PHASES_SCHEMA };
105
+
106
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","names":[],"sources":["../../../../src/storage/domains/boards/schemas.ts"],"sourcesContent":["import type { CollectionSchema } from '@mastra/core/storage';\n\n/** A project's boards. `board_key` is the id cards reference (`work_items.board`). */\nexport const FACTORY_BOARDS_SCHEMA: CollectionSchema = {\n name: 'factory_boards',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n board_key: { type: 'text' },\n name: { type: 'text' },\n description: { type: 'text', nullable: true },\n position: { type: 'integer' },\n // Rises on every write to the board or its phases; the compare-and-set token for edits.\n revision: { type: 'integer' },\n created_by: { type: 'text' },\n created_at: { type: 'timestamp' },\n updated_by: { type: 'text' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'factory_boards_project_key', columns: ['org_id', 'factory_project_id', 'board_key'] }],\n indexes: [{ name: 'factory_boards_project', columns: ['org_id', 'factory_project_id'] }],\n};\n\n/** A board's ordered phases. `phase_key` is the stage id cards sit in. */\nexport const FACTORY_BOARD_PHASES_SCHEMA: CollectionSchema = {\n name: 'factory_board_phases',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n board_key: { type: 'text' },\n phase_key: { type: 'text' },\n position: { type: 'integer' },\n name: { type: 'text' },\n description: { type: 'text', nullable: true },\n kind: { type: 'text' },\n // { skill?, context?, instructions? }; room for other runner types later.\n runner: { type: 'json', nullable: true },\n next_phases: { type: 'json', nullable: true },\n auto_start: { type: 'boolean', default: false },\n auto_start_when: { type: 'json', nullable: true },\n exit_tool: { type: 'json', nullable: true },\n },\n uniqueIndexes: [\n {\n name: 'factory_board_phases_project_board_phase',\n columns: ['org_id', 'factory_project_id', 'board_key', 'phase_key'],\n },\n ],\n indexes: [{ name: 'factory_board_phases_project', columns: ['org_id', 'factory_project_id'] }],\n};\n\n/**\n * One row per project whose `version` is replaced inside every board write, so other replicas\n * can tell their cached boards are stale with one indexed read.\n */\nexport const FACTORY_BOARD_DEFINITION_VERSIONS_SCHEMA: CollectionSchema = {\n name: 'factory_board_definition_versions',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n factory_project_id: { type: 'text' },\n version: { type: 'text' },\n updated_at: { type: 'timestamp' },\n },\n uniqueIndexes: [{ name: 'factory_board_definition_versions_project', columns: ['org_id', 'factory_project_id'] }],\n};\n"],"mappings":";;AAGA,MAAa,wBAA0C;CACrD,MAAM;CACN,SAAS;EACP,IAAI,EAAE,MAAM,UAAU;EACtB,QAAQ,EAAE,MAAM,OAAO;EACvB,oBAAoB,EAAE,MAAM,OAAO;EACnC,WAAW,EAAE,MAAM,OAAO;EAC1B,MAAM,EAAE,MAAM,OAAO;EACrB,aAAa;GAAE,MAAM;GAAQ,UAAU;EAAK;EAC5C,UAAU,EAAE,MAAM,UAAU;EAE5B,UAAU,EAAE,MAAM,UAAU;EAC5B,YAAY,EAAE,MAAM,OAAO;EAC3B,YAAY,EAAE,MAAM,YAAY;EAChC,YAAY,EAAE,MAAM,OAAO;EAC3B,YAAY,EAAE,MAAM,YAAY;CAClC;CACA,eAAe,CAAC;EAAE,MAAM;EAA8B,SAAS;GAAC;GAAU;GAAsB;EAAW;CAAE,CAAC;CAC9G,SAAS,CAAC;EAAE,MAAM;EAA0B,SAAS,CAAC,UAAU,oBAAoB;CAAE,CAAC;AACzF;;AAGA,MAAa,8BAAgD;CAC3D,MAAM;CACN,SAAS;EACP,IAAI,EAAE,MAAM,UAAU;EACtB,QAAQ,EAAE,MAAM,OAAO;EACvB,oBAAoB,EAAE,MAAM,OAAO;EACnC,WAAW,EAAE,MAAM,OAAO;EAC1B,WAAW,EAAE,MAAM,OAAO;EAC1B,UAAU,EAAE,MAAM,UAAU;EAC5B,MAAM,EAAE,MAAM,OAAO;EACrB,aAAa;GAAE,MAAM;GAAQ,UAAU;EAAK;EAC5C,MAAM,EAAE,MAAM,OAAO;EAErB,QAAQ;GAAE,MAAM;GAAQ,UAAU;EAAK;EACvC,aAAa;GAAE,MAAM;GAAQ,UAAU;EAAK;EAC5C,YAAY;GAAE,MAAM;GAAW,SAAS;EAAM;EAC9C,iBAAiB;GAAE,MAAM;GAAQ,UAAU;EAAK;EAChD,WAAW;GAAE,MAAM;GAAQ,UAAU;EAAK;CAC5C;CACA,eAAe,CACb;EACE,MAAM;EACN,SAAS;GAAC;GAAU;GAAsB;GAAa;EAAW;CACpE,CACF;CACA,SAAS,CAAC;EAAE,MAAM;EAAgC,SAAS,CAAC,UAAU,oBAAoB;CAAE,CAAC;AAC/F;;;;;AAMA,MAAa,2CAA6D;CACxE,MAAM;CACN,SAAS;EACP,IAAI,EAAE,MAAM,UAAU;EACtB,QAAQ,EAAE,MAAM,OAAO;EACvB,oBAAoB,EAAE,MAAM,OAAO;EACnC,SAAS,EAAE,MAAM,OAAO;EACxB,YAAY,EAAE,MAAM,YAAY;CAClC;CACA,eAAe,CAAC;EAAE,MAAM;EAA6C,SAAS,CAAC,UAAU,oBAAoB;CAAE,CAAC;AAClH"}
@@ -113,8 +113,11 @@ export declare class ModelCredentialsStorage extends FactoryStorageDomain {
113
113
  * skipped and the fresh credential is returned. Returns `undefined` when no
114
114
  * OAuth row exists for the tenant. A failed `refreshFn` is recorded on the
115
115
  * row (surfaced as `CredentialRecord.refreshFailedAt`) and rethrown.
116
+ *
117
+ * `needsRefresh` replaces the expiry check, e.g. to replace an access token
118
+ * the provider rejected before its recorded expiry.
116
119
  */
117
- refreshOAuth(tenant: CredentialTenant, provider: string, refreshFn: (current: OAuthCredential) => Promise<OAuthCredential>): Promise<OAuthCredential | undefined>;
120
+ refreshOAuth(tenant: CredentialTenant, provider: string, refreshFn: (current: OAuthCredential) => Promise<OAuthCredential>, needsRefresh?: (current: OAuthCredential) => boolean): Promise<OAuthCredential | undefined>;
118
121
  /** Persist a pending login flow started by a web route. */
119
122
  createLoginSession(input: CreateLoginSessionInput): Promise<LoginSessionRow>;
120
123
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/credentials/base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/storage/domains/credentials/base.ts"],"names":[],"mappings":"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"}
@@ -200,15 +200,18 @@ var ModelCredentialsStorage = class extends FactoryStorageDomain {
200
200
  * skipped and the fresh credential is returned. Returns `undefined` when no
201
201
  * OAuth row exists for the tenant. A failed `refreshFn` is recorded on the
202
202
  * row (surfaced as `CredentialRecord.refreshFailedAt`) and rethrown.
203
+ *
204
+ * `needsRefresh` replaces the expiry check, e.g. to replace an access token
205
+ * the provider rejected before its recorded expiry.
203
206
  */
204
- async refreshOAuth(tenant, provider, refreshFn) {
207
+ async refreshOAuth(tenant, provider, refreshFn, needsRefresh = (current) => isOAuthCredentialExpired(current)) {
205
208
  let result;
206
209
  let failure;
207
210
  await this.#db.updateAtomic("model_provider_credentials", tenantWhere(tenant, provider), async (row) => {
208
211
  const decrypted = await this.encryption.decrypt(row.data);
209
212
  if (decrypted.value.type !== "oauth") return null;
210
213
  const current = decrypted.value;
211
- if (!isOAuthCredentialExpired(current)) {
214
+ if (!needsRefresh(current)) {
212
215
  result = current;
213
216
  return decrypted.needsReencryption ? { data: await this.encryption.encrypt(current) } : null;
214
217
  }
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","names":[],"sources":["../../../../src/storage/domains/credentials/base.ts"],"sourcesContent":["/**\n * Model provider credentials domain — tenant-scoped OAuth tokens and API keys\n * for model providers (Anthropic/Claude Max, OpenAI Codex, GitHub Copilot,\n * xAI, plus plain API-key providers).\n *\n * Tenancy is two-level:\n * - **user-scoped** rows (`userId` present): personal plan OAuth tokens and\n * personal API keys. OAuth plan tokens are ONLY ever user-scoped — they are\n * personal subscriptions, and sharing one would bill/rate-limit a single\n * account for the whole org.\n * - **org-scoped** rows (`userId` absent): shared API keys set by an org\n * admin, inherited by every member.\n *\n * Model calls resolve exactly one scope: user or org. Deployed tenant stores\n * never fall back to the other scope or implicitly borrow server environment keys.\n *\n * Secrets posture: credentials are stored server-side only and never returned\n * to the client (same as `linear_connections.access_token`). Encryption at\n * rest is out of scope, matching the existing posture.\n *\n * The domain also owns `oauth_login_sessions`: pending web login flows\n * (paste-code PKCE verifiers, device-code poll state) persisted server-side so\n * any replica can complete or poll a flow started on another.\n */\n\nimport type { AuthCredential, OAuthCredential } from '@mastra/code-sdk/auth/types';\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, CollectionWhere, FactoryStorageOps } from '@mastra/core/storage';\nimport { createPlaintextFactorySecretEncryption } from '../../../secret-encryption.js';\nimport type { FactorySecretEncryption } from '../../../secret-encryption.js';\n\n/** Owning tenant of a credential row. `userId` absent = org-scoped row. */\nexport interface CredentialTenant {\n orgId: string;\n userId?: string;\n}\n\n/** Which tenancy level a credential row lives at. */\nexport type CredentialScope = 'user' | 'org';\n\n/** One stored credential, annotated with its scope. */\nexport interface CredentialRecord {\n provider: string;\n scope: CredentialScope;\n credential: AuthCredential;\n updatedAt: Date;\n /** When the last OAuth token refresh failed; cleared by the next successful refresh or a new sign-in. */\n refreshFailedAt?: Date;\n}\n\n/** Result of per-caller resolution: the winning credential and its scope. */\nexport interface ResolvedCredential {\n provider: string;\n scope: CredentialScope;\n credential: AuthCredential;\n}\n\n/** Kind of pending web login flow a session row tracks. */\nexport type LoginSessionKind = 'paste-code' | 'device-code';\n\n/** Scope the completed credential should be written to. */\nexport type LoginCredentialScope = 'user' | 'org';\n\n/** One pending OAuth login flow, persisted so any replica can continue it. */\nexport interface LoginSessionRow {\n sessionId: string;\n orgId: string;\n userId: string;\n provider: string;\n kind: LoginSessionKind;\n /** Where the completed credential lands; absent rows default to `'user'`. */\n credentialScope?: LoginCredentialScope;\n /** Serialized flow state (PKCE verifier, device-code pending state, ...). */\n pending: Record<string, unknown>;\n expiresAt: Date;\n /** Earliest time the next upstream poll is allowed (device-code flows). */\n nextPollAt: Date | null;\n createdAt: Date;\n}\n\nexport interface CreateLoginSessionInput {\n sessionId: string;\n orgId: string;\n userId: string;\n provider: string;\n kind: LoginSessionKind;\n credentialScope?: LoginCredentialScope;\n pending: Record<string, unknown>;\n expiresAt: Date;\n nextPollAt?: Date | null;\n}\n\n/** Mirror of `AuthStorage.getApiKey()`'s expiry check. */\nexport function isOAuthCredentialExpired(credential: OAuthCredential, now = Date.now()): boolean {\n return now >= credential.expires;\n}\n\nexport const MODEL_CREDENTIALS_SCHEMA: CollectionSchema = {\n name: 'model_provider_credentials',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n user_id: { type: 'text', nullable: true },\n provider: { type: 'text' },\n type: { type: 'text' },\n data: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n refresh_failed_at: { type: 'timestamp', nullable: true },\n },\n uniqueIndexes: [\n // Scope is encoded in `user_id` nullability (NULL = org-scoped shared\n // row), enforced by two partial unique indexes.\n {\n name: 'model_provider_credentials_user_unique',\n columns: ['org_id', 'user_id', 'provider'],\n whereNotNull: 'user_id',\n },\n { name: 'model_provider_credentials_org_unique', columns: ['org_id', 'provider'], whereNull: 'user_id' },\n ],\n};\n\nexport const OAUTH_LOGIN_SESSIONS_SCHEMA: CollectionSchema = {\n name: 'oauth_login_sessions',\n columns: {\n session_id: { type: 'text', primaryKey: true },\n org_id: { type: 'text' },\n user_id: { type: 'text' },\n provider: { type: 'text' },\n kind: { type: 'text' },\n credential_scope: { type: 'text', nullable: true },\n pending: { type: 'json' },\n expires_at: { type: 'timestamp' },\n next_poll_at: { type: 'timestamp', nullable: true },\n created_at: { type: 'timestamp' },\n },\n};\n\n/** Column shape of one `model_provider_credentials` row as returned by ops. */\ninterface CredentialDbRow extends Record<string, unknown> {\n id: string;\n provider: string;\n user_id: string | null;\n data: unknown;\n updated_at: Date;\n refresh_failed_at: Date | null;\n}\n\n/** Column shape of one `oauth_login_sessions` row as returned by ops. */\ninterface LoginSessionDbRow extends Record<string, unknown> {\n session_id: string;\n org_id: string;\n user_id: string;\n provider: string;\n kind: LoginSessionKind;\n credential_scope: LoginCredentialScope | null;\n pending: Record<string, unknown>;\n expires_at: Date;\n next_poll_at: Date | null;\n created_at: Date;\n}\n\nfunction toSessionRow(db: LoginSessionDbRow): LoginSessionRow {\n return {\n sessionId: db.session_id,\n orgId: db.org_id,\n userId: db.user_id,\n provider: db.provider,\n kind: db.kind,\n ...(db.credential_scope ? { credentialScope: db.credential_scope } : {}),\n pending: db.pending,\n expiresAt: db.expires_at,\n nextPollAt: db.next_poll_at,\n createdAt: db.created_at,\n };\n}\n\n/** Filter selecting exactly the tenant's row for a provider (`null` = org row). */\nfunction tenantWhere(tenant: CredentialTenant, provider: string): CollectionWhere {\n return { org_id: tenant.orgId, provider, user_id: tenant.userId ?? null };\n}\n\n/**\n * Model-credentials storage, written once against the generic\n * `FactoryStorageOps` surface. `refreshOAuth()` and `claimLoginSession()`\n * ride `updateAtomic` so concurrent replicas serialize instead of\n * invalidating each other's rotating tokens / double-claiming a flow.\n */\nexport class ModelCredentialsStorage extends FactoryStorageDomain {\n constructor(private readonly encryption: FactorySecretEncryption = createPlaintextFactorySecretEncryption()) {\n super('model-credentials');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([MODEL_CREDENTIALS_SCHEMA, OAUTH_LOGIN_SESSIONS_SCHEMA]);\n const rows = await this.ops.findMany<CredentialDbRow>('model_provider_credentials', {});\n await Promise.all(rows.map(row => this.#migrateCredential(row.id)));\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('oauth_login_sessions', {});\n await this.ops.deleteMany('model_provider_credentials', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n async #decryptCredential(value: unknown): Promise<AuthCredential> {\n return (await this.encryption.decrypt<AuthCredential>(value)).value;\n }\n\n async #toRecord(row: CredentialDbRow, scope: CredentialScope): Promise<CredentialRecord> {\n return {\n provider: row.provider,\n scope,\n credential: await this.#decryptCredential(row.data),\n updatedAt: row.updated_at,\n refreshFailedAt: row.refresh_failed_at ?? undefined,\n };\n }\n\n async #migrateCredential(id: string): Promise<void> {\n await this.#db.updateAtomic<CredentialDbRow>('model_provider_credentials', { id }, async row => {\n const decrypted = await this.encryption.decrypt<AuthCredential>(row.data);\n if (!decrypted.needsReencryption) return null;\n return { data: await this.encryption.encrypt(decrypted.value) };\n });\n }\n\n /** Read the tenant's credential for a provider at exactly that scope. */\n async getCredential(tenant: CredentialTenant, provider: string): Promise<AuthCredential | undefined> {\n const row = await this.#db.findOne<CredentialDbRow>('model_provider_credentials', tenantWhere(tenant, provider));\n return row ? this.#decryptCredential(row.data) : undefined;\n }\n\n /** Upsert the tenant's credential (`created_at` is preserved on update). */\n async setCredential(tenant: CredentialTenant, provider: string, credential: AuthCredential): Promise<void> {\n const where = tenantWhere(tenant, provider);\n const encrypted = await this.encryption.encrypt(credential);\n const update = async () =>\n this.#db.updateMany('model_provider_credentials', where, {\n type: credential.type,\n data: encrypted,\n updated_at: new Date(),\n refresh_failed_at: null,\n });\n\n let lastError: unknown;\n for (let attempt = 0; attempt < 2; attempt++) {\n if ((await update()) > 0) return;\n\n const now = new Date();\n try {\n await this.#db.insertOne<CredentialDbRow>('model_provider_credentials', {\n org_id: tenant.orgId,\n user_id: tenant.userId ?? null,\n provider,\n type: credential.type,\n data: encrypted,\n created_at: now,\n updated_at: now,\n });\n return;\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n lastError = error;\n }\n }\n throw lastError;\n }\n\n /** Delete the tenant's credential at exactly that scope. True when a row was removed. */\n async removeCredential(tenant: CredentialTenant, provider: string): Promise<boolean> {\n const deleted = await this.#db.deleteMany('model_provider_credentials', tenantWhere(tenant, provider));\n return deleted > 0;\n }\n\n /** All credentials visible to a user: their own rows plus the org's shared rows. */\n async listCredentials(orgId: string, userId: string): Promise<CredentialRecord[]> {\n // SQL `IN` never matches NULL, so the org rows need their own query.\n const [userRows, orgRows] = await Promise.all([\n this.#db.findMany<CredentialDbRow>('model_provider_credentials', { org_id: orgId, user_id: userId }),\n this.#db.findMany<CredentialDbRow>('model_provider_credentials', { org_id: orgId, user_id: null }),\n ]);\n return Promise.all([\n ...userRows.map(row => this.#toRecord(row, 'user')),\n ...orgRows.map(row => this.#toRecord(row, 'org')),\n ]);\n }\n\n /** Resolve a provider credential from exactly one tenancy scope. */\n async resolveCredential(\n orgId: string,\n userId: string,\n provider: string,\n scope: CredentialScope,\n ): Promise<ResolvedCredential | undefined> {\n const tenant = scope === 'org' ? { orgId } : { orgId, userId };\n const row = await this.#db.findOne<CredentialDbRow>('model_provider_credentials', tenantWhere(tenant, provider));\n return row ? { provider: row.provider, scope, credential: await this.#decryptCredential(row.data) } : undefined;\n }\n\n /**\n * Refresh the tenant's OAuth credential under the backend's atomic\n * read-modify-write so concurrent replicas don't invalidate each other's\n * rotating refresh tokens. The expiry is re-checked under the lock —\n * another replica may have refreshed already, in which case `refreshFn` is\n * skipped and the fresh credential is returned. Returns `undefined` when no\n * OAuth row exists for the tenant. A failed `refreshFn` is recorded on the\n * row (surfaced as `CredentialRecord.refreshFailedAt`) and rethrown.\n */\n async refreshOAuth(\n tenant: CredentialTenant,\n provider: string,\n refreshFn: (current: OAuthCredential) => Promise<OAuthCredential>,\n ): Promise<OAuthCredential | undefined> {\n let result: OAuthCredential | undefined;\n let failure: { error: unknown } | undefined;\n await this.#db.updateAtomic<CredentialDbRow>(\n 'model_provider_credentials',\n tenantWhere(tenant, provider),\n async row => {\n const decrypted = await this.encryption.decrypt<AuthCredential>(row.data);\n if (decrypted.value.type !== 'oauth') return null;\n const current = decrypted.value;\n // Re-check under the lock: another replica may have refreshed while we waited.\n if (!isOAuthCredentialExpired(current)) {\n result = current;\n return decrypted.needsReencryption ? { data: await this.encryption.encrypt(current) } : null;\n }\n try {\n result = await refreshFn(current);\n } catch (error) {\n failure = { error };\n return { refresh_failed_at: new Date() };\n }\n return { data: await this.encryption.encrypt(result), updated_at: new Date(), refresh_failed_at: null };\n },\n );\n if (failure) throw failure.error;\n return result;\n }\n\n /** Persist a pending login flow started by a web route. */\n async createLoginSession(input: CreateLoginSessionInput): Promise<LoginSessionRow> {\n const inserted = await this.#db.insertOne<LoginSessionDbRow>('oauth_login_sessions', {\n session_id: input.sessionId,\n org_id: input.orgId,\n user_id: input.userId,\n provider: input.provider,\n kind: input.kind,\n credential_scope: input.credentialScope ?? null,\n pending: input.pending,\n expires_at: input.expiresAt,\n next_poll_at: input.nextPollAt ?? null,\n created_at: new Date(),\n });\n return toSessionRow(inserted);\n }\n\n /**\n * Read a pending login flow. An expired session is deleted on read (TTL\n * cleanup) and reported as absent.\n */\n async getLoginSession(sessionId: string): Promise<LoginSessionRow | undefined> {\n const row = await this.#db.findOne<LoginSessionDbRow>('oauth_login_sessions', { session_id: sessionId });\n if (!row) return undefined;\n if (row.expires_at.getTime() <= Date.now()) {\n await this.#db.deleteMany('oauth_login_sessions', { session_id: sessionId });\n return undefined;\n }\n return toSessionRow(row);\n }\n\n /**\n * Atomically claim a due login session for one upstream completion/poll attempt.\n * Returns undefined when the session is missing, expired, owned by another\n * tenant/provider, or already claimed by a concurrent request.\n */\n async claimLoginSession(\n sessionId: string,\n owner: Pick<LoginSessionRow, 'orgId' | 'userId' | 'provider' | 'kind'>,\n ): Promise<LoginSessionRow | undefined> {\n const now = Date.now();\n let claimed = false;\n const updated = await this.#db.updateAtomic<LoginSessionDbRow>(\n 'oauth_login_sessions',\n { session_id: sessionId },\n row => {\n if (\n row.org_id !== owner.orgId ||\n row.user_id !== owner.userId ||\n row.provider !== owner.provider ||\n row.kind !== owner.kind\n ) {\n return null;\n }\n if (row.expires_at.getTime() <= now) return null;\n if (row.next_poll_at && row.next_poll_at.getTime() > now) return null;\n claimed = true;\n // Park the next poll at expiry so a concurrent claim loses until the\n // claimer either touches the session forward or deletes it.\n return { next_poll_at: row.expires_at };\n },\n );\n return claimed && updated ? toSessionRow(updated) : undefined;\n }\n\n /** Update a pending flow's serialized state and/or next allowed poll time. */\n async touchLoginSession(\n sessionId: string,\n updates: { pending?: Record<string, unknown>; nextPollAt?: Date | null },\n ): Promise<void> {\n const set: Record<string, unknown> = {};\n if (updates.pending !== undefined) set.pending = updates.pending;\n if (updates.nextPollAt !== undefined) set.next_poll_at = updates.nextPollAt;\n if (Object.keys(set).length === 0) return;\n await this.#db.updateMany('oauth_login_sessions', { session_id: sessionId }, set);\n }\n\n /** Remove a pending login flow (completed, cancelled, or failed). */\n async deleteLoginSession(sessionId: string): Promise<void> {\n await this.#db.deleteMany('oauth_login_sessions', { session_id: sessionId });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"base.js","names":[],"sources":["../../../../src/storage/domains/credentials/base.ts"],"sourcesContent":["/**\n * Model provider credentials domain — tenant-scoped OAuth tokens and API keys\n * for model providers (Anthropic/Claude Max, OpenAI Codex, GitHub Copilot,\n * xAI, plus plain API-key providers).\n *\n * Tenancy is two-level:\n * - **user-scoped** rows (`userId` present): personal plan OAuth tokens and\n * personal API keys. OAuth plan tokens are ONLY ever user-scoped — they are\n * personal subscriptions, and sharing one would bill/rate-limit a single\n * account for the whole org.\n * - **org-scoped** rows (`userId` absent): shared API keys set by an org\n * admin, inherited by every member.\n *\n * Model calls resolve exactly one scope: user or org. Deployed tenant stores\n * never fall back to the other scope or implicitly borrow server environment keys.\n *\n * Secrets posture: credentials are stored server-side only and never returned\n * to the client (same as `linear_connections.access_token`). Encryption at\n * rest is out of scope, matching the existing posture.\n *\n * The domain also owns `oauth_login_sessions`: pending web login flows\n * (paste-code PKCE verifiers, device-code poll state) persisted server-side so\n * any replica can complete or poll a flow started on another.\n */\n\nimport type { AuthCredential, OAuthCredential } from '@mastra/code-sdk/auth/types';\nimport { FactoryStorageDomain, UniqueViolationError } from '@mastra/core/storage';\nimport type { CollectionSchema, CollectionWhere, FactoryStorageOps } from '@mastra/core/storage';\nimport { createPlaintextFactorySecretEncryption } from '../../../secret-encryption.js';\nimport type { FactorySecretEncryption } from '../../../secret-encryption.js';\n\n/** Owning tenant of a credential row. `userId` absent = org-scoped row. */\nexport interface CredentialTenant {\n orgId: string;\n userId?: string;\n}\n\n/** Which tenancy level a credential row lives at. */\nexport type CredentialScope = 'user' | 'org';\n\n/** One stored credential, annotated with its scope. */\nexport interface CredentialRecord {\n provider: string;\n scope: CredentialScope;\n credential: AuthCredential;\n updatedAt: Date;\n /** When the last OAuth token refresh failed; cleared by the next successful refresh or a new sign-in. */\n refreshFailedAt?: Date;\n}\n\n/** Result of per-caller resolution: the winning credential and its scope. */\nexport interface ResolvedCredential {\n provider: string;\n scope: CredentialScope;\n credential: AuthCredential;\n}\n\n/** Kind of pending web login flow a session row tracks. */\nexport type LoginSessionKind = 'paste-code' | 'device-code';\n\n/** Scope the completed credential should be written to. */\nexport type LoginCredentialScope = 'user' | 'org';\n\n/** One pending OAuth login flow, persisted so any replica can continue it. */\nexport interface LoginSessionRow {\n sessionId: string;\n orgId: string;\n userId: string;\n provider: string;\n kind: LoginSessionKind;\n /** Where the completed credential lands; absent rows default to `'user'`. */\n credentialScope?: LoginCredentialScope;\n /** Serialized flow state (PKCE verifier, device-code pending state, ...). */\n pending: Record<string, unknown>;\n expiresAt: Date;\n /** Earliest time the next upstream poll is allowed (device-code flows). */\n nextPollAt: Date | null;\n createdAt: Date;\n}\n\nexport interface CreateLoginSessionInput {\n sessionId: string;\n orgId: string;\n userId: string;\n provider: string;\n kind: LoginSessionKind;\n credentialScope?: LoginCredentialScope;\n pending: Record<string, unknown>;\n expiresAt: Date;\n nextPollAt?: Date | null;\n}\n\n/** Mirror of `AuthStorage.getApiKey()`'s expiry check. */\nexport function isOAuthCredentialExpired(credential: OAuthCredential, now = Date.now()): boolean {\n return now >= credential.expires;\n}\n\nexport const MODEL_CREDENTIALS_SCHEMA: CollectionSchema = {\n name: 'model_provider_credentials',\n columns: {\n id: { type: 'uuid-pk' },\n org_id: { type: 'text' },\n user_id: { type: 'text', nullable: true },\n provider: { type: 'text' },\n type: { type: 'text' },\n data: { type: 'json' },\n created_at: { type: 'timestamp' },\n updated_at: { type: 'timestamp' },\n refresh_failed_at: { type: 'timestamp', nullable: true },\n },\n uniqueIndexes: [\n // Scope is encoded in `user_id` nullability (NULL = org-scoped shared\n // row), enforced by two partial unique indexes.\n {\n name: 'model_provider_credentials_user_unique',\n columns: ['org_id', 'user_id', 'provider'],\n whereNotNull: 'user_id',\n },\n { name: 'model_provider_credentials_org_unique', columns: ['org_id', 'provider'], whereNull: 'user_id' },\n ],\n};\n\nexport const OAUTH_LOGIN_SESSIONS_SCHEMA: CollectionSchema = {\n name: 'oauth_login_sessions',\n columns: {\n session_id: { type: 'text', primaryKey: true },\n org_id: { type: 'text' },\n user_id: { type: 'text' },\n provider: { type: 'text' },\n kind: { type: 'text' },\n credential_scope: { type: 'text', nullable: true },\n pending: { type: 'json' },\n expires_at: { type: 'timestamp' },\n next_poll_at: { type: 'timestamp', nullable: true },\n created_at: { type: 'timestamp' },\n },\n};\n\n/** Column shape of one `model_provider_credentials` row as returned by ops. */\ninterface CredentialDbRow extends Record<string, unknown> {\n id: string;\n provider: string;\n user_id: string | null;\n data: unknown;\n updated_at: Date;\n refresh_failed_at: Date | null;\n}\n\n/** Column shape of one `oauth_login_sessions` row as returned by ops. */\ninterface LoginSessionDbRow extends Record<string, unknown> {\n session_id: string;\n org_id: string;\n user_id: string;\n provider: string;\n kind: LoginSessionKind;\n credential_scope: LoginCredentialScope | null;\n pending: Record<string, unknown>;\n expires_at: Date;\n next_poll_at: Date | null;\n created_at: Date;\n}\n\nfunction toSessionRow(db: LoginSessionDbRow): LoginSessionRow {\n return {\n sessionId: db.session_id,\n orgId: db.org_id,\n userId: db.user_id,\n provider: db.provider,\n kind: db.kind,\n ...(db.credential_scope ? { credentialScope: db.credential_scope } : {}),\n pending: db.pending,\n expiresAt: db.expires_at,\n nextPollAt: db.next_poll_at,\n createdAt: db.created_at,\n };\n}\n\n/** Filter selecting exactly the tenant's row for a provider (`null` = org row). */\nfunction tenantWhere(tenant: CredentialTenant, provider: string): CollectionWhere {\n return { org_id: tenant.orgId, provider, user_id: tenant.userId ?? null };\n}\n\n/**\n * Model-credentials storage, written once against the generic\n * `FactoryStorageOps` surface. `refreshOAuth()` and `claimLoginSession()`\n * ride `updateAtomic` so concurrent replicas serialize instead of\n * invalidating each other's rotating tokens / double-claiming a flow.\n */\nexport class ModelCredentialsStorage extends FactoryStorageDomain {\n constructor(private readonly encryption: FactorySecretEncryption = createPlaintextFactorySecretEncryption()) {\n super('model-credentials');\n }\n\n async init(): Promise<void> {\n await this.ensureCollections([MODEL_CREDENTIALS_SCHEMA, OAUTH_LOGIN_SESSIONS_SCHEMA]);\n const rows = await this.ops.findMany<CredentialDbRow>('model_provider_credentials', {});\n await Promise.all(rows.map(row => this.#migrateCredential(row.id)));\n }\n\n async dangerouslyClearAll(): Promise<void> {\n await this.ops.deleteMany('oauth_login_sessions', {});\n await this.ops.deleteMany('model_provider_credentials', {});\n }\n\n get #db(): FactoryStorageOps {\n return this.ops;\n }\n\n async #decryptCredential(value: unknown): Promise<AuthCredential> {\n return (await this.encryption.decrypt<AuthCredential>(value)).value;\n }\n\n async #toRecord(row: CredentialDbRow, scope: CredentialScope): Promise<CredentialRecord> {\n return {\n provider: row.provider,\n scope,\n credential: await this.#decryptCredential(row.data),\n updatedAt: row.updated_at,\n refreshFailedAt: row.refresh_failed_at ?? undefined,\n };\n }\n\n async #migrateCredential(id: string): Promise<void> {\n await this.#db.updateAtomic<CredentialDbRow>('model_provider_credentials', { id }, async row => {\n const decrypted = await this.encryption.decrypt<AuthCredential>(row.data);\n if (!decrypted.needsReencryption) return null;\n return { data: await this.encryption.encrypt(decrypted.value) };\n });\n }\n\n /** Read the tenant's credential for a provider at exactly that scope. */\n async getCredential(tenant: CredentialTenant, provider: string): Promise<AuthCredential | undefined> {\n const row = await this.#db.findOne<CredentialDbRow>('model_provider_credentials', tenantWhere(tenant, provider));\n return row ? this.#decryptCredential(row.data) : undefined;\n }\n\n /** Upsert the tenant's credential (`created_at` is preserved on update). */\n async setCredential(tenant: CredentialTenant, provider: string, credential: AuthCredential): Promise<void> {\n const where = tenantWhere(tenant, provider);\n const encrypted = await this.encryption.encrypt(credential);\n const update = async () =>\n this.#db.updateMany('model_provider_credentials', where, {\n type: credential.type,\n data: encrypted,\n updated_at: new Date(),\n refresh_failed_at: null,\n });\n\n let lastError: unknown;\n for (let attempt = 0; attempt < 2; attempt++) {\n if ((await update()) > 0) return;\n\n const now = new Date();\n try {\n await this.#db.insertOne<CredentialDbRow>('model_provider_credentials', {\n org_id: tenant.orgId,\n user_id: tenant.userId ?? null,\n provider,\n type: credential.type,\n data: encrypted,\n created_at: now,\n updated_at: now,\n });\n return;\n } catch (error) {\n if (!(error instanceof UniqueViolationError)) throw error;\n lastError = error;\n }\n }\n throw lastError;\n }\n\n /** Delete the tenant's credential at exactly that scope. True when a row was removed. */\n async removeCredential(tenant: CredentialTenant, provider: string): Promise<boolean> {\n const deleted = await this.#db.deleteMany('model_provider_credentials', tenantWhere(tenant, provider));\n return deleted > 0;\n }\n\n /** All credentials visible to a user: their own rows plus the org's shared rows. */\n async listCredentials(orgId: string, userId: string): Promise<CredentialRecord[]> {\n // SQL `IN` never matches NULL, so the org rows need their own query.\n const [userRows, orgRows] = await Promise.all([\n this.#db.findMany<CredentialDbRow>('model_provider_credentials', { org_id: orgId, user_id: userId }),\n this.#db.findMany<CredentialDbRow>('model_provider_credentials', { org_id: orgId, user_id: null }),\n ]);\n return Promise.all([\n ...userRows.map(row => this.#toRecord(row, 'user')),\n ...orgRows.map(row => this.#toRecord(row, 'org')),\n ]);\n }\n\n /** Resolve a provider credential from exactly one tenancy scope. */\n async resolveCredential(\n orgId: string,\n userId: string,\n provider: string,\n scope: CredentialScope,\n ): Promise<ResolvedCredential | undefined> {\n const tenant = scope === 'org' ? { orgId } : { orgId, userId };\n const row = await this.#db.findOne<CredentialDbRow>('model_provider_credentials', tenantWhere(tenant, provider));\n return row ? { provider: row.provider, scope, credential: await this.#decryptCredential(row.data) } : undefined;\n }\n\n /**\n * Refresh the tenant's OAuth credential under the backend's atomic\n * read-modify-write so concurrent replicas don't invalidate each other's\n * rotating refresh tokens. The expiry is re-checked under the lock —\n * another replica may have refreshed already, in which case `refreshFn` is\n * skipped and the fresh credential is returned. Returns `undefined` when no\n * OAuth row exists for the tenant. A failed `refreshFn` is recorded on the\n * row (surfaced as `CredentialRecord.refreshFailedAt`) and rethrown.\n *\n * `needsRefresh` replaces the expiry check, e.g. to replace an access token\n * the provider rejected before its recorded expiry.\n */\n async refreshOAuth(\n tenant: CredentialTenant,\n provider: string,\n refreshFn: (current: OAuthCredential) => Promise<OAuthCredential>,\n needsRefresh: (current: OAuthCredential) => boolean = current => isOAuthCredentialExpired(current),\n ): Promise<OAuthCredential | undefined> {\n let result: OAuthCredential | undefined;\n let failure: { error: unknown } | undefined;\n await this.#db.updateAtomic<CredentialDbRow>(\n 'model_provider_credentials',\n tenantWhere(tenant, provider),\n async row => {\n const decrypted = await this.encryption.decrypt<AuthCredential>(row.data);\n if (decrypted.value.type !== 'oauth') return null;\n const current = decrypted.value;\n // Re-check under the lock: another replica may have refreshed while we waited.\n if (!needsRefresh(current)) {\n result = current;\n return decrypted.needsReencryption ? { data: await this.encryption.encrypt(current) } : null;\n }\n try {\n result = await refreshFn(current);\n } catch (error) {\n failure = { error };\n return { refresh_failed_at: new Date() };\n }\n return { data: await this.encryption.encrypt(result), updated_at: new Date(), refresh_failed_at: null };\n },\n );\n if (failure) throw failure.error;\n return result;\n }\n\n /** Persist a pending login flow started by a web route. */\n async createLoginSession(input: CreateLoginSessionInput): Promise<LoginSessionRow> {\n const inserted = await this.#db.insertOne<LoginSessionDbRow>('oauth_login_sessions', {\n session_id: input.sessionId,\n org_id: input.orgId,\n user_id: input.userId,\n provider: input.provider,\n kind: input.kind,\n credential_scope: input.credentialScope ?? null,\n pending: input.pending,\n expires_at: input.expiresAt,\n next_poll_at: input.nextPollAt ?? null,\n created_at: new Date(),\n });\n return toSessionRow(inserted);\n }\n\n /**\n * Read a pending login flow. An expired session is deleted on read (TTL\n * cleanup) and reported as absent.\n */\n async getLoginSession(sessionId: string): Promise<LoginSessionRow | undefined> {\n const row = await this.#db.findOne<LoginSessionDbRow>('oauth_login_sessions', { session_id: sessionId });\n if (!row) return undefined;\n if (row.expires_at.getTime() <= Date.now()) {\n await this.#db.deleteMany('oauth_login_sessions', { session_id: sessionId });\n return undefined;\n }\n return toSessionRow(row);\n }\n\n /**\n * Atomically claim a due login session for one upstream completion/poll attempt.\n * Returns undefined when the session is missing, expired, owned by another\n * tenant/provider, or already claimed by a concurrent request.\n */\n async claimLoginSession(\n sessionId: string,\n owner: Pick<LoginSessionRow, 'orgId' | 'userId' | 'provider' | 'kind'>,\n ): Promise<LoginSessionRow | undefined> {\n const now = Date.now();\n let claimed = false;\n const updated = await this.#db.updateAtomic<LoginSessionDbRow>(\n 'oauth_login_sessions',\n { session_id: sessionId },\n row => {\n if (\n row.org_id !== owner.orgId ||\n row.user_id !== owner.userId ||\n row.provider !== owner.provider ||\n row.kind !== owner.kind\n ) {\n return null;\n }\n if (row.expires_at.getTime() <= now) return null;\n if (row.next_poll_at && row.next_poll_at.getTime() > now) return null;\n claimed = true;\n // Park the next poll at expiry so a concurrent claim loses until the\n // claimer either touches the session forward or deletes it.\n return { next_poll_at: row.expires_at };\n },\n );\n return claimed && updated ? toSessionRow(updated) : undefined;\n }\n\n /** Update a pending flow's serialized state and/or next allowed poll time. */\n async touchLoginSession(\n sessionId: string,\n updates: { pending?: Record<string, unknown>; nextPollAt?: Date | null },\n ): Promise<void> {\n const set: Record<string, unknown> = {};\n if (updates.pending !== undefined) set.pending = updates.pending;\n if (updates.nextPollAt !== undefined) set.next_poll_at = updates.nextPollAt;\n if (Object.keys(set).length === 0) return;\n await this.#db.updateMany('oauth_login_sessions', { session_id: sessionId }, set);\n }\n\n /** Remove a pending login flow (completed, cancelled, or failed). */\n async deleteLoginSession(sessionId: string): Promise<void> {\n await this.#db.deleteMany('oauth_login_sessions', { session_id: sessionId });\n }\n}\n"],"mappings":"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"}