@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.
- package/CHANGELOG.md +36 -0
- package/dist/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +401 -314
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +441 -39455
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39466
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3624
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1833
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +2 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +401 -326
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -3942
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -915
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28618
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +438 -677
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.d.ts.map +1 -1
- package/dist/spa-static.js +91 -64
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +9 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -702
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -344
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1132
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +2 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +236 -438
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-review/SKILL.md +9 -2
- package/package.json +9 -9
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
package/dist/state-signing.js
CHANGED
|
@@ -1,48 +1,77 @@
|
|
|
1
|
-
// src/state-signing.ts
|
|
2
1
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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":"
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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":"
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|