@agent-native/core 0.201.0-nightly-20261005153421 → 0.201.0

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 (118) hide show
  1. package/README.md +22 -22
  2. package/dist/a2a/server.d.ts +2 -2
  3. package/dist/a2a/server.js +13 -2
  4. package/dist/agent/thread-data-builder.d.ts +4 -0
  5. package/dist/agent/thread-data-builder.js +8 -0
  6. package/dist/agent-sql/analysis.d.ts +31 -0
  7. package/dist/agent-sql/analysis.js +169 -0
  8. package/dist/agent-sql/index.d.ts +4 -0
  9. package/dist/agent-sql/index.js +4 -0
  10. package/dist/agent-sql/lexer.d.ts +37 -0
  11. package/dist/agent-sql/lexer.js +357 -0
  12. package/dist/agent-sql/postgres.d.ts +51 -0
  13. package/dist/agent-sql/postgres.js +197 -0
  14. package/dist/agent-sql/query.d.ts +30 -0
  15. package/dist/agent-sql/query.js +514 -0
  16. package/dist/catalog.json +24 -24
  17. package/dist/client/analytics/index.d.ts +1 -1
  18. package/dist/client/analytics/index.js +1 -1
  19. package/dist/client/analytics.d.ts +8 -0
  20. package/dist/client/analytics.js +17 -2
  21. package/dist/client/session-replay.js +3 -2
  22. package/dist/identity/offboard.js +31 -3
  23. package/dist/identity/rekey.d.ts +12 -1
  24. package/dist/identity/rekey.js +77 -6
  25. package/dist/identity/retired-emails.d.ts +23 -0
  26. package/dist/identity/retired-emails.js +55 -0
  27. package/dist/mcp/actions/create-org-service-token.js +20 -8
  28. package/dist/mcp/actions/service-token-access.d.ts +1 -0
  29. package/dist/mcp/actions/service-token-access.js +41 -24
  30. package/dist/mcp/build-server.d.ts +20 -6
  31. package/dist/mcp/build-server.js +131 -35
  32. package/dist/mcp/connect-route.d.ts +4 -3
  33. package/dist/mcp/connect-route.js +140 -82
  34. package/dist/mcp/connect-store.d.ts +17 -11
  35. package/dist/mcp/connect-store.js +88 -60
  36. package/dist/mcp/credential-issuance.d.ts +23 -0
  37. package/dist/mcp/credential-issuance.js +77 -0
  38. package/dist/mcp/credential-membership.d.ts +26 -0
  39. package/dist/mcp/credential-membership.js +67 -0
  40. package/dist/mcp/oauth-route.js +133 -45
  41. package/dist/mcp/oauth-store.d.ts +15 -5
  42. package/dist/mcp/oauth-store.js +119 -34
  43. package/dist/mcp/oauth-token.d.ts +4 -0
  44. package/dist/mcp/oauth-token.js +5 -0
  45. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  46. package/dist/mcp/server.js +14 -0
  47. package/dist/onboarding/default-steps.js +1 -1
  48. package/dist/org/handlers.d.ts +1 -1
  49. package/dist/org/index.d.ts +2 -2
  50. package/dist/org/membership.d.ts +3 -1
  51. package/dist/org/membership.js +5 -2
  52. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  53. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  54. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  55. package/dist/resource-changes/store.d.ts +2 -0
  56. package/dist/resource-changes/store.js +4 -0
  57. package/dist/resources/handlers.d.ts +1 -1
  58. package/dist/scripts/db/exec.js +28 -101
  59. package/dist/scripts/db/patch.js +14 -34
  60. package/dist/scripts/db/postgres-client.d.ts +8 -1
  61. package/dist/scripts/db/postgres-client.js +20 -6
  62. package/dist/scripts/db/query.js +31 -33
  63. package/dist/scripts/db/safety.d.ts +47 -2
  64. package/dist/scripts/db/safety.js +383 -137
  65. package/dist/scripts/db/scoping.d.ts +1 -0
  66. package/dist/scripts/db/scoping.js +3 -2
  67. package/dist/search/index-store.d.ts +21 -0
  68. package/dist/search/index-store.js +47 -0
  69. package/dist/search/indexer.js +35 -32
  70. package/dist/search/registry.d.ts +6 -3
  71. package/dist/search/registry.js +30 -8
  72. package/dist/secrets/routes.d.ts +6 -6
  73. package/dist/server/agent-chat-plugin.js +18 -6
  74. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  75. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  76. package/dist/server/agent-run-context.js +9 -0
  77. package/dist/server/attribution.d.ts +2 -0
  78. package/dist/server/attribution.js +7 -1
  79. package/dist/server/auth.js +9 -0
  80. package/dist/server/credential-membership-unavailable.d.ts +13 -0
  81. package/dist/server/credential-membership-unavailable.js +22 -0
  82. package/dist/server/index.d.ts +1 -0
  83. package/dist/server/index.js +1 -0
  84. package/dist/server/onboarding-html.js +1 -1
  85. package/dist/server/realtime-token.d.ts +1 -1
  86. package/dist/server/recap-image-route.js +4 -0
  87. package/dist/server/release-schema.js +4 -0
  88. package/dist/shared/docs-url.d.ts +3 -1
  89. package/dist/shared/docs-url.js +6 -3
  90. package/dist/shared/environment-lanes.d.ts +8 -0
  91. package/dist/shared/environment-lanes.js +26 -0
  92. package/dist/templates/chat/.agents/skills/security/SKILL.md +9 -2
  93. package/dist/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
  94. package/dist/templates/chat/README.md +2 -2
  95. package/dist/templates/default/.agents/skills/security/SKILL.md +9 -2
  96. package/dist/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
  97. package/dist/templates/default/app/routes/home.tsx +5 -1
  98. package/dist/templates/headless/.agents/skills/security/SKILL.md +9 -2
  99. package/dist/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
  100. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
  101. package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +12 -7
  102. package/dist/tracking/providers.js +13 -1
  103. package/docs/content/authentication.mdx +5 -1
  104. package/docs/content/durable-background-runs.mdx +17 -8
  105. package/docs/content/external-agents.mdx +3 -1
  106. package/docs/content/mcp-protocol.mdx +6 -2
  107. package/docs/content/security.mdx +1 -1
  108. package/package.json +5 -4
  109. package/src/templates/chat/.agents/skills/security/SKILL.md +9 -2
  110. package/src/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
  111. package/src/templates/chat/README.md +2 -2
  112. package/src/templates/default/.agents/skills/security/SKILL.md +9 -2
  113. package/src/templates/default/.agents/skills/storing-data/SKILL.md +12 -7
  114. package/src/templates/default/app/routes/home.tsx +5 -1
  115. package/src/templates/headless/.agents/skills/security/SKILL.md +9 -2
  116. package/src/templates/headless/.agents/skills/storing-data/SKILL.md +12 -7
  117. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +9 -2
  118. package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +12 -7
@@ -11,6 +11,7 @@ export const MCP_OAUTH_SCOPES = [
11
11
  "offline_access",
12
12
  ];
13
13
  export const MCP_OAUTH_DEFAULT_SCOPE = MCP_OAUTH_SCOPES.join(" ");
14
+ const MCP_OAUTH_CREDENTIAL_VERSION = 2;
14
15
  function signingSecret() {
15
16
  return new TextEncoder().encode(process.env.A2A_SECRET?.trim() || getAuthSecret());
16
17
  }
@@ -60,6 +61,7 @@ export function parseMcpOAuthOrgIdClaim(payload) {
60
61
  export async function signMcpOAuthAccessToken(params) {
61
62
  return new jose.SignJWT({
62
63
  typ: "agent-native-mcp-oauth",
64
+ credential_version: MCP_OAUTH_CREDENTIAL_VERSION,
63
65
  sub: params.ownerEmail,
64
66
  ...(params.orgId !== undefined ? { org_id: params.orgId } : {}),
65
67
  ...(params.orgDomain ? { org_domain: params.orgDomain } : {}),
@@ -122,6 +124,8 @@ export async function verifyMcpOAuthAccessToken(token, resource) {
122
124
  try {
123
125
  if (payload.typ !== "agent-native-mcp-oauth")
124
126
  return null;
127
+ if (payload.credential_version !== MCP_OAUTH_CREDENTIAL_VERSION)
128
+ return null;
125
129
  if (typeof payload.resource !== "string")
126
130
  return null;
127
131
  const embeddedResource = normaliseResource(payload.resource);
@@ -148,6 +152,7 @@ export async function verifyMcpOAuthAccessToken(token, resource) {
148
152
  clientId: payload.client_id,
149
153
  jti: typeof payload.jti === "string" ? payload.jti : undefined,
150
154
  ...(payload.catalog_scope === "full" ? { catalogScope: "full" } : {}),
155
+ ...(typeof payload.iat === "number" ? { issuedAt: payload.iat } : {}),
151
156
  };
152
157
  }
153
158
  catch {
@@ -140,13 +140,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
140
140
  clientHint?: undefined;
141
141
  timestamp?: undefined;
142
142
  chapterId?: undefined;
143
- count?: undefined;
144
143
  startAt?: undefined;
145
144
  endAt?: undefined;
146
145
  reason?: undefined;
147
146
  includeMicrophone?: undefined;
148
147
  includeSystemAudio?: undefined;
149
148
  requestId?: undefined;
149
+ count?: undefined;
150
150
  };
151
151
  required?: undefined;
152
152
  };
@@ -171,13 +171,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
171
171
  clientHint?: undefined;
172
172
  timestamp?: undefined;
173
173
  chapterId?: undefined;
174
- count?: undefined;
175
174
  startAt?: undefined;
176
175
  endAt?: undefined;
177
176
  reason?: undefined;
178
177
  includeMicrophone?: undefined;
179
178
  includeSystemAudio?: undefined;
180
179
  requestId?: undefined;
180
+ count?: undefined;
181
181
  };
182
182
  required?: undefined;
183
183
  };
@@ -196,13 +196,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
196
196
  clientHint?: undefined;
197
197
  timestamp?: undefined;
198
198
  chapterId?: undefined;
199
- count?: undefined;
200
199
  startAt?: undefined;
201
200
  endAt?: undefined;
202
201
  reason?: undefined;
203
202
  includeMicrophone?: undefined;
204
203
  includeSystemAudio?: undefined;
205
204
  requestId?: undefined;
205
+ count?: undefined;
206
206
  };
207
207
  required?: undefined;
208
208
  };
@@ -231,13 +231,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
231
231
  };
232
232
  timestamp?: undefined;
233
233
  chapterId?: undefined;
234
- count?: undefined;
235
234
  startAt?: undefined;
236
235
  endAt?: undefined;
237
236
  reason?: undefined;
238
237
  includeMicrophone?: undefined;
239
238
  includeSystemAudio?: undefined;
240
239
  requestId?: undefined;
240
+ count?: undefined;
241
241
  };
242
242
  };
243
243
  } | {
@@ -260,12 +260,12 @@ export declare function screenMemoryMcpToolDefinitions(): ({
260
260
  description: string;
261
261
  };
262
262
  chapterId?: undefined;
263
- count?: undefined;
264
263
  startAt?: undefined;
265
264
  endAt?: undefined;
266
265
  includeMicrophone?: undefined;
267
266
  includeSystemAudio?: undefined;
268
267
  requestId?: undefined;
268
+ count?: undefined;
269
269
  };
270
270
  };
271
271
  } | {
@@ -317,7 +317,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
317
317
  clientHint?: undefined;
318
318
  timestamp?: undefined;
319
319
  chapterId?: undefined;
320
- count?: undefined;
321
320
  startAt: {
322
321
  type: string;
323
322
  description: string;
@@ -337,6 +336,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
337
336
  type: string;
338
337
  };
339
338
  requestId?: undefined;
339
+ count?: undefined;
340
340
  };
341
341
  };
342
342
  } | {
@@ -352,7 +352,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
352
352
  clientHint?: undefined;
353
353
  timestamp?: undefined;
354
354
  chapterId?: undefined;
355
- count?: undefined;
356
355
  startAt?: undefined;
357
356
  endAt?: undefined;
358
357
  reason?: undefined;
@@ -361,6 +360,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
361
360
  requestId: {
362
361
  type: string;
363
362
  };
363
+ count?: undefined;
364
364
  };
365
365
  };
366
366
  })[];
@@ -2,7 +2,9 @@ import { defineEventHandler, setResponseStatus, setResponseHeader, getMethod, ge
2
2
  import { getAppConfig } from "../app-config/store.js";
3
3
  import { getConfiguredAppBasePath } from "../server/app-base-path.js";
4
4
  import { isLoopbackRequest } from "../server/auth.js";
5
+ import { CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE } from "../server/credential-membership-unavailable.js";
5
6
  import { getH3App } from "../server/framework-request-handler.js";
7
+ import { getOrigin } from "../server/google-oauth.js";
6
8
  import { readBody } from "../server/h3-helpers.js";
7
9
  import { trackMcpInitialize } from "./analytics.js";
8
10
  import { createMCPServerForRequest, verifyAuth, getAccessTokens, resolveOrgIdFromDomain, buildLinkArtifacts, McpDirectoryProfileValidationError, validateMcpDirectoryProfile, validateMcpDirectoryWidgetDomain, selectMcpActionSurface, } from "./build-server.js";
@@ -186,7 +188,19 @@ export async function handleMcpRequest(event, config, routePath = MCP_PUBLIC_ROU
186
188
  isLoopbackOrigin(requestMeta.origin) &&
187
189
  (hasLocalOwnerHint || process.env.AGENT_NATIVE_MCP_DEV_OPEN === "1"),
188
190
  resourceUrl: getMcpOAuthAudiences(event, routePath),
191
+ requestOrigin: getOrigin(event),
189
192
  });
193
+ if (!authResult.authed && authResult.unavailable) {
194
+ // The token is valid but its org membership could not be checked. No auth
195
+ // challenge: re-authenticating would not help, and the client must keep
196
+ // its tokens and retry.
197
+ setResponseStatus(event, 503);
198
+ setResponseHeader(event, "Retry-After", "5");
199
+ return {
200
+ error: "Service Unavailable",
201
+ message: CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE,
202
+ };
203
+ }
190
204
  if (!authResult.authed) {
191
205
  setResponseStatus(event, 401);
192
206
  setResponseHeader(event, "WWW-Authenticate", buildMcpOAuthChallenge(event, routePath));
@@ -230,7 +230,7 @@ const emailStep = {
230
230
  label: "Set email variables on your host",
231
231
  description: "See which variables each email provider needs and how to set them.",
232
232
  payload: {
233
- url: "https://www.agent-native.com/docs/deployment#email-provider",
233
+ url: "https://www.agent-native.com/docs/deployment?utm_source=app&utm_medium=product&utm_content=onboarding-email-provider#email-provider",
234
234
  external: true,
235
235
  },
236
236
  },
@@ -72,9 +72,9 @@ export declare const setWorkspaceAppDefaultVisibilityHandler: import("h3").Event
72
72
  }>>;
73
73
  export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
74
74
  success: boolean;
75
- id?: undefined;
76
75
  name?: undefined;
77
76
  role?: undefined;
77
+ id?: undefined;
78
78
  } | {
79
79
  success?: undefined;
80
80
  id: string;
@@ -179,13 +179,13 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
179
179
  export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
180
180
  name?: undefined;
181
181
  success: boolean;
182
- id?: undefined;
183
182
  role?: undefined;
183
+ id?: undefined;
184
184
  } | {
185
+ success?: undefined;
185
186
  id: string;
186
187
  name: string;
187
188
  role: import("./types.js").OrgRole;
188
- success?: undefined;
189
189
  }>>;
190
190
  export declare const updateOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
191
191
  orgId: string;
@@ -1,2 +1,4 @@
1
1
  export declare function isMissingOrganizationTableError(error: unknown): boolean;
2
- export declare function isOrgMember(orgId: string, email: string): Promise<boolean>;
2
+ export declare function isOrgMember(orgId: string, email: string, options?: {
3
+ requireOrganizationMetadata?: boolean;
4
+ }): Promise<boolean>;
@@ -17,7 +17,7 @@ export function isMissingOrganizationTableError(error) {
17
17
  }
18
18
  return false;
19
19
  }
20
- export async function isOrgMember(orgId, email) {
20
+ export async function isOrgMember(orgId, email, options = {}) {
21
21
  const normalized = email.trim().toLowerCase();
22
22
  if (!orgId || !normalized)
23
23
  return false;
@@ -41,11 +41,14 @@ export async function isOrgMember(orgId, email) {
41
41
  })).rows;
42
42
  }
43
43
  catch (error) {
44
- if (!isMissingOrganizationTableError(error))
44
+ if (options.requireOrganizationMetadata ||
45
+ !isMissingOrganizationTableError(error))
45
46
  throw error;
46
47
  return true;
47
48
  }
48
49
  const organization = organizationRows[0];
50
+ if (!organization && options.requireOrganizationMetadata)
51
+ return false;
49
52
  const linked = String(organization?.identity_authority ?? "").trim() ||
50
53
  String(organization?.identity_id ?? "").trim();
51
54
  if (!linked)
@@ -76,8 +76,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
78
  id?: undefined;
79
- message?: undefined;
80
79
  deleted?: undefined;
80
+ provider?: undefined;
81
81
  providers: {
82
82
  id: string;
83
83
  label: string;
@@ -88,45 +88,45 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
88
88
  updatedAt: number;
89
89
  }[];
90
90
  count: number;
91
- provider?: undefined;
92
91
  found?: undefined;
93
92
  registered?: undefined;
94
93
  label?: undefined;
94
+ message?: undefined;
95
95
  } | {
96
96
  id?: undefined;
97
- count?: undefined;
98
- message?: undefined;
99
97
  deleted?: undefined;
100
98
  providers?: undefined;
99
+ count?: undefined;
101
100
  found: boolean;
102
101
  provider: import("../custom-registry.js").CustomProviderConfig;
103
102
  registered?: undefined;
104
103
  label?: undefined;
105
- } | {
106
- count?: undefined;
107
104
  message?: undefined;
105
+ } | {
108
106
  deleted?: undefined;
109
- providers?: undefined;
110
107
  provider?: undefined;
108
+ providers?: undefined;
109
+ count?: undefined;
111
110
  found: boolean;
112
111
  id: string;
113
112
  registered?: undefined;
114
113
  label?: undefined;
115
- } | {
116
- count?: undefined;
117
114
  message?: undefined;
118
- providers?: undefined;
115
+ } | {
119
116
  provider?: undefined;
117
+ providers?: undefined;
118
+ count?: undefined;
120
119
  found?: undefined;
121
120
  deleted: boolean;
122
121
  id: string;
123
122
  registered?: undefined;
124
123
  label?: undefined;
124
+ message?: undefined;
125
125
  } | {
126
- count?: undefined;
127
126
  deleted?: undefined;
128
- providers?: undefined;
129
127
  provider?: undefined;
128
+ providers?: undefined;
129
+ count?: undefined;
130
130
  found?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
@@ -312,8 +312,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
312
312
  scope?: "org" | "user" | undefined;
313
313
  }, {
314
314
  provider?: undefined;
315
- id?: undefined;
316
315
  deleted?: undefined;
316
+ message?: undefined;
317
+ id?: undefined;
317
318
  providers: {
318
319
  id: string;
319
320
  label: string;
@@ -327,42 +328,41 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
327
328
  found?: undefined;
328
329
  registered?: undefined;
329
330
  label?: undefined;
330
- message?: undefined;
331
331
  } | {
332
- id?: undefined;
332
+ count?: undefined;
333
333
  deleted?: undefined;
334
+ message?: undefined;
335
+ id?: undefined;
334
336
  providers?: undefined;
335
- count?: undefined;
336
337
  found: boolean;
337
338
  provider: import("../custom-registry.js").CustomProviderConfig;
338
339
  registered?: undefined;
339
340
  label?: undefined;
340
- message?: undefined;
341
341
  } | {
342
342
  provider?: undefined;
343
+ count?: undefined;
343
344
  deleted?: undefined;
345
+ message?: undefined;
344
346
  providers?: undefined;
345
- count?: undefined;
346
347
  found: boolean;
347
348
  id: string;
348
349
  registered?: undefined;
349
350
  label?: undefined;
350
- message?: undefined;
351
351
  } | {
352
352
  provider?: undefined;
353
- providers?: undefined;
354
353
  count?: undefined;
354
+ message?: undefined;
355
+ providers?: undefined;
355
356
  found?: undefined;
356
357
  deleted: boolean;
357
358
  id: string;
358
359
  registered?: undefined;
359
360
  label?: undefined;
360
- message?: undefined;
361
361
  } | {
362
362
  provider?: undefined;
363
+ count?: undefined;
363
364
  deleted?: undefined;
364
365
  providers?: undefined;
365
- count?: undefined;
366
366
  found?: undefined;
367
367
  registered: boolean;
368
368
  id: string;
@@ -73,6 +73,7 @@ export declare function createProviderCorpusJobAction(options: CreateProviderCor
73
73
  offset?: unknown;
74
74
  limit?: unknown;
75
75
  }, {
76
+ deleted?: undefined;
76
77
  jobs: {
77
78
  id: string;
78
79
  name: string;
@@ -83,7 +84,6 @@ export declare function createProviderCorpusJobAction(options: CreateProviderCor
83
84
  updatedAt: string;
84
85
  }[];
85
86
  total: number;
86
- deleted?: undefined;
87
87
  jobId?: undefined;
88
88
  } | {
89
89
  jobs?: undefined;
@@ -91,9 +91,9 @@ export declare function createProviderCorpusJobAction(options: CreateProviderCor
91
91
  deleted: boolean;
92
92
  jobId: string;
93
93
  } | {
94
+ deleted?: undefined;
94
95
  jobs?: undefined;
95
96
  total?: undefined;
96
- deleted?: undefined;
97
97
  jobId?: undefined;
98
98
  hits: Record<string, unknown>[];
99
99
  offset: number;
@@ -73,6 +73,8 @@ export declare function resourceChangeTriggerNames(source: ResourceChangeSource)
73
73
  * committed between a drop and a create would never be recorded.
74
74
  */
75
75
  export declare function resourceChangeTriggerSql(source: ResourceChangeSource): string[];
76
+ /** A hash of the capture SQL for a source, which changes whenever it does. */
77
+ export declare function resourceChangeCaptureFingerprint(source: ResourceChangeSource): string;
76
78
  /**
77
79
  * Installs change capture for a source and subscribes a consumer to it.
78
80
  * Apps call this from a named migration, so the triggers ship with the app's
@@ -230,6 +230,10 @@ $an_rc$`,
230
230
  `CREATE OR REPLACE TRIGGER "${names.truncateTrigger}" BEFORE TRUNCATE ON "${table}" FOR EACH STATEMENT EXECUTE FUNCTION "${names.function}"()`,
231
231
  ];
232
232
  }
233
+ /** A hash of the capture SQL for a source, which changes whenever it does. */
234
+ export function resourceChangeCaptureFingerprint(source) {
235
+ return shortHash(resourceChangeTriggerSql(source).join("\n"));
236
+ }
233
237
  /**
234
238
  * Installs change capture for a source and subscribes a consumer to it.
235
239
  * Apps call this from a named migration, so the triggers ship with the app's
@@ -39,8 +39,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
39
39
  error: string;
40
40
  }>;
41
41
  export declare function handleDeleteResource(event: any): Promise<{
42
- ok?: undefined;
43
42
  error: string;
43
+ ok?: undefined;
44
44
  } | {
45
45
  error?: undefined;
46
46
  ok: boolean;
@@ -1,8 +1,8 @@
1
1
  import path from "node:path";
2
- import { getDatabaseUrl, toPostgresParams } from "../../db/client.js";
2
+ import { getDatabaseUrl } from "../../db/client.js";
3
3
  import { parseArgs, fail } from "../utils.js";
4
4
  import { createPostgresScriptClient } from "./postgres-client.js";
5
- import { assertNoRawDbAccessControlWrite, assertNoSchemaQualifiedTables, assertNoSensitiveFrameworkTables, } from "./safety.js";
5
+ import { finalRawDbSql, rawDbInsertTarget, readRawDbWriteStatement, verifyRawDbStatement, } from "./safety.js";
6
6
  import { buildScopingPostgres } from "./scoping.js";
7
7
  function parseSqlArgs(raw, label = "--args") {
8
8
  if (!raw)
@@ -50,106 +50,20 @@ function parseStatements(parsed) {
50
50
  }
51
51
  return [{ sql: parsed.sql, args: parseSqlArgs(parsed.args) }];
52
52
  }
53
- function stripLeadingSqlComments(sql) {
54
- return sql
55
- .replace(/^\s*--[^\n]*\n/gm, "")
56
- .replace(/\/\*[\s\S]*?\*\//g, "")
57
- .trim();
58
- }
59
- function hasAdditionalStatement(sql) {
60
- let state = "normal";
61
- for (let i = 0; i < sql.length; i++) {
62
- const ch = sql[i];
63
- const next = sql[i + 1];
64
- if (state === "line") {
65
- if (ch === "\n")
66
- state = "normal";
67
- continue;
68
- }
69
- if (state === "block") {
70
- if (ch === "*" && next === "/") {
71
- i++;
72
- state = "normal";
73
- }
74
- continue;
75
- }
76
- if (state === "single") {
77
- if (ch === "'" && next === "'")
78
- i++;
79
- else if (ch === "'")
80
- state = "normal";
81
- continue;
82
- }
83
- if (state === "double") {
84
- if (ch === '"' && next === '"')
85
- i++;
86
- else if (ch === '"')
87
- state = "normal";
88
- continue;
89
- }
90
- if (ch === "-" && next === "-") {
91
- i++;
92
- state = "line";
93
- }
94
- else if (ch === "/" && next === "*") {
95
- i++;
96
- state = "block";
97
- }
98
- else if (ch === "'") {
99
- state = "single";
100
- }
101
- else if (ch === '"') {
102
- state = "double";
103
- }
104
- else if (ch === ";") {
105
- return sql.slice(i + 1).trim().length > 0;
106
- }
107
- }
108
- return false;
109
- }
110
- function normalizeUserSql(sql, index) {
111
- const normalized = stripLeadingSqlComments(sql);
112
- if (!normalized)
113
- fail(`Statement ${index} is empty`);
114
- if (hasAdditionalStatement(normalized)) {
115
- fail(`Statement ${index} contains multiple SQL statements. Use --statements for batches so each write can be validated and run transactionally.`);
116
- }
117
- return normalized.replace(/;\s*$/, "");
118
- }
119
- function validateWriteSql(sql, index) {
120
- const normalized = normalizeUserSql(sql, index);
121
- const upper = normalized.toUpperCase();
122
- const allowed = ["INSERT", "UPDATE", "DELETE"];
123
- const blocked = ["SELECT", "WITH", "EXPLAIN"];
124
- if (blocked.some((keyword) => upper.startsWith(keyword))) {
125
- fail(`Statement ${index}: use db-query for read statements. db-exec is for writes only.`);
126
- }
127
- if (upper.startsWith("CREATE") || upper.startsWith("ALTER")) {
128
- fail(`Statement ${index}: schema changes are not allowed through db-exec. Additive schema changes must go through reviewed migrations or startup code.`);
129
- }
130
- if (!allowed.some((keyword) => upper.startsWith(keyword))) {
131
- fail(`Statement ${index}: only ${allowed.join(", ")} statements are allowed. Dangerous operations such as DROP, TRUNCATE, GRANT, and REVOKE are blocked.`);
132
- }
133
- assertNoSensitiveFrameworkTables(normalized, "write");
134
- assertNoRawDbAccessControlWrite(normalized);
135
- assertNoSchemaQualifiedTables(normalized, "write");
136
- return normalized;
137
- }
138
53
  const INSERT_INTO = "INSERT\\s+INTO";
139
- function injectOwnership(sql, scoping) {
140
- if (!scoping.active)
54
+ function injectOwnership(statement, scoping) {
55
+ const sql = statement.sql;
56
+ if (!scoping.active || statement.keyword !== "insert")
141
57
  return sql;
142
- if (!stripLeadingSqlComments(sql).toUpperCase().startsWith("INSERT")) {
58
+ const target = rawDbInsertTarget(statement);
59
+ if (!target)
143
60
  return sql;
144
- }
145
- const match = sql.match(new RegExp(`${INSERT_INTO}\\s+["']?(\\w+)["']?`, "i"));
146
- if (!match)
147
- return sql;
148
- const tableName = match[1];
61
+ const tableName = target.table;
62
+ const listed = new Set(target.columns ?? []);
149
63
  const injections = [];
150
64
  if (scoping.userEmail &&
151
65
  scoping.ownerEmailTables.has(tableName) &&
152
- !/owner_email/i.test(sql)) {
66
+ !listed.has("owner_email")) {
153
67
  injections.push({
154
68
  column: "owner_email",
155
69
  value: `'${scoping.userEmail.replace(/'/g, "''")}'`,
@@ -157,7 +71,7 @@ function injectOwnership(sql, scoping) {
157
71
  }
158
72
  if (scoping.orgId &&
159
73
  scoping.orgIdTables.has(tableName) &&
160
- !/org_id/i.test(sql)) {
74
+ !listed.has("org_id")) {
161
75
  injections.push({
162
76
  column: "org_id",
163
77
  value: `'${scoping.orgId.replace(/'/g, "''")}'`,
@@ -227,7 +141,7 @@ Options:
227
141
  return;
228
142
  }
229
143
  const statements = parseStatements(parsed).map((statement, index) => ({
230
- sql: validateWriteSql(statement.sql, index + 1),
144
+ statement: readRawDbWriteStatement(statement.sql, `Statement ${index + 1}`),
231
145
  args: statement.args,
232
146
  }));
233
147
  const url = parsed.db
@@ -243,14 +157,27 @@ Options:
243
157
  try {
244
158
  for (let index = 0; index < statements.length; index++) {
245
159
  const statement = statements[index];
246
- const sql = toPostgresParams(injectOwnership(statement.sql, scoping));
160
+ const label = `Statement ${index + 1}`;
161
+ // Ownership injection rewrites the text, so the guards re-read the
162
+ // final SQL and check it against this transaction's state right
163
+ // before it runs, after any earlier statement in the batch.
164
+ const executed = finalRawDbSql(injectOwnership(statement.statement, scoping), "write", label);
165
+ await verifyRawDbStatement(tx, executed.statement, {
166
+ columnsByTable: scoping.columnsByTable,
167
+ label,
168
+ });
169
+ const sql = executed.sql;
247
170
  try {
248
- const result = await tx.unsafe(sql, statement.args);
171
+ const result = await tx.unsafe(sql, statement.args, {
172
+ singleStatement: true,
173
+ });
249
174
  results.push({
250
175
  index: index + 1,
251
176
  sql,
252
177
  changes: result.count ?? 0,
253
- rows: /\bRETURNING\b/i.test(sql) ? Array.from(result) : [],
178
+ rows: executed.statement.tokens.some((token) => token.kind === "word" && token.value === "returning")
179
+ ? Array.from(result)
180
+ : [],
254
181
  });
255
182
  }
256
183
  catch (error) {