@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +401 -314
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/connection.js +1 -1
  7. package/dist/capabilities/intake.js +1 -1
  8. package/dist/capabilities/version-control.js +1 -1
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +441 -39455
  11. package/dist/factory.js.map +1 -1
  12. package/dist/index.js +2 -39466
  13. package/dist/integrations/base.js +1 -1
  14. package/dist/integrations/github/config.js +36 -22
  15. package/dist/integrations/github/config.js.map +1 -1
  16. package/dist/integrations/github/factory-session.js +33 -37
  17. package/dist/integrations/github/factory-session.js.map +1 -1
  18. package/dist/integrations/github/integration.js +1037 -3624
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-triage.js +71 -71
  21. package/dist/integrations/github/issue-triage.js.map +1 -1
  22. package/dist/integrations/github/pat.js +47 -36
  23. package/dist/integrations/github/pat.js.map +1 -1
  24. package/dist/integrations/github/provenance.js +72 -101
  25. package/dist/integrations/github/provenance.js.map +1 -1
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +1272 -1833
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/rules.js +203 -471
  30. package/dist/integrations/github/rules.js.map +1 -1
  31. package/dist/integrations/github/sandbox.d.ts +2 -0
  32. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  33. package/dist/integrations/github/sandbox.js +401 -326
  34. package/dist/integrations/github/sandbox.js.map +1 -1
  35. package/dist/integrations/github/session-subscriptions.js +165 -223
  36. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  37. package/dist/integrations/github/subscriptions.js +43 -51
  38. package/dist/integrations/github/subscriptions.js.map +1 -1
  39. package/dist/integrations/github/token-refresh.js +15 -18
  40. package/dist/integrations/github/token-refresh.js.map +1 -1
  41. package/dist/integrations/github/webhook.js +359 -343
  42. package/dist/integrations/github/webhook.js.map +1 -1
  43. package/dist/integrations/linear/agent-tools.js +77 -100
  44. package/dist/integrations/linear/agent-tools.js.map +1 -1
  45. package/dist/integrations/linear/integration.js +611 -1309
  46. package/dist/integrations/linear/integration.js.map +1 -1
  47. package/dist/integrations/linear/routes.js +227 -230
  48. package/dist/integrations/linear/routes.js.map +1 -1
  49. package/dist/integrations/linear/rules.js +126 -346
  50. package/dist/integrations/linear/rules.js.map +1 -1
  51. package/dist/integrations/linear/storage.js +1 -1
  52. package/dist/integrations/platform/api-client.js +131 -135
  53. package/dist/integrations/platform/api-client.js.map +1 -1
  54. package/dist/integrations/platform/github/event-worker.js +275 -573
  55. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  56. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  57. package/dist/integrations/platform/github/integration.js +845 -3942
  58. package/dist/integrations/platform/github/integration.js.map +1 -1
  59. package/dist/integrations/platform/linear/integration.js +457 -1309
  60. package/dist/integrations/platform/linear/integration.js.map +1 -1
  61. package/dist/integrations/workos/integration.js +80 -85
  62. package/dist/integrations/workos/integration.js.map +1 -1
  63. package/dist/routes/config.d.ts.map +1 -1
  64. package/dist/routes/config.js +933 -915
  65. package/dist/routes/config.js.map +1 -1
  66. package/dist/routes/custom-provider-source.js +110 -93
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.js +401 -349
  69. package/dist/routes/fs.js.map +1 -1
  70. package/dist/routes/intake.js +165 -165
  71. package/dist/routes/intake.js.map +1 -1
  72. package/dist/routes/oauth.js +447 -25166
  73. package/dist/routes/oauth.js.map +1 -1
  74. package/dist/routes/projects.js +437 -376
  75. package/dist/routes/projects.js.map +1 -1
  76. package/dist/routes/provider-credentials.js +76 -58
  77. package/dist/routes/provider-credentials.js.map +1 -1
  78. package/dist/routes/route.js +12 -8
  79. package/dist/routes/route.js.map +1 -1
  80. package/dist/routes/skills.js +160 -173
  81. package/dist/routes/skills.js.map +1 -1
  82. package/dist/routes/surface.js +250 -28618
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js +141 -166
  85. package/dist/routes/tenant-credentials.js.map +1 -1
  86. package/dist/routes/work-items.js +654 -862
  87. package/dist/routes/work-items.js.map +1 -1
  88. package/dist/rules/binding-context.js +23 -35
  89. package/dist/rules/binding-context.js.map +1 -1
  90. package/dist/rules/defaults.js +200 -309
  91. package/dist/rules/defaults.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +438 -677
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/index.js +5 -599
  96. package/dist/rules/processor.js +344 -596
  97. package/dist/rules/processor.js.map +1 -1
  98. package/dist/rules/resolve.js +21 -18
  99. package/dist/rules/resolve.js.map +1 -1
  100. package/dist/rules/start-coordinator.js +168 -157
  101. package/dist/rules/start-coordinator.js.map +1 -1
  102. package/dist/rules/tools.js +50 -85
  103. package/dist/rules/tools.js.map +1 -1
  104. package/dist/rules/transition-service.js +183 -454
  105. package/dist/rules/transition-service.js.map +1 -1
  106. package/dist/rules/types.js +33 -29
  107. package/dist/rules/types.js.map +1 -1
  108. package/dist/rules/validation.js +266 -281
  109. package/dist/rules/validation.js.map +1 -1
  110. package/dist/sandbox/fleet.js +272 -250
  111. package/dist/sandbox/fleet.js.map +1 -1
  112. package/dist/sandbox/reattach.js +13 -6
  113. package/dist/sandbox/reattach.js.map +1 -1
  114. package/dist/server-error.js +12 -16
  115. package/dist/server-error.js.map +1 -1
  116. package/dist/skills/service.js +30 -35
  117. package/dist/skills/service.js.map +1 -1
  118. package/dist/spa-static.d.ts.map +1 -1
  119. package/dist/spa-static.js +91 -64
  120. package/dist/spa-static.js.map +1 -1
  121. package/dist/state-signing.js +73 -44
  122. package/dist/state-signing.js.map +1 -1
  123. package/dist/storage/domains/audit/agent-audit.js +60 -59
  124. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  125. package/dist/storage/domains/audit/base.js +169 -112
  126. package/dist/storage/domains/audit/base.js.map +1 -1
  127. package/dist/storage/domains/audit/domain.js +155 -153
  128. package/dist/storage/domains/audit/domain.js.map +1 -1
  129. package/dist/storage/domains/credentials/base.js +259 -243
  130. package/dist/storage/domains/credentials/base.js.map +1 -1
  131. package/dist/storage/domains/custom-providers/base.js +138 -143
  132. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  133. package/dist/storage/domains/intake/base.js +75 -57
  134. package/dist/storage/domains/intake/base.js.map +1 -1
  135. package/dist/storage/domains/integrations/base.js +310 -245
  136. package/dist/storage/domains/integrations/base.js.map +1 -1
  137. package/dist/storage/domains/memory-settings/base.js +117 -102
  138. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  139. package/dist/storage/domains/model-packs/base.js +89 -88
  140. package/dist/storage/domains/model-packs/base.js.map +1 -1
  141. package/dist/storage/domains/projects/base.js +97 -92
  142. package/dist/storage/domains/projects/base.js.map +1 -1
  143. package/dist/storage/domains/queue-health/base.js +111 -76
  144. package/dist/storage/domains/queue-health/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +9 -0
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +777 -702
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +322 -344
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  154. package/dist/storage/domains/work-items/base.js +1266 -1132
  155. package/dist/storage/domains/work-items/base.js.map +1 -1
  156. package/dist/storage/domains/work-items/metrics.js +202 -184
  157. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  158. package/dist/timing.d.ts +15 -0
  159. package/dist/timing.d.ts.map +1 -0
  160. package/dist/timing.js +34 -0
  161. package/dist/timing.js.map +1 -0
  162. package/dist/workspace.d.ts +2 -3
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +236 -438
  165. package/dist/workspace.js.map +1 -1
  166. package/factory-skills/factory-review/SKILL.md +9 -2
  167. package/package.json +9 -9
  168. package/dist/capabilities/connection.js.map +0 -1
  169. package/dist/capabilities/intake.js.map +0 -1
  170. package/dist/capabilities/version-control.js.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/integrations/base.js.map +0 -1
  173. package/dist/integrations/linear/storage.js.map +0 -1
  174. package/dist/rules/index.js.map +0 -1
@@ -1,48 +1,77 @@
1
- // src/state-signing.ts
2
1
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3
- var STATE_MAX_AGE_MS = 10 * 60 * 1e3;
2
+ //#region src/state-signing.ts
3
+ /**
4
+ * Shared OAuth/install `state` signing for web integrations.
5
+ *
6
+ * Both the GitHub and Linear OAuth flows round-trip a signed `state` value
7
+ * through the third party to bind the callback to the `(orgId, userId)` tenant
8
+ * that initiated it (CSRF protection + tenant routing). The signer is a system
9
+ * facility: `MastraFactory` creates ONE signer at boot and hands it to every
10
+ * registered integration through `IntegrationContext` (see
11
+ * `./factory-integration.ts`), so all integrations sign and verify with the
12
+ * same secret.
13
+ *
14
+ * Secret resolution happens in the factory, not here: explicit
15
+ * `config.stateSecret` → the GitHub integration's webhook secret → a
16
+ * per-process random secret. A random secret is NOT stable across replicas —
17
+ * a `state` signed by one replica cannot be verified by another — which is
18
+ * what the `stable` flag reports. The factory fails loud at boot when a
19
+ * registered integration requires a stable signer but only a random one is
20
+ * available.
21
+ *
22
+ * The wire format (base64url JSON payload + `.` + HMAC-SHA256 base64url
23
+ * signature) is unchanged from the previous `github/config.ts` implementation
24
+ * so in-flight OAuth states survive a deploy.
25
+ */
26
+ /** Signed `state` values expire after this window to bound the CSRF token. */
27
+ const STATE_MAX_AGE_MS = 600 * 1e3;
28
+ /**
29
+ * Create a state signer. With a `secret`, the signer is deployment-stable
30
+ * (`stable: true`); without one it falls back to a per-process random secret
31
+ * (`stable: false`).
32
+ */
4
33
  function createStateSigner(secret) {
5
- const stable = typeof secret === "string" && secret.length > 0;
6
- const key = stable ? secret : randomBytes(32).toString("hex");
7
- return {
8
- stable,
9
- sign(orgId, userId) {
10
- const payload = {
11
- orgId,
12
- userId,
13
- nonce: randomBytes(8).toString("hex"),
14
- issuedAt: Date.now()
15
- };
16
- const body = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
17
- const sig = createHmac("sha256", key).update(body).digest("base64url");
18
- return `${body}.${sig}`;
19
- },
20
- verify(state) {
21
- if (!state) return null;
22
- const dot = state.lastIndexOf(".");
23
- if (dot <= 0) return null;
24
- const body = state.slice(0, dot);
25
- const sig = state.slice(dot + 1);
26
- const expected = createHmac("sha256", key).update(body).digest("base64url");
27
- const sigBuf = Buffer.from(sig);
28
- const expectedBuf = Buffer.from(expected);
29
- if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
30
- return null;
31
- }
32
- try {
33
- const parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
34
- if (typeof parsed.orgId !== "string" || typeof parsed.userId !== "string") return null;
35
- if (typeof parsed.issuedAt !== "number" || !Number.isFinite(parsed.issuedAt)) return null;
36
- const age = Date.now() - parsed.issuedAt;
37
- if (age < 0 || age > STATE_MAX_AGE_MS) return null;
38
- return { orgId: parsed.orgId, userId: parsed.userId };
39
- } catch {
40
- return null;
41
- }
42
- }
43
- };
34
+ const stable = typeof secret === "string" && secret.length > 0;
35
+ const key = stable ? secret : randomBytes(32).toString("hex");
36
+ return {
37
+ stable,
38
+ sign(orgId, userId) {
39
+ const payload = {
40
+ orgId,
41
+ userId,
42
+ nonce: randomBytes(8).toString("hex"),
43
+ issuedAt: Date.now()
44
+ };
45
+ const body = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
46
+ return `${body}.${createHmac("sha256", key).update(body).digest("base64url")}`;
47
+ },
48
+ verify(state) {
49
+ if (!state) return null;
50
+ const dot = state.lastIndexOf(".");
51
+ if (dot <= 0) return null;
52
+ const body = state.slice(0, dot);
53
+ const sig = state.slice(dot + 1);
54
+ const expected = createHmac("sha256", key).update(body).digest("base64url");
55
+ const sigBuf = Buffer.from(sig);
56
+ const expectedBuf = Buffer.from(expected);
57
+ if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) return null;
58
+ try {
59
+ const parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
60
+ if (typeof parsed.orgId !== "string" || typeof parsed.userId !== "string") return null;
61
+ if (typeof parsed.issuedAt !== "number" || !Number.isFinite(parsed.issuedAt)) return null;
62
+ const age = Date.now() - parsed.issuedAt;
63
+ if (age < 0 || age > STATE_MAX_AGE_MS) return null;
64
+ return {
65
+ orgId: parsed.orgId,
66
+ userId: parsed.userId
67
+ };
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ };
44
73
  }
45
- export {
46
- createStateSigner
47
- };
74
+ //#endregion
75
+ export { createStateSigner };
76
+
48
77
  //# sourceMappingURL=state-signing.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/state-signing.ts"],"sourcesContent":["/**\n * Shared OAuth/install `state` signing for web integrations.\n *\n * Both the GitHub and Linear OAuth flows round-trip a signed `state` value\n * through the third party to bind the callback to the `(orgId, userId)` tenant\n * that initiated it (CSRF protection + tenant routing). The signer is a system\n * facility: `MastraFactory` creates ONE signer at boot and hands it to every\n * registered integration through `IntegrationContext` (see\n * `./factory-integration.ts`), so all integrations sign and verify with the\n * same secret.\n *\n * Secret resolution happens in the factory, not here: explicit\n * `config.stateSecret` → the GitHub integration's webhook secret → a\n * per-process random secret. A random secret is NOT stable across replicas —\n * a `state` signed by one replica cannot be verified by another — which is\n * what the `stable` flag reports. The factory fails loud at boot when a\n * registered integration requires a stable signer but only a random one is\n * available.\n *\n * The wire format (base64url JSON payload + `.` + HMAC-SHA256 base64url\n * signature) is unchanged from the previous `github/config.ts` implementation\n * so in-flight OAuth states survive a deploy.\n */\n\nimport { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\n\n/** Verified `(orgId, userId)` tenant carried by a signed `state`. */\nexport interface StateTenant {\n orgId: string;\n userId: string;\n}\n\n/** Signs and verifies OAuth `state` values bound to a `(orgId, userId)` tenant. */\nexport interface StateSigner {\n /** Build a signed `state` bound to the tenant. */\n sign(orgId: string, userId: string): string;\n /** Verify a signed `state`; returns the bound tenant, or `null` if invalid. */\n verify(state: string | undefined): StateTenant | null;\n /**\n * True when the signer was built from an explicit deployment-stable secret.\n * False means a per-process random secret: fine for single-process/local\n * dev, broken for multi-replica deploys (see module docs).\n */\n readonly stable: boolean;\n}\n\ninterface StatePayload {\n orgId: string;\n userId: string;\n nonce: string;\n issuedAt: number;\n}\n\n/** Signed `state` values expire after this window to bound the CSRF token. */\nconst STATE_MAX_AGE_MS = 10 * 60 * 1000;\n\n/**\n * Create a state signer. With a `secret`, the signer is deployment-stable\n * (`stable: true`); without one it falls back to a per-process random secret\n * (`stable: false`).\n */\nexport function createStateSigner(secret?: string): StateSigner {\n const stable = typeof secret === 'string' && secret.length > 0;\n const key = stable ? secret : randomBytes(32).toString('hex');\n return {\n stable,\n sign(orgId: string, userId: string): string {\n const payload: StatePayload = {\n orgId,\n userId,\n nonce: randomBytes(8).toString('hex'),\n issuedAt: Date.now(),\n };\n const body = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');\n const sig = createHmac('sha256', key).update(body).digest('base64url');\n return `${body}.${sig}`;\n },\n verify(state: string | undefined): StateTenant | null {\n if (!state) return null;\n const dot = state.lastIndexOf('.');\n if (dot <= 0) return null;\n const body = state.slice(0, dot);\n const sig = state.slice(dot + 1);\n const expected = createHmac('sha256', key).update(body).digest('base64url');\n const sigBuf = Buffer.from(sig);\n const expectedBuf = Buffer.from(expected);\n if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {\n return null;\n }\n try {\n const parsed = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as StatePayload;\n if (typeof parsed.orgId !== 'string' || typeof parsed.userId !== 'string') return null;\n if (typeof parsed.issuedAt !== 'number' || !Number.isFinite(parsed.issuedAt)) return null;\n const age = Date.now() - parsed.issuedAt;\n if (age < 0 || age > STATE_MAX_AGE_MS) return null;\n return { orgId: parsed.orgId, userId: parsed.userId };\n } catch {\n return null;\n }\n },\n };\n}\n"],"mappings":";AAwBA,SAAS,YAAY,aAAa,uBAAuB;AA8BzD,IAAM,mBAAmB,KAAK,KAAK;AAO5B,SAAS,kBAAkB,QAA8B;AAC9D,QAAM,SAAS,OAAO,WAAW,YAAY,OAAO,SAAS;AAC7D,QAAM,MAAM,SAAS,SAAS,YAAY,EAAE,EAAE,SAAS,KAAK;AAC5D,SAAO;AAAA,IACL;AAAA,IACA,KAAK,OAAe,QAAwB;AAC1C,YAAM,UAAwB;AAAA,QAC5B;AAAA,QACA;AAAA,QACA,OAAO,YAAY,CAAC,EAAE,SAAS,KAAK;AAAA,QACpC,UAAU,KAAK,IAAI;AAAA,MACrB;AACA,YAAM,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,GAAG,MAAM,EAAE,SAAS,WAAW;AAC9E,YAAM,MAAM,WAAW,UAAU,GAAG,EAAE,OAAO,IAAI,EAAE,OAAO,WAAW;AACrE,aAAO,GAAG,IAAI,IAAI,GAAG;AAAA,IACvB;AAAA,IACA,OAAO,OAA+C;AACpD,UAAI,CAAC,MAAO,QAAO;AACnB,YAAM,MAAM,MAAM,YAAY,GAAG;AACjC,UAAI,OAAO,EAAG,QAAO;AACrB,YAAM,OAAO,MAAM,MAAM,GAAG,GAAG;AAC/B,YAAM,MAAM,MAAM,MAAM,MAAM,CAAC;AAC/B,YAAM,WAAW,WAAW,UAAU,GAAG,EAAE,OAAO,IAAI,EAAE,OAAO,WAAW;AAC1E,YAAM,SAAS,OAAO,KAAK,GAAG;AAC9B,YAAM,cAAc,OAAO,KAAK,QAAQ;AACxC,UAAI,OAAO,WAAW,YAAY,UAAU,CAAC,gBAAgB,QAAQ,WAAW,GAAG;AACjF,eAAO;AAAA,MACT;AACA,UAAI;AACF,cAAM,SAAS,KAAK,MAAM,OAAO,KAAK,MAAM,WAAW,EAAE,SAAS,MAAM,CAAC;AACzE,YAAI,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,SAAU,QAAO;AAClF,YAAI,OAAO,OAAO,aAAa,YAAY,CAAC,OAAO,SAAS,OAAO,QAAQ,EAAG,QAAO;AACrF,cAAM,MAAM,KAAK,IAAI,IAAI,OAAO;AAChC,YAAI,MAAM,KAAK,MAAM,iBAAkB,QAAO;AAC9C,eAAO,EAAE,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO;AAAA,MACtD,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"file":"state-signing.js","names":[],"sources":["../src/state-signing.ts"],"sourcesContent":["/**\n * Shared OAuth/install `state` signing for web integrations.\n *\n * Both the GitHub and Linear OAuth flows round-trip a signed `state` value\n * through the third party to bind the callback to the `(orgId, userId)` tenant\n * that initiated it (CSRF protection + tenant routing). The signer is a system\n * facility: `MastraFactory` creates ONE signer at boot and hands it to every\n * registered integration through `IntegrationContext` (see\n * `./factory-integration.ts`), so all integrations sign and verify with the\n * same secret.\n *\n * Secret resolution happens in the factory, not here: explicit\n * `config.stateSecret` → the GitHub integration's webhook secret → a\n * per-process random secret. A random secret is NOT stable across replicas —\n * a `state` signed by one replica cannot be verified by another — which is\n * what the `stable` flag reports. The factory fails loud at boot when a\n * registered integration requires a stable signer but only a random one is\n * available.\n *\n * The wire format (base64url JSON payload + `.` + HMAC-SHA256 base64url\n * signature) is unchanged from the previous `github/config.ts` implementation\n * so in-flight OAuth states survive a deploy.\n */\n\nimport { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\n\n/** Verified `(orgId, userId)` tenant carried by a signed `state`. */\nexport interface StateTenant {\n orgId: string;\n userId: string;\n}\n\n/** Signs and verifies OAuth `state` values bound to a `(orgId, userId)` tenant. */\nexport interface StateSigner {\n /** Build a signed `state` bound to the tenant. */\n sign(orgId: string, userId: string): string;\n /** Verify a signed `state`; returns the bound tenant, or `null` if invalid. */\n verify(state: string | undefined): StateTenant | null;\n /**\n * True when the signer was built from an explicit deployment-stable secret.\n * False means a per-process random secret: fine for single-process/local\n * dev, broken for multi-replica deploys (see module docs).\n */\n readonly stable: boolean;\n}\n\ninterface StatePayload {\n orgId: string;\n userId: string;\n nonce: string;\n issuedAt: number;\n}\n\n/** Signed `state` values expire after this window to bound the CSRF token. */\nconst STATE_MAX_AGE_MS = 10 * 60 * 1000;\n\n/**\n * Create a state signer. With a `secret`, the signer is deployment-stable\n * (`stable: true`); without one it falls back to a per-process random secret\n * (`stable: false`).\n */\nexport function createStateSigner(secret?: string): StateSigner {\n const stable = typeof secret === 'string' && secret.length > 0;\n const key = stable ? secret : randomBytes(32).toString('hex');\n return {\n stable,\n sign(orgId: string, userId: string): string {\n const payload: StatePayload = {\n orgId,\n userId,\n nonce: randomBytes(8).toString('hex'),\n issuedAt: Date.now(),\n };\n const body = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');\n const sig = createHmac('sha256', key).update(body).digest('base64url');\n return `${body}.${sig}`;\n },\n verify(state: string | undefined): StateTenant | null {\n if (!state) return null;\n const dot = state.lastIndexOf('.');\n if (dot <= 0) return null;\n const body = state.slice(0, dot);\n const sig = state.slice(dot + 1);\n const expected = createHmac('sha256', key).update(body).digest('base64url');\n const sigBuf = Buffer.from(sig);\n const expectedBuf = Buffer.from(expected);\n if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {\n return null;\n }\n try {\n const parsed = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as StatePayload;\n if (typeof parsed.orgId !== 'string' || typeof parsed.userId !== 'string') return null;\n if (typeof parsed.issuedAt !== 'number' || !Number.isFinite(parsed.issuedAt)) return null;\n const age = Date.now() - parsed.issuedAt;\n if (age < 0 || age > STATE_MAX_AGE_MS) return null;\n return { orgId: parsed.orgId, userId: parsed.userId };\n } catch {\n return null;\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,mBAAmB,MAAU;;;;;;AAOnC,SAAgB,kBAAkB,QAA8B;CAC9D,MAAM,SAAS,OAAO,WAAW,YAAY,OAAO,SAAS;CAC7D,MAAM,MAAM,SAAS,SAAS,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;CAC5D,OAAO;EACL;EACA,KAAK,OAAe,QAAwB;GAC1C,MAAM,UAAwB;IAC5B;IACA;IACA,OAAO,YAAY,CAAC,CAAC,CAAC,SAAS,KAAK;IACpC,UAAU,KAAK,IAAI;GACrB;GACA,MAAM,OAAO,OAAO,KAAK,KAAK,UAAU,OAAO,GAAG,MAAM,CAAC,CAAC,SAAS,WAAW;GAE9E,OAAO,GAAG,KAAK,GADH,WAAW,UAAU,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,WACtC;EACtB;EACA,OAAO,OAA+C;GACpD,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,MAAM,MAAM,YAAY,GAAG;GACjC,IAAI,OAAO,GAAG,OAAO;GACrB,MAAM,OAAO,MAAM,MAAM,GAAG,GAAG;GAC/B,MAAM,MAAM,MAAM,MAAM,MAAM,CAAC;GAC/B,MAAM,WAAW,WAAW,UAAU,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,WAAW;GAC1E,MAAM,SAAS,OAAO,KAAK,GAAG;GAC9B,MAAM,cAAc,OAAO,KAAK,QAAQ;GACxC,IAAI,OAAO,WAAW,YAAY,UAAU,CAAC,gBAAgB,QAAQ,WAAW,GAC9E,OAAO;GAET,IAAI;IACF,MAAM,SAAS,KAAK,MAAM,OAAO,KAAK,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;IACzE,IAAI,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,UAAU,OAAO;IAClF,IAAI,OAAO,OAAO,aAAa,YAAY,CAAC,OAAO,SAAS,OAAO,QAAQ,GAAG,OAAO;IACrF,MAAM,MAAM,KAAK,IAAI,IAAI,OAAO;IAChC,IAAI,MAAM,KAAK,MAAM,kBAAkB,OAAO;IAC9C,OAAO;KAAE,OAAO,OAAO;KAAO,QAAQ,OAAO;IAAO;GACtD,QAAQ;IACN,OAAO;GACT;EACF;CACF;AACF"}
@@ -1,65 +1,66 @@
1
- // src/storage/domains/audit/agent-audit.ts
2
- var GIT_COMMIT_RE = /(?:^|\n|;|&&|\|\|)\s*git\s+commit(?:\s|$)/;
3
- var GIT_PUSH_RE = /(?:^|\n|;|&&|\|\|)\s*git\s+push(?:\s|$)/;
1
+ //#region src/storage/domains/audit/agent-audit.ts
2
+ /** Match command-start positions while ignoring command text embedded in heredoc bodies. */
3
+ const GIT_COMMIT_RE = /(?:^|\n|;|&&|\|\|)\s*git\s+commit(?:\s|$)/;
4
+ const GIT_PUSH_RE = /(?:^|\n|;|&&|\|\|)\s*git\s+push(?:\s|$)/;
4
5
  function stripHeredocBodies(command) {
5
- const lines = command.split("\n");
6
- const executableLines = [];
7
- let delimiter;
8
- for (const line of lines) {
9
- if (delimiter) {
10
- if (line.trim() === delimiter) delimiter = void 0;
11
- continue;
12
- }
13
- executableLines.push(line);
14
- const heredoc = line.match(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/);
15
- delimiter = heredoc?.[2];
16
- }
17
- return executableLines.join("\n");
6
+ const lines = command.split("\n");
7
+ const executableLines = [];
8
+ let delimiter;
9
+ for (const line of lines) {
10
+ if (delimiter) {
11
+ if (line.trim() === delimiter) delimiter = void 0;
12
+ continue;
13
+ }
14
+ executableLines.push(line);
15
+ delimiter = line.match(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/)?.[2];
16
+ }
17
+ return executableLines.join("\n");
18
18
  }
19
+ /** Parse the branch from a plain `git push <remote> <branch>` invocation. */
19
20
  function parsePushedBranch(command) {
20
- const match = command.match(
21
- /(?:^|\n|;|&&|\|\|)\s*git\s+push\s+(?:-[^\s]+\s+)*([^\s;&|-][^\s;&|]*)\s+([^\s;&|-][^\s;&|]*)/
22
- );
23
- return match?.[2];
21
+ return command.match(/(?:^|\n|;|&&|\|\|)\s*git\s+push\s+(?:-[^\s]+\s+)*([^\s;&|-][^\s;&|]*)\s+([^\s;&|-][^\s;&|]*)/)?.[2];
24
22
  }
25
- async function observeAgentGitAction({
26
- audit,
27
- toolContext
28
- }) {
29
- try {
30
- if (toolContext.toolName !== "execute_command" || toolContext.error) return;
31
- const rawCommand = toolContext.input?.command;
32
- if (typeof rawCommand !== "string") return;
33
- const command = stripHeredocBodies(rawCommand);
34
- const controller = toolContext.context.get("controller");
35
- const worktreePath = controller?.scope;
36
- if (GIT_COMMIT_RE.test(command)) {
37
- await audit.emitAgent({
38
- requestContext: toolContext.context,
39
- input: {
40
- action: "factory.agent.commit",
41
- targets: worktreePath ? [{ type: "worktree", id: worktreePath }] : []
42
- }
43
- });
44
- }
45
- if (GIT_PUSH_RE.test(command)) {
46
- const branch = parsePushedBranch(command);
47
- await audit.emitAgent({
48
- requestContext: toolContext.context,
49
- input: {
50
- action: "factory.agent.push",
51
- targets: worktreePath ? [{ type: "worktree", id: worktreePath }] : [],
52
- ...branch ? { metadata: { branch } } : {}
53
- }
54
- });
55
- }
56
- } catch (err) {
57
- console.warn("[Audit] Failed to observe agent git action", {
58
- error: err instanceof Error ? err.message : String(err)
59
- });
60
- }
23
+ /**
24
+ * Detect externally-visible git side effects in a completed tool call and
25
+ * record `factory.agent.*` audit events for them. One command can emit
26
+ * multiple events (`git commit && git push` emits both). Never throws.
27
+ */
28
+ async function observeAgentGitAction({ audit, toolContext }) {
29
+ try {
30
+ if (toolContext.toolName !== "execute_command" || toolContext.error) return;
31
+ const rawCommand = toolContext.input?.command;
32
+ if (typeof rawCommand !== "string") return;
33
+ const command = stripHeredocBodies(rawCommand);
34
+ const worktreePath = toolContext.context.get("controller")?.scope;
35
+ if (GIT_COMMIT_RE.test(command)) await audit.emitAgent({
36
+ requestContext: toolContext.context,
37
+ input: {
38
+ action: "factory.agent.commit",
39
+ targets: worktreePath ? [{
40
+ type: "worktree",
41
+ id: worktreePath
42
+ }] : []
43
+ }
44
+ });
45
+ if (GIT_PUSH_RE.test(command)) {
46
+ const branch = parsePushedBranch(command);
47
+ await audit.emitAgent({
48
+ requestContext: toolContext.context,
49
+ input: {
50
+ action: "factory.agent.push",
51
+ targets: worktreePath ? [{
52
+ type: "worktree",
53
+ id: worktreePath
54
+ }] : [],
55
+ ...branch ? { metadata: { branch } } : {}
56
+ }
57
+ });
58
+ }
59
+ } catch (err) {
60
+ console.warn("[Audit] Failed to observe agent git action", { error: err instanceof Error ? err.message : String(err) });
61
+ }
61
62
  }
62
- export {
63
- observeAgentGitAction
64
- };
63
+ //#endregion
64
+ export { observeAgentGitAction };
65
+
65
66
  //# sourceMappingURL=agent-audit.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/storage/domains/audit/agent-audit.ts"],"sourcesContent":["/**\n * Agent-level audit event detection (Audit v1.1).\n *\n * Git actions performed by agents inside runs never touch web routes, so this\n * observer detects externally-visible git side effects and delegates recording\n * to the factory-owned audit domain.\n */\n\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nimport type { AuditAgentEmitter } from './domain.js';\n\ntype FactorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\ninterface ToolObserverContext {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n context: RequestContext;\n}\n\n/** Match command-start positions while ignoring command text embedded in heredoc bodies. */\nconst GIT_COMMIT_RE = /(?:^|\\n|;|&&|\\|\\|)\\s*git\\s+commit(?:\\s|$)/;\nconst GIT_PUSH_RE = /(?:^|\\n|;|&&|\\|\\|)\\s*git\\s+push(?:\\s|$)/;\n\nfunction stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\n/** Parse the branch from a plain `git push <remote> <branch>` invocation. */\nfunction parsePushedBranch(command: string): string | undefined {\n const match = command.match(\n /(?:^|\\n|;|&&|\\|\\|)\\s*git\\s+push\\s+(?:-[^\\s]+\\s+)*([^\\s;&|-][^\\s;&|]*)\\s+([^\\s;&|-][^\\s;&|]*)/,\n );\n return match?.[2];\n}\n\n/**\n * Detect externally-visible git side effects in a completed tool call and\n * record `factory.agent.*` audit events for them. One command can emit\n * multiple events (`git commit && git push` emits both). Never throws.\n */\nexport async function observeAgentGitAction({\n audit,\n toolContext,\n}: {\n audit: AuditAgentEmitter;\n toolContext: ToolObserverContext;\n}): Promise<void> {\n try {\n if (toolContext.toolName !== 'execute_command' || toolContext.error) return;\n const rawCommand = (toolContext.input as { command?: unknown } | undefined)?.command;\n if (typeof rawCommand !== 'string') return;\n const command = stripHeredocBodies(rawCommand);\n\n const controller = toolContext.context.get('controller') as\n | AgentControllerRequestContext<FactorySessionState>\n | undefined;\n const worktreePath = controller?.scope;\n\n if (GIT_COMMIT_RE.test(command)) {\n await audit.emitAgent({\n requestContext: toolContext.context,\n input: {\n action: 'factory.agent.commit',\n targets: worktreePath ? [{ type: 'worktree', id: worktreePath }] : [],\n },\n });\n }\n\n if (GIT_PUSH_RE.test(command)) {\n const branch = parsePushedBranch(command);\n await audit.emitAgent({\n requestContext: toolContext.context,\n input: {\n action: 'factory.agent.push',\n targets: worktreePath ? [{ type: 'worktree', id: worktreePath }] : [],\n ...(branch ? { metadata: { branch } } : {}),\n },\n });\n }\n } catch (err) {\n console.warn('[Audit] Failed to observe agent git action', {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n}\n"],"mappings":";AAwBA,IAAM,gBAAgB;AACtB,IAAM,cAAc;AAEpB,SAAS,mBAAmB,SAAyB;AACnD,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,kBAA4B,CAAC;AACnC,MAAI;AAEJ,aAAW,QAAQ,OAAO;AACxB,QAAI,WAAW;AACb,UAAI,KAAK,KAAK,MAAM,UAAW,aAAY;AAC3C;AAAA,IACF;AACA,oBAAgB,KAAK,IAAI;AACzB,UAAM,UAAU,KAAK,MAAM,0CAA0C;AACrE,gBAAY,UAAU,CAAC;AAAA,EACzB;AAEA,SAAO,gBAAgB,KAAK,IAAI;AAClC;AAGA,SAAS,kBAAkB,SAAqC;AAC9D,QAAM,QAAQ,QAAQ;AAAA,IACpB;AAAA,EACF;AACA,SAAO,QAAQ,CAAC;AAClB;AAOA,eAAsB,sBAAsB;AAAA,EAC1C;AAAA,EACA;AACF,GAGkB;AAChB,MAAI;AACF,QAAI,YAAY,aAAa,qBAAqB,YAAY,MAAO;AACrE,UAAM,aAAc,YAAY,OAA6C;AAC7E,QAAI,OAAO,eAAe,SAAU;AACpC,UAAM,UAAU,mBAAmB,UAAU;AAE7C,UAAM,aAAa,YAAY,QAAQ,IAAI,YAAY;AAGvD,UAAM,eAAe,YAAY;AAEjC,QAAI,cAAc,KAAK,OAAO,GAAG;AAC/B,YAAM,MAAM,UAAU;AAAA,QACpB,gBAAgB,YAAY;AAAA,QAC5B,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,SAAS,eAAe,CAAC,EAAE,MAAM,YAAY,IAAI,aAAa,CAAC,IAAI,CAAC;AAAA,QACtE;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI,YAAY,KAAK,OAAO,GAAG;AAC7B,YAAM,SAAS,kBAAkB,OAAO;AACxC,YAAM,MAAM,UAAU;AAAA,QACpB,gBAAgB,YAAY;AAAA,QAC5B,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,SAAS,eAAe,CAAC,EAAE,MAAM,YAAY,IAAI,aAAa,CAAC,IAAI,CAAC;AAAA,UACpE,GAAI,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,QAC3C;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,KAAK,8CAA8C;AAAA,MACzD,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACxD,CAAC;AAAA,EACH;AACF;","names":[]}
1
+ {"version":3,"file":"agent-audit.js","names":[],"sources":["../../../../src/storage/domains/audit/agent-audit.ts"],"sourcesContent":["/**\n * Agent-level audit event detection (Audit v1.1).\n *\n * Git actions performed by agents inside runs never touch web routes, so this\n * observer detects externally-visible git side effects and delegates recording\n * to the factory-owned audit domain.\n */\n\nimport type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nimport type { AuditAgentEmitter } from './domain.js';\n\ntype FactorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\ninterface ToolObserverContext {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n context: RequestContext;\n}\n\n/** Match command-start positions while ignoring command text embedded in heredoc bodies. */\nconst GIT_COMMIT_RE = /(?:^|\\n|;|&&|\\|\\|)\\s*git\\s+commit(?:\\s|$)/;\nconst GIT_PUSH_RE = /(?:^|\\n|;|&&|\\|\\|)\\s*git\\s+push(?:\\s|$)/;\n\nfunction stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\n/** Parse the branch from a plain `git push <remote> <branch>` invocation. */\nfunction parsePushedBranch(command: string): string | undefined {\n const match = command.match(\n /(?:^|\\n|;|&&|\\|\\|)\\s*git\\s+push\\s+(?:-[^\\s]+\\s+)*([^\\s;&|-][^\\s;&|]*)\\s+([^\\s;&|-][^\\s;&|]*)/,\n );\n return match?.[2];\n}\n\n/**\n * Detect externally-visible git side effects in a completed tool call and\n * record `factory.agent.*` audit events for them. One command can emit\n * multiple events (`git commit && git push` emits both). Never throws.\n */\nexport async function observeAgentGitAction({\n audit,\n toolContext,\n}: {\n audit: AuditAgentEmitter;\n toolContext: ToolObserverContext;\n}): Promise<void> {\n try {\n if (toolContext.toolName !== 'execute_command' || toolContext.error) return;\n const rawCommand = (toolContext.input as { command?: unknown } | undefined)?.command;\n if (typeof rawCommand !== 'string') return;\n const command = stripHeredocBodies(rawCommand);\n\n const controller = toolContext.context.get('controller') as\n | AgentControllerRequestContext<FactorySessionState>\n | undefined;\n const worktreePath = controller?.scope;\n\n if (GIT_COMMIT_RE.test(command)) {\n await audit.emitAgent({\n requestContext: toolContext.context,\n input: {\n action: 'factory.agent.commit',\n targets: worktreePath ? [{ type: 'worktree', id: worktreePath }] : [],\n },\n });\n }\n\n if (GIT_PUSH_RE.test(command)) {\n const branch = parsePushedBranch(command);\n await audit.emitAgent({\n requestContext: toolContext.context,\n input: {\n action: 'factory.agent.push',\n targets: worktreePath ? [{ type: 'worktree', id: worktreePath }] : [],\n ...(branch ? { metadata: { branch } } : {}),\n },\n });\n }\n } catch (err) {\n console.warn('[Audit] Failed to observe agent git action', {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n}\n"],"mappings":";;AAwBA,MAAM,gBAAgB;AACtB,MAAM,cAAc;AAEpB,SAAS,mBAAmB,SAAyB;CACnD,MAAM,QAAQ,QAAQ,MAAM,IAAI;CAChC,MAAM,kBAA4B,CAAC;CACnC,IAAI;CAEJ,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,WAAW;GACb,IAAI,KAAK,KAAK,MAAM,WAAW,YAAY,KAAA;GAC3C;EACF;EACA,gBAAgB,KAAK,IAAI;EAEzB,YADgB,KAAK,MAAM,0CACT,CAAC,GAAG;CACxB;CAEA,OAAO,gBAAgB,KAAK,IAAI;AAClC;;AAGA,SAAS,kBAAkB,SAAqC;CAI9D,OAHc,QAAQ,MACpB,8FAES,CAAC,GAAG;AACjB;;;;;;AAOA,eAAsB,sBAAsB,EAC1C,OACA,eAIgB;CAChB,IAAI;EACF,IAAI,YAAY,aAAa,qBAAqB,YAAY,OAAO;EACrE,MAAM,aAAc,YAAY,OAA6C;EAC7E,IAAI,OAAO,eAAe,UAAU;EACpC,MAAM,UAAU,mBAAmB,UAAU;EAK7C,MAAM,eAHa,YAAY,QAAQ,IAAI,YAGb,CAAC,EAAE;EAEjC,IAAI,cAAc,KAAK,OAAO,GAC5B,MAAM,MAAM,UAAU;GACpB,gBAAgB,YAAY;GAC5B,OAAO;IACL,QAAQ;IACR,SAAS,eAAe,CAAC;KAAE,MAAM;KAAY,IAAI;IAAa,CAAC,IAAI,CAAC;GACtE;EACF,CAAC;EAGH,IAAI,YAAY,KAAK,OAAO,GAAG;GAC7B,MAAM,SAAS,kBAAkB,OAAO;GACxC,MAAM,MAAM,UAAU;IACpB,gBAAgB,YAAY;IAC5B,OAAO;KACL,QAAQ;KACR,SAAS,eAAe,CAAC;MAAE,MAAM;MAAY,IAAI;KAAa,CAAC,IAAI,CAAC;KACpE,GAAI,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;IAC3C;GACF,CAAC;EACH;CACF,SAAS,KAAK;EACZ,QAAQ,KAAK,8CAA8C,EACzD,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EACxD,CAAC;CACH;AACF"}
@@ -1,127 +1,184 @@
1
- // src/storage/domains/audit/base.ts
2
1
  import { FactoryStorageDomain } from "@mastra/core/storage";
3
- var MAX_METADATA_JSON_LENGTH = 4096;
4
- var DEFAULT_PAGE_SIZE = 50;
5
- var MAX_PAGE_SIZE = 200;
2
+ //#region src/storage/domains/audit/base.ts
3
+ /**
4
+ * Factory audit events domain — the append-only "who did what, when" trail
5
+ * behind the software factory.
6
+ *
7
+ * One `audit_events` row records a single audited mutation (work-item change,
8
+ * stage move, run start, worktree create/delete, git action, intake config
9
+ * change). Rows are append-only: there is no update/delete API, and the table
10
+ * is the local source of truth even when the WorkOS Audit Logs mirror is
11
+ * unavailable.
12
+ *
13
+ * Tenancy is **org-first**, like `work_items`: events are scoped by `org_id`
14
+ * and (usually) `factory_project_id`; `actor_id` records who acted but never
15
+ * scopes reads.
16
+ *
17
+ * v1 action taxonomy (register these in the WorkOS dashboard under
18
+ * Audit Logs → Events for the export mirror to accept them):
19
+ * - factory.work_item.created
20
+ * - factory.work_item.updated
21
+ * - factory.work_item.stage_moved
22
+ * - factory.work_item.deleted
23
+ * - factory.run.started
24
+ * - factory.worktree.created
25
+ * - factory.worktree.deleted
26
+ * - factory.triage.started
27
+ * - factory.git.commit
28
+ * - factory.git.push
29
+ * - factory.git.pr_opened
30
+ * - factory.intake.config_updated
31
+ *
32
+ * v1.1 adds agent-level actions (also register these in WorkOS):
33
+ * - factory.agent.commit
34
+ * - factory.agent.push
35
+ * - factory.agent.pr_opened
36
+ *
37
+ * Agent events carry `actor_type = 'agent'` with `actor_id = 'agent:<threadId>'`
38
+ * and `metadata.startedBy = <userId>` chaining accountability back to the human
39
+ * whose message drove the run.
40
+ */
41
+ /** Metadata is a bounded summary — never full payloads, never secrets. */
42
+ const MAX_METADATA_JSON_LENGTH = 4096;
43
+ const DEFAULT_PAGE_SIZE = 50;
44
+ const MAX_PAGE_SIZE = 200;
45
+ /** Truncate oversized metadata rather than dropping the whole event. */
6
46
  function boundAuditMetadata(metadata) {
7
- if (!metadata) return {};
8
- try {
9
- if (JSON.stringify(metadata).length <= MAX_METADATA_JSON_LENGTH) return metadata;
10
- } catch {
11
- return { truncated: true };
12
- }
13
- return { truncated: true };
47
+ if (!metadata) return {};
48
+ try {
49
+ if (JSON.stringify(metadata).length <= MAX_METADATA_JSON_LENGTH) return metadata;
50
+ } catch {
51
+ return { truncated: true };
52
+ }
53
+ return { truncated: true };
14
54
  }
55
+ /** Normalize a requested page size to a finite integer in `[1, MAX_PAGE_SIZE]`. */
15
56
  function clampAuditLimit(limit) {
16
- const normalized = typeof limit === "number" && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE;
17
- return Math.min(Math.max(normalized, 1), MAX_PAGE_SIZE);
57
+ return Math.min(Math.max(typeof limit === "number" && Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_PAGE_SIZE, 1), MAX_PAGE_SIZE);
18
58
  }
59
+ /** Encode the `(occurredAt, id)` keyset cursor of a row. */
19
60
  function encodeAuditCursor(row) {
20
- return `${row.occurredAt.toISOString()}_${row.id}`;
61
+ return `${row.occurredAt.toISOString()}_${row.id}`;
21
62
  }
63
+ /** Decode a cursor back into its `(occurredAt, id)` parts, or `undefined`. */
22
64
  function decodeAuditCursor(cursor) {
23
- const sep = cursor.lastIndexOf("_");
24
- if (sep <= 0) return void 0;
25
- const occurredAt = new Date(cursor.slice(0, sep));
26
- const id = cursor.slice(sep + 1);
27
- if (Number.isNaN(occurredAt.getTime()) || !id) return void 0;
28
- return { occurredAt, id };
65
+ const sep = cursor.lastIndexOf("_");
66
+ if (sep <= 0) return void 0;
67
+ const occurredAt = new Date(cursor.slice(0, sep));
68
+ const id = cursor.slice(sep + 1);
69
+ if (Number.isNaN(occurredAt.getTime()) || !id) return void 0;
70
+ return {
71
+ occurredAt,
72
+ id
73
+ };
29
74
  }
30
- var AUDIT_EVENTS_SCHEMA = {
31
- name: "audit_events",
32
- columns: {
33
- id: { type: "uuid-pk" },
34
- org_id: { type: "text" },
35
- actor_id: { type: "text" },
36
- actor_type: { type: "text", default: "human" },
37
- action: { type: "text" },
38
- targets: { type: "json" },
39
- metadata: { type: "json" },
40
- factory_project_id: { type: "text", nullable: true },
41
- project_repository_id: { type: "text", nullable: true },
42
- context: { type: "json" },
43
- occurred_at: { type: "timestamp" }
44
- },
45
- indexes: [
46
- { name: "audit_events_org_occurred_idx", columns: ["org_id", "occurred_at"] },
47
- { name: "audit_events_org_project_occurred_idx", columns: ["org_id", "factory_project_id", "occurred_at"] }
48
- ]
75
+ const AUDIT_EVENTS_SCHEMA = {
76
+ name: "audit_events",
77
+ columns: {
78
+ id: { type: "uuid-pk" },
79
+ org_id: { type: "text" },
80
+ actor_id: { type: "text" },
81
+ actor_type: {
82
+ type: "text",
83
+ default: "human"
84
+ },
85
+ action: { type: "text" },
86
+ targets: { type: "json" },
87
+ metadata: { type: "json" },
88
+ factory_project_id: {
89
+ type: "text",
90
+ nullable: true
91
+ },
92
+ project_repository_id: {
93
+ type: "text",
94
+ nullable: true
95
+ },
96
+ context: { type: "json" },
97
+ occurred_at: { type: "timestamp" }
98
+ },
99
+ indexes: [{
100
+ name: "audit_events_org_occurred_idx",
101
+ columns: ["org_id", "occurred_at"]
102
+ }, {
103
+ name: "audit_events_org_project_occurred_idx",
104
+ columns: [
105
+ "org_id",
106
+ "factory_project_id",
107
+ "occurred_at"
108
+ ]
109
+ }]
49
110
  };
50
111
  function toRow(db) {
51
- return {
52
- id: db.id,
53
- orgId: db.org_id,
54
- actorId: db.actor_id,
55
- actorType: db.actor_type,
56
- action: db.action,
57
- targets: db.targets,
58
- metadata: db.metadata,
59
- factoryProjectId: db.factory_project_id,
60
- projectRepositoryId: db.project_repository_id,
61
- context: db.context,
62
- occurredAt: db.occurred_at
63
- };
112
+ return {
113
+ id: db.id,
114
+ orgId: db.org_id,
115
+ actorId: db.actor_id,
116
+ actorType: db.actor_type,
117
+ action: db.action,
118
+ targets: db.targets,
119
+ metadata: db.metadata,
120
+ factoryProjectId: db.factory_project_id,
121
+ projectRepositoryId: db.project_repository_id,
122
+ context: db.context,
123
+ occurredAt: db.occurred_at
124
+ };
64
125
  }
126
+ /**
127
+ * Audit event storage, written once against the generic `FactoryStorageOps`
128
+ * surface. `record()` normalizes inputs (defaults, metadata bounding);
129
+ * `list()` is keyset-paginated on `(occurred_at, id)` newest-first.
130
+ */
65
131
  var AuditStorage = class extends FactoryStorageDomain {
66
- constructor() {
67
- super("audit");
68
- }
69
- async init() {
70
- await this.ensureCollections([AUDIT_EVENTS_SCHEMA]);
71
- }
72
- async dangerouslyClearAll() {
73
- await this.ops.deleteMany("audit_events", {});
74
- }
75
- get #db() {
76
- return this.ops;
77
- }
78
- /** Append one audit event. Throws on failure — swallow-on-failure lives in the caller. */
79
- async record(input) {
80
- const inserted = await this.#db.insertOne("audit_events", {
81
- org_id: input.orgId,
82
- actor_id: input.actorId,
83
- actor_type: input.actorType ?? "human",
84
- action: input.action,
85
- targets: input.targets,
86
- metadata: boundAuditMetadata(input.metadata),
87
- factory_project_id: input.factoryProjectId ?? null,
88
- project_repository_id: input.projectRepositoryId ?? null,
89
- context: input.context ?? {},
90
- occurred_at: input.occurredAt ?? /* @__PURE__ */ new Date()
91
- });
92
- return toRow(inserted);
93
- }
94
- /** List an org's audit events newest-first with keyset pagination. */
95
- async list(input) {
96
- const limit = clampAuditLimit(input.limit);
97
- const where = { org_id: input.orgId };
98
- if (input.factoryProjectId) where.factory_project_id = input.factoryProjectId;
99
- if (input.actions && input.actions.length > 0) where.action = { in: input.actions };
100
- if (input.actorId) where.actor_id = input.actorId;
101
- const cursor = input.before ? decodeAuditCursor(input.before) : void 0;
102
- const rows = await this.#db.findMany("audit_events", where, {
103
- orderBy: [
104
- ["occurred_at", "desc"],
105
- ["id", "desc"]
106
- ],
107
- limit: limit + 1,
108
- ...cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}
109
- });
110
- const events = rows.slice(0, limit).map(toRow);
111
- const hasMore = rows.length > limit;
112
- const last = events[events.length - 1];
113
- return {
114
- events,
115
- ...hasMore && last ? { nextCursor: encodeAuditCursor(last) } : {}
116
- };
117
- }
118
- };
119
- export {
120
- AUDIT_EVENTS_SCHEMA,
121
- AuditStorage,
122
- boundAuditMetadata,
123
- clampAuditLimit,
124
- decodeAuditCursor,
125
- encodeAuditCursor
132
+ constructor() {
133
+ super("audit");
134
+ }
135
+ async init() {
136
+ await this.ensureCollections([AUDIT_EVENTS_SCHEMA]);
137
+ }
138
+ async dangerouslyClearAll() {
139
+ await this.ops.deleteMany("audit_events", {});
140
+ }
141
+ get #db() {
142
+ return this.ops;
143
+ }
144
+ /** Append one audit event. Throws on failure — swallow-on-failure lives in the caller. */
145
+ async record(input) {
146
+ return toRow(await this.#db.insertOne("audit_events", {
147
+ org_id: input.orgId,
148
+ actor_id: input.actorId,
149
+ actor_type: input.actorType ?? "human",
150
+ action: input.action,
151
+ targets: input.targets,
152
+ metadata: boundAuditMetadata(input.metadata),
153
+ factory_project_id: input.factoryProjectId ?? null,
154
+ project_repository_id: input.projectRepositoryId ?? null,
155
+ context: input.context ?? {},
156
+ occurred_at: input.occurredAt ?? /* @__PURE__ */ new Date()
157
+ }));
158
+ }
159
+ /** List an org's audit events newest-first with keyset pagination. */
160
+ async list(input) {
161
+ const limit = clampAuditLimit(input.limit);
162
+ const where = { org_id: input.orgId };
163
+ if (input.factoryProjectId) where.factory_project_id = input.factoryProjectId;
164
+ if (input.actions && input.actions.length > 0) where.action = { in: input.actions };
165
+ if (input.actorId) where.actor_id = input.actorId;
166
+ const cursor = input.before ? decodeAuditCursor(input.before) : void 0;
167
+ const rows = await this.#db.findMany("audit_events", where, {
168
+ orderBy: [["occurred_at", "desc"], ["id", "desc"]],
169
+ limit: limit + 1,
170
+ ...cursor ? { cursor: { values: [cursor.occurredAt, cursor.id] } } : {}
171
+ });
172
+ const events = rows.slice(0, limit).map(toRow);
173
+ const hasMore = rows.length > limit;
174
+ const last = events[events.length - 1];
175
+ return {
176
+ events,
177
+ ...hasMore && last ? { nextCursor: encodeAuditCursor(last) } : {}
178
+ };
179
+ }
126
180
  };
181
+ //#endregion
182
+ export { AUDIT_EVENTS_SCHEMA, AuditStorage, boundAuditMetadata, clampAuditLimit, decodeAuditCursor, encodeAuditCursor };
183
+
127
184
  //# sourceMappingURL=base.js.map