@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
@@ -73,6 +73,9 @@ export async function ensureTable() {
73
73
  }
74
74
  return _initPromise;
75
75
  }
76
+ export async function ensureConnectTables() {
77
+ await ensureTable();
78
+ }
76
79
  export function serviceIdentityEmail(serviceName, orgId) {
77
80
  return `svc-${normalizeServiceName(serviceName)}@service.${orgId}`;
78
81
  }
@@ -96,11 +99,12 @@ export function normalizeServiceName(raw) {
96
99
  * Persist a record of a minted token. The token value itself (a signed JWT)
97
100
  * is NEVER stored — only its `jti`, so revocation is a cheap SQL lookup.
98
101
  */
99
- export async function recordMintedToken(params) {
100
- await ensureTable();
101
- const client = getDbExec();
102
+ export async function recordMintedToken(params, exec) {
103
+ if (!exec)
104
+ await ensureTable();
105
+ const client = exec ?? getDbExec();
102
106
  const id = randomUUID();
103
- await client.execute({
107
+ const result = await client.execute({
104
108
  sql: `INSERT INTO mcp_connect_tokens (id, jti, owner_email, org_id, label, kind, service_name, created_by, created_at, last_used_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
105
109
  args: [
106
110
  id,
@@ -116,6 +120,9 @@ export async function recordMintedToken(params) {
116
120
  null,
117
121
  ],
118
122
  });
123
+ if (result.rowsAffected !== 1) {
124
+ throw new Error("Unexpected affected row count for MCP connect token insert.");
125
+ }
119
126
  return id;
120
127
  }
121
128
  /**
@@ -124,40 +131,45 @@ export async function recordMintedToken(params) {
124
131
  * lock every connected agent out. Signature verification is unaffected — this
125
132
  * is only the post-verify revoke check (see `verifyAuth` in build-server.ts).
126
133
  */
134
+ /**
135
+ * Throws when the revocation state can't be read. Answering "not revoked"
136
+ * instead would let a transient read failure admit a revoked token.
137
+ */
127
138
  export async function isJtiRevoked(jti) {
128
- try {
129
- await ensureTable();
130
- const client = getDbExec();
131
- const { rows } = await client.execute({
132
- sql: `SELECT revoked_at FROM mcp_connect_tokens WHERE jti = ?`,
133
- args: [jti],
134
- });
135
- if (rows.length === 0)
136
- return false;
137
- const revokedAt = rows[0].revoked_at ?? rows[0].revokedAt;
138
- return revokedAt != null;
139
- }
140
- catch (err) {
141
- // Fail open: a DB blip must not turn every minted token into a 401.
142
- // (Signature checks already passed; this only gates explicit revokes.)
143
- if (isConnectionError(err))
144
- return false;
139
+ await ensureTable();
140
+ const client = getDbExec();
141
+ const { rows } = await client.execute({
142
+ sql: `SELECT revoked_at FROM mcp_connect_tokens WHERE jti = ?`,
143
+ args: [jti],
144
+ });
145
+ if (rows.length === 0)
145
146
  return false;
146
- }
147
+ const revokedAt = rows[0].revoked_at ?? rows[0].revokedAt;
148
+ return revokedAt != null;
147
149
  }
148
150
  export async function lookupConnectTokenOrg(jti) {
149
151
  try {
150
152
  await ensureTable();
151
153
  const client = getDbExec();
152
154
  const { rows } = await client.execute({
153
- sql: `SELECT org_id FROM mcp_connect_tokens WHERE jti = ?`,
155
+ sql: `SELECT org_id, owner_email, kind FROM mcp_connect_tokens WHERE jti = ?`,
154
156
  args: [jti],
155
157
  });
156
158
  if (rows.length === 0)
157
159
  return { status: "missing" };
158
160
  const rawOrgId = rows[0].org_id ?? rows[0].orgId;
161
+ const ownerEmail = rows[0].owner_email ?? rows[0].ownerEmail;
162
+ const kind = rows[0].kind;
163
+ if (typeof ownerEmail !== "string" ||
164
+ !ownerEmail.trim() ||
165
+ (kind !== "personal" && kind !== "service") ||
166
+ (rawOrgId != null && (typeof rawOrgId !== "string" || !rawOrgId.trim()))) {
167
+ return { status: "unavailable" };
168
+ }
159
169
  return {
160
170
  status: "found",
171
+ ownerEmail,
172
+ kind,
161
173
  orgId: typeof rawOrgId === "string" && rawOrgId.trim()
162
174
  ? rawOrgId.trim()
163
175
  : null,
@@ -341,9 +353,10 @@ function mapDeviceRow(r) {
341
353
  consumedAt: numOrNull(r.consumed_at ?? r.consumedAt),
342
354
  };
343
355
  }
344
- export async function getDeviceCode(deviceCode) {
345
- await ensureTable();
346
- const client = getDbExec();
356
+ export async function getDeviceCode(deviceCode, exec) {
357
+ if (!exec)
358
+ await ensureTable();
359
+ const client = exec ?? getDbExec();
347
360
  const { rows } = await client.execute({
348
361
  sql: `SELECT * FROM mcp_device_codes WHERE device_code = ?`,
349
362
  args: [deviceCode],
@@ -352,9 +365,10 @@ export async function getDeviceCode(deviceCode) {
352
365
  return null;
353
366
  return mapDeviceRow(rows[0]);
354
367
  }
355
- export async function getDeviceCodeByUserCode(userCode) {
356
- await ensureTable();
357
- const client = getDbExec();
368
+ export async function getDeviceCodeByUserCode(userCode, exec) {
369
+ if (!exec)
370
+ await ensureTable();
371
+ const client = exec ?? getDbExec();
358
372
  const { rows } = await client.execute({
359
373
  sql: `SELECT * FROM mcp_device_codes WHERE user_code = ?`,
360
374
  args: [userCode],
@@ -363,10 +377,18 @@ export async function getDeviceCodeByUserCode(userCode) {
363
377
  return null;
364
378
  return mapDeviceRow(rows[0]);
365
379
  }
366
- export async function approveDeviceCode(userCode, ownerEmail, orgId) {
367
- await ensureTable();
368
- const client = getDbExec();
369
- const row = await getDeviceCodeByUserCode(userCode);
380
+ function deviceCodeMutationSucceeded(rowsAffected) {
381
+ if (rowsAffected === 1)
382
+ return true;
383
+ if (rowsAffected === 0)
384
+ return false;
385
+ throw new Error("Unexpected affected row count for MCP device code mutation.");
386
+ }
387
+ export async function approveDeviceCode(userCode, ownerEmail, orgId, exec) {
388
+ if (!exec)
389
+ await ensureTable();
390
+ const client = exec ?? getDbExec();
391
+ const row = await getDeviceCodeByUserCode(userCode, client);
370
392
  if (!row)
371
393
  return "not_found";
372
394
  if ((row.expiresAt ?? 0) < Date.now())
@@ -374,11 +396,13 @@ export async function approveDeviceCode(userCode, ownerEmail, orgId) {
374
396
  if (row.status !== "pending")
375
397
  return "already";
376
398
  const result = await client.execute({
377
- sql: `UPDATE mcp_device_codes SET status = 'approved', owner_email = ?, org_id = ? WHERE user_code = ? AND status = 'pending'`,
378
- args: [ownerEmail, orgId, userCode],
399
+ sql: `UPDATE mcp_device_codes SET status = 'approved', owner_email = ?, org_id = ? WHERE user_code = ? AND status = 'pending' AND expires_at >= ?`,
400
+ args: [ownerEmail, orgId, userCode, Date.now()],
379
401
  });
380
- if (result.rowsAffected === 0) {
381
- const fresh = await getDeviceCodeByUserCode(userCode);
402
+ if (!deviceCodeMutationSucceeded(result.rowsAffected)) {
403
+ const fresh = await getDeviceCodeByUserCode(userCode, client);
404
+ if (fresh && (fresh.expiresAt ?? 0) < Date.now())
405
+ return "expired";
382
406
  return fresh && fresh.status !== "pending" ? "already" : "not_found";
383
407
  }
384
408
  return {
@@ -388,49 +412,53 @@ export async function approveDeviceCode(userCode, ownerEmail, orgId) {
388
412
  orgId,
389
413
  };
390
414
  }
391
- export async function consumeDeviceCode(deviceCode, tokenJti) {
392
- await ensureTable();
393
- const client = getDbExec();
394
- const row = await getDeviceCode(deviceCode);
415
+ export async function consumeDeviceCode(deviceCode, tokenJti, exec) {
416
+ if (!exec)
417
+ await ensureTable();
418
+ const client = exec ?? getDbExec();
419
+ const row = await getDeviceCode(deviceCode, client);
395
420
  if (!row)
396
421
  return null;
397
- if (row.status !== "approved")
422
+ if (row.status !== "approved" || (row.expiresAt ?? 0) < Date.now())
398
423
  return null;
399
424
  const result = await client.execute({
400
- sql: `UPDATE mcp_device_codes SET status = 'consumed', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved'`,
401
- args: [tokenJti, Date.now(), deviceCode],
425
+ sql: `UPDATE mcp_device_codes SET status = 'consumed', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved' AND expires_at >= ?`,
426
+ args: [tokenJti, Date.now(), deviceCode, Date.now()],
402
427
  });
403
- if (result.rowsAffected === 0)
428
+ if (!deviceCodeMutationSucceeded(result.rowsAffected))
404
429
  return null;
405
430
  return row;
406
431
  }
407
- export async function claimDeviceCodeForMint(deviceCode, tokenJti) {
408
- await ensureTable();
409
- const client = getDbExec();
410
- const row = await getDeviceCode(deviceCode);
411
- if (!row || row.status !== "approved")
432
+ export async function claimDeviceCodeForMint(deviceCode, tokenJti, exec) {
433
+ if (!exec)
434
+ await ensureTable();
435
+ const client = exec ?? getDbExec();
436
+ const row = await getDeviceCode(deviceCode, client);
437
+ if (!row || row.status !== "approved" || (row.expiresAt ?? 0) < Date.now())
412
438
  return null;
413
439
  const result = await client.execute({
414
- sql: `UPDATE mcp_device_codes SET status = 'minting', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved'`,
415
- args: [tokenJti, Date.now(), deviceCode],
440
+ sql: `UPDATE mcp_device_codes SET status = 'minting', token_jti = ?, consumed_at = ? WHERE device_code = ? AND status = 'approved' AND expires_at >= ?`,
441
+ args: [tokenJti, Date.now(), deviceCode, Date.now()],
416
442
  });
417
- if (result.rowsAffected === 0)
443
+ if (!deviceCodeMutationSucceeded(result.rowsAffected))
418
444
  return null;
419
445
  return row;
420
446
  }
421
- export async function finishDeviceCodeMint(deviceCode, tokenJti) {
422
- await ensureTable();
423
- const client = getDbExec();
447
+ export async function finishDeviceCodeMint(deviceCode, tokenJti, exec) {
448
+ if (!exec)
449
+ await ensureTable();
450
+ const client = exec ?? getDbExec();
424
451
  const result = await client.execute({
425
452
  sql: `UPDATE mcp_device_codes SET status = 'consumed' WHERE device_code = ? AND status = 'minting' AND token_jti = ?`,
426
453
  args: [deviceCode, tokenJti],
427
454
  });
428
- return result.rowsAffected > 0;
455
+ return deviceCodeMutationSucceeded(result.rowsAffected);
429
456
  }
430
- export async function releaseDeviceCodeMint(deviceCode, tokenJti) {
457
+ export async function releaseDeviceCodeMint(deviceCode, tokenJti, exec) {
431
458
  try {
432
- await ensureTable();
433
- const client = getDbExec();
459
+ if (!exec)
460
+ await ensureTable();
461
+ const client = exec ?? getDbExec();
434
462
  await client.execute({
435
463
  sql: `UPDATE mcp_device_codes SET status = 'approved', token_jti = NULL, consumed_at = NULL WHERE device_code = ? AND status = 'minting' AND token_jti = ?`,
436
464
  args: [deviceCode, tokenJti],
@@ -0,0 +1,23 @@
1
+ import { type DbExec } from "../db/client.js";
2
+ import type { OrgRole } from "../org/types.js";
3
+ export declare class McpCredentialIssuanceError extends Error {
4
+ /**
5
+ * `not-member`: the owner lacks the live membership or role the credential
6
+ * needs, or holds an address an email change retired.
7
+ */
8
+ readonly reason: "not-member" | "unavailable";
9
+ constructor(
10
+ /**
11
+ * `not-member`: the owner lacks the live membership or role the credential
12
+ * needs, or holds an address an email change retired.
13
+ */
14
+ reason: "not-member" | "unavailable", options?: ErrorOptions);
15
+ }
16
+ export declare function withMcpCredentialIssuance<T>(input: {
17
+ email: string;
18
+ orgId?: string | null;
19
+ requestOrigin?: string;
20
+ kind?: "personal" | "service";
21
+ /** Roles allowed to issue, rechecked under the membership lock. */
22
+ roles?: readonly OrgRole[];
23
+ }, run: (tx: DbExec) => Promise<T>): Promise<T>;
@@ -0,0 +1,77 @@
1
+ import { getDbExec } from "../db/client.js";
2
+ import { identityCredentialLockKey, readEmailRetiredAt, } from "../identity/retired-emails.js";
3
+ import { checkCredentialOrgMembership } from "./credential-membership.js";
4
+ export class McpCredentialIssuanceError extends Error {
5
+ reason;
6
+ constructor(
7
+ /**
8
+ * `not-member`: the owner lacks the live membership or role the credential
9
+ * needs, or holds an address an email change retired.
10
+ */
11
+ reason, options) {
12
+ super(reason, options);
13
+ this.reason = reason;
14
+ this.name = "McpCredentialIssuanceError";
15
+ }
16
+ }
17
+ export async function withMcpCredentialIssuance(input, run) {
18
+ const orgId = input.orgId?.trim() || null;
19
+ const email = input.email.trim().toLowerCase();
20
+ const human = input.kind !== "service";
21
+ const requiresMembership = orgId !== null && human;
22
+ if (requiresMembership) {
23
+ // Federation validation may update membership on another connection.
24
+ // Finish it before acquiring the local offboarding lock.
25
+ const membership = await checkCredentialOrgMembership({
26
+ orgId,
27
+ email,
28
+ requestOrigin: input.requestOrigin,
29
+ });
30
+ if (membership !== "member") {
31
+ throw new McpCredentialIssuanceError(membership);
32
+ }
33
+ }
34
+ const db = getDbExec();
35
+ if (!db.transaction)
36
+ throw new McpCredentialIssuanceError("unavailable");
37
+ try {
38
+ return await db.transaction(async (tx) => {
39
+ if (human) {
40
+ // Email rekey takes this lock for both addresses before its row locks.
41
+ await tx.execute({
42
+ sql: "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))",
43
+ args: [identityCredentialLockKey(email)],
44
+ });
45
+ if ((await readEmailRetiredAt(tx, email)) !== null) {
46
+ const { rows } = await tx.execute({
47
+ sql: `SELECT 1 FROM "user" WHERE LOWER("email") = ? LIMIT 1`,
48
+ args: [email],
49
+ });
50
+ if (rows.length === 0) {
51
+ throw new McpCredentialIssuanceError("not-member");
52
+ }
53
+ }
54
+ }
55
+ if (requiresMembership) {
56
+ const { rows } = await tx.execute({
57
+ sql: `SELECT role FROM org_members
58
+ WHERE org_id = ? AND LOWER(email) = ?
59
+ AND federation_removal_pending_at IS NULL
60
+ FOR UPDATE`,
61
+ args: [orgId, email],
62
+ });
63
+ if (rows.length === 0 ||
64
+ (input.roles &&
65
+ !input.roles.includes(String(rows[0].role)))) {
66
+ throw new McpCredentialIssuanceError("not-member");
67
+ }
68
+ }
69
+ return run(tx);
70
+ });
71
+ }
72
+ catch (error) {
73
+ if (error instanceof McpCredentialIssuanceError)
74
+ throw error;
75
+ throw new McpCredentialIssuanceError("unavailable", { cause: error });
76
+ }
77
+ }
@@ -0,0 +1,26 @@
1
+ import type { StoredConnectTokenIdentity } from "./connect-store.js";
2
+ /**
3
+ * `unavailable` means the check could not run (database or identity-authority
4
+ * error, or organization tables that don't exist yet). It is neither answer:
5
+ * callers must refuse the request with a retryable error, and must not revoke
6
+ * anything on it.
7
+ */
8
+ export type CredentialOrgMembership = "member" | "not-member" | "unavailable";
9
+ export declare function checkCredentialOrgMembership(input: {
10
+ orgId: string;
11
+ email: string | undefined;
12
+ storedConnectToken?: StoredConnectTokenIdentity;
13
+ /** This app's public origin; federated orgs need it to reach the identity authority. */
14
+ requestOrigin?: string;
15
+ }): Promise<CredentialOrgMembership>;
16
+ /**
17
+ * `retired`: an email change moved this account off the credential's subject
18
+ * address after the credential was signed. `unavailable` means the same as for
19
+ * membership: refuse with a retryable error.
20
+ */
21
+ export type CredentialEmailRetirement = "current" | "retired" | "unavailable";
22
+ export declare function checkCredentialEmailRetirement(input: {
23
+ email: string;
24
+ /** The credential's `iat`, in seconds; absent counts as signed before. */
25
+ issuedAt: number | undefined;
26
+ }): Promise<CredentialEmailRetirement>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Live organization-membership check for long-lived MCP credentials.
3
+ *
4
+ * MCP OAuth access and refresh tokens, authorization codes, and connect tokens
5
+ * carry the organization they were issued for (a signed `org_id` claim or the
6
+ * stored org of a connect token). Removing a member does not touch those
7
+ * credentials, so the org they name is only an assertion from issuance time.
8
+ * Every use re-checks it here: two indexed queries (the membership row, then
9
+ * the organization row), plus a call to the identity authority when the org
10
+ * is linked. Nothing is cached, so a removal on any instance takes effect on
11
+ * the next request everywhere.
12
+ */
13
+ import { getDbExec } from "../db/client.js";
14
+ import { readEmailRetiredAt } from "../identity/retired-emails.js";
15
+ import { isMissingOrganizationTableError, isOrgMember, } from "../org/membership.js";
16
+ import { implicitServiceOrgRole } from "../org/service-identity.js";
17
+ import { getRequestContext, runWithRequestContext, } from "../server/request-context.js";
18
+ export async function checkCredentialOrgMembership(input) {
19
+ const orgId = input.orgId.trim();
20
+ const email = input.email?.trim();
21
+ if (!orgId || !email)
22
+ return "not-member";
23
+ // A human OAuth subject can use a service-shaped address. Only the local
24
+ // record of an authenticated connect token proves it is a service identity.
25
+ const stored = input.storedConnectToken;
26
+ if (stored?.kind === "service" &&
27
+ stored.ownerEmail === email &&
28
+ stored.orgId === orgId &&
29
+ implicitServiceOrgRole({ email, orgId, requestOrgId: stored.orgId })) {
30
+ return "member";
31
+ }
32
+ const lookup = () => isOrgMember(orgId, email, { requireOrganizationMetadata: true });
33
+ const context = getRequestContext();
34
+ try {
35
+ const member = !input.requestOrigin || context?.requestOrigin
36
+ ? await lookup()
37
+ : await runWithRequestContext({ ...context, requestOrigin: input.requestOrigin }, lookup);
38
+ return member ? "member" : "not-member";
39
+ }
40
+ catch (error) {
41
+ // Missing organization tables mean a partial migration or a fresh
42
+ // database, not a removal. Refuse with a retryable error; revoking a
43
+ // refresh token on this could not be undone.
44
+ if (isMissingOrganizationTableError(error)) {
45
+ console.error("[mcp] Organization tables are missing; refusing the credential without revoking it.");
46
+ return "unavailable";
47
+ }
48
+ console.error("[mcp] Organization membership check failed; refusing the credential:", error);
49
+ return "unavailable";
50
+ }
51
+ }
52
+ export async function checkCredentialEmailRetirement(input) {
53
+ try {
54
+ const retiredAt = await readEmailRetiredAt(getDbExec(), input.email);
55
+ if (retiredAt === null)
56
+ return "current";
57
+ // `iat` truncates to whole seconds, so a credential signed in the same
58
+ // second as the rekey is refused.
59
+ return input.issuedAt !== undefined && input.issuedAt * 1000 > retiredAt
60
+ ? "current"
61
+ : "retired";
62
+ }
63
+ catch (error) {
64
+ console.error("[mcp] Email retirement check failed; refusing the credential:", error);
65
+ return "unavailable";
66
+ }
67
+ }