@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
@@ -3,9 +3,12 @@ import { getHeader, getMethod, getQuery, setResponseStatus } from "h3";
3
3
  import { getOrgDomain } from "../org/context.js";
4
4
  import { getConfiguredLoginHtml, getSession } from "../server/auth.js";
5
5
  import { getAuthSecret } from "../server/better-auth-instance.js";
6
+ import { CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE } from "../server/credential-membership-unavailable.js";
7
+ import { getOrigin } from "../server/google-oauth.js";
6
8
  import { readBody } from "../server/h3-helpers.js";
9
+ import { McpCredentialIssuanceError, withMcpCredentialIssuance, } from "./credential-issuance.js";
7
10
  import { applicationTypeForRedirectUris, isAllowedOAuthRedirectUri, isUrlBasedOAuthClientId, matchesRegisteredRedirectUri, resolveOAuthClientMetadataDocument, } from "./oauth-client-metadata.js";
8
- import { createOAuthCode, createOAuthRefreshToken, consumeOAuthCode, generateOpaqueToken, getOAuthClient, getOAuthCode, getOAuthRefreshToken, registerOAuthClient, touchOAuthRefreshToken, } from "./oauth-store.js";
11
+ import { createOAuthCode, createOAuthRefreshToken, consumeOAuthCode, ensureOAuthTables, generateOpaqueToken, getOAuthClient, getOAuthCode, getOAuthRefreshToken, registerOAuthClient, revokeOAuthRefreshToken, touchOAuthRefreshToken, } from "./oauth-store.js";
9
12
  import { MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS, MCP_OAUTH_SCOPES, normalizeOAuthScope, signMcpOAuthAccessToken, } from "./oauth-token.js";
10
13
  import { resolveMcpOrgChoices } from "./org-choice.js";
11
14
  import { MCP_DIRECTORY_ROUTE_PREFIX, MCP_LEGACY_ROUTE_PREFIX, MCP_PUBLIC_ROUTE_PREFIX, MCP_ROUTE_PREFIXES, } from "./route-paths.js";
@@ -700,17 +703,37 @@ async function handleAuthorize(event, options) {
700
703
  const orgDomain = selectedOrganization
701
704
  ? await resolveOrgDomain(selectedOrganization.id)
702
705
  : undefined;
703
- const code = await createOAuthCode({
704
- clientId,
705
- redirectUri,
706
- codeChallenge: params.code_challenge,
707
- codeChallengeMethod: "S256",
708
- ownerEmail: session.email,
709
- orgId: selectedOrganization?.id ?? null,
710
- orgDomain: orgDomain ?? null,
711
- scope,
712
- resource,
713
- });
706
+ let code;
707
+ try {
708
+ await ensureOAuthTables();
709
+ code = await withMcpCredentialIssuance({
710
+ email: session.email,
711
+ orgId: selectedOrganization?.id ?? null,
712
+ requestOrigin: getOrigin(event),
713
+ }, (tx) => createOAuthCode({
714
+ clientId,
715
+ redirectUri,
716
+ codeChallenge: params.code_challenge,
717
+ codeChallengeMethod: "S256",
718
+ ownerEmail: session.email,
719
+ orgId: selectedOrganization?.id ?? null,
720
+ orgDomain: orgDomain ?? null,
721
+ scope,
722
+ resource,
723
+ }, tx));
724
+ }
725
+ catch (error) {
726
+ if (error instanceof McpCredentialIssuanceError &&
727
+ error.reason === "not-member") {
728
+ return redirectWithOAuthError({
729
+ redirectUri,
730
+ issuer,
731
+ state,
732
+ error: "access_denied",
733
+ });
734
+ }
735
+ return grantUnavailableError(CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE);
736
+ }
714
737
  let isDeepLinkRedirect = false;
715
738
  try {
716
739
  const protocol = new URL(redirectUri).protocol;
@@ -741,7 +764,7 @@ async function handleAuthorize(event, options) {
741
764
  code: code.code,
742
765
  });
743
766
  }
744
- async function issueTokenSet(params) {
767
+ async function issueTokenSet(params, tx) {
745
768
  const refreshToken = generateOpaqueToken();
746
769
  await createOAuthRefreshToken({
747
770
  refreshToken,
@@ -751,7 +774,7 @@ async function issueTokenSet(params) {
751
774
  orgDomain: params.orgDomain ?? null,
752
775
  scope: params.scope,
753
776
  resource: params.resource,
754
- });
777
+ }, tx);
755
778
  const accessToken = await signMcpOAuthAccessToken(params);
756
779
  return {
757
780
  access_token: accessToken,
@@ -761,6 +784,13 @@ async function issueTokenSet(params) {
761
784
  scope: params.scope,
762
785
  };
763
786
  }
787
+ const NOT_A_MEMBER_DESCRIPTION = "The user is no longer a member of the organization this grant was issued for";
788
+ /** Retryable: the client must keep its refresh token, not start over. */
789
+ function grantUnavailableError(description) {
790
+ const response = oauthError("temporarily_unavailable", description, 503);
791
+ response.headers.set("Retry-After", "5");
792
+ return response;
793
+ }
764
794
  async function handleAuthorizationCodeGrant(event, body) {
765
795
  const code = body.code;
766
796
  const clientId = body.client_id;
@@ -769,7 +799,14 @@ async function handleAuthorizationCodeGrant(event, body) {
769
799
  if (!code || !clientId || !redirectUri || !isValidCodeVerifier(verifier)) {
770
800
  return oauthError("invalid_request", "Missing authorization-code fields");
771
801
  }
772
- const row = await getOAuthCode(code);
802
+ let row;
803
+ try {
804
+ await ensureOAuthTables();
805
+ row = await getOAuthCode(code);
806
+ }
807
+ catch {
808
+ return grantUnavailableError(CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE);
809
+ }
773
810
  if (!row)
774
811
  return oauthError("invalid_grant", "Invalid or expired code");
775
812
  if (row.clientId !== clientId || row.redirectUri !== redirectUri) {
@@ -779,21 +816,42 @@ async function handleAuthorizationCodeGrant(event, body) {
779
816
  if (!safeEqual(expectedChallenge, row.codeChallenge)) {
780
817
  return oauthError("invalid_grant", "PKCE verification failed");
781
818
  }
782
- const consumed = await consumeOAuthCode(code);
783
- if (!consumed)
784
- return oauthError("invalid_grant", "Invalid or expired code");
785
819
  const issuer = getMcpOAuthIssuer(event);
786
820
  if (!issuer)
787
821
  return oauthError("server_error", "Unable to derive issuer", 500);
788
- return json(await issueTokenSet({
789
- ownerEmail: row.ownerEmail,
790
- orgId: row.orgId,
791
- orgDomain: row.orgDomain,
792
- clientId,
793
- scope: row.scope,
794
- resource: row.resource,
795
- issuer,
796
- }));
822
+ try {
823
+ return await withMcpCredentialIssuance({
824
+ email: row.ownerEmail,
825
+ orgId: row.orgId,
826
+ requestOrigin: getOrigin(event),
827
+ }, async (tx) => {
828
+ const consumed = await consumeOAuthCode(code, row.ownerEmail, tx);
829
+ if (!consumed)
830
+ return oauthError("invalid_grant", "Invalid or expired code");
831
+ return json(await issueTokenSet({
832
+ ownerEmail: consumed.ownerEmail,
833
+ orgId: consumed.orgId,
834
+ orgDomain: consumed.orgDomain,
835
+ clientId,
836
+ scope: consumed.scope,
837
+ resource: consumed.resource,
838
+ issuer,
839
+ }, tx));
840
+ });
841
+ }
842
+ catch (error) {
843
+ if (error instanceof McpCredentialIssuanceError &&
844
+ error.reason === "not-member") {
845
+ try {
846
+ await consumeOAuthCode(code, row.ownerEmail);
847
+ }
848
+ catch {
849
+ return grantUnavailableError(CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE);
850
+ }
851
+ return oauthError("invalid_grant", NOT_A_MEMBER_DESCRIPTION);
852
+ }
853
+ return grantUnavailableError(CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE);
854
+ }
797
855
  }
798
856
  async function handleRefreshTokenGrant(event, body) {
799
857
  const refreshToken = body.refresh_token;
@@ -804,32 +862,62 @@ async function handleRefreshTokenGrant(event, body) {
804
862
  if (!clientId) {
805
863
  return oauthError("invalid_request", "client_id is required");
806
864
  }
807
- const existing = await getOAuthRefreshToken(refreshToken);
865
+ let existing;
866
+ try {
867
+ await ensureOAuthTables();
868
+ existing = await getOAuthRefreshToken(refreshToken);
869
+ }
870
+ catch {
871
+ return grantUnavailableError(CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE);
872
+ }
808
873
  if (!existing)
809
874
  return oauthError("invalid_grant", "Invalid refresh token");
810
875
  if (existing.clientId !== clientId) {
811
876
  return oauthError("invalid_grant", "Refresh token belongs to another client");
812
877
  }
813
- await touchOAuthRefreshToken(refreshToken).catch(() => undefined);
814
878
  const issuer = getMcpOAuthIssuer(event);
815
879
  if (!issuer)
816
880
  return oauthError("server_error", "Unable to derive issuer", 500);
817
- const accessToken = await signMcpOAuthAccessToken({
818
- ownerEmail: existing.ownerEmail,
819
- orgId: existing.orgId,
820
- orgDomain: existing.orgDomain,
821
- clientId: existing.clientId,
822
- scope: existing.scope,
823
- resource: existing.resource,
824
- issuer,
825
- });
826
- return json({
827
- access_token: accessToken,
828
- token_type: "Bearer",
829
- expires_in: MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
830
- refresh_token: refreshToken,
831
- scope: existing.scope,
832
- });
881
+ try {
882
+ return await withMcpCredentialIssuance({
883
+ email: existing.ownerEmail,
884
+ orgId: existing.orgId,
885
+ requestOrigin: getOrigin(event),
886
+ }, async (tx) => {
887
+ const renewal = await touchOAuthRefreshToken(refreshToken, existing.ownerEmail, tx);
888
+ if (renewal !== "renewed")
889
+ return oauthError("invalid_grant", "Invalid refresh token");
890
+ const accessToken = await signMcpOAuthAccessToken({
891
+ ownerEmail: existing.ownerEmail,
892
+ orgId: existing.orgId,
893
+ orgDomain: existing.orgDomain,
894
+ clientId: existing.clientId,
895
+ scope: existing.scope,
896
+ resource: existing.resource,
897
+ issuer,
898
+ });
899
+ return json({
900
+ access_token: accessToken,
901
+ token_type: "Bearer",
902
+ expires_in: MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS,
903
+ refresh_token: refreshToken,
904
+ scope: existing.scope,
905
+ });
906
+ });
907
+ }
908
+ catch (error) {
909
+ if (error instanceof McpCredentialIssuanceError &&
910
+ error.reason === "not-member") {
911
+ try {
912
+ await revokeOAuthRefreshToken(refreshToken, existing.ownerEmail);
913
+ }
914
+ catch {
915
+ return grantUnavailableError(CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE);
916
+ }
917
+ return oauthError("invalid_grant", NOT_A_MEMBER_DESCRIPTION);
918
+ }
919
+ return grantUnavailableError(CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE);
920
+ }
833
921
  }
834
922
  async function handleToken(event) {
835
923
  if (getMethod(event) !== "POST") {
@@ -5,6 +5,7 @@
5
5
  * short-lived authorization codes, and hashed refresh tokens. Access tokens
6
6
  * are signed JWTs and are never persisted.
7
7
  */
8
+ import { type DbExec } from "../db/client.js";
8
9
  export declare const MCP_OAUTH_CODE_TTL_MS: number;
9
10
  export declare const MCP_OAUTH_ACCESS_TOKEN_TTL: string;
10
11
  export declare const MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS: number;
@@ -12,6 +13,7 @@ export declare const MCP_OAUTH_REFRESH_TOKEN_TTL_MS: number;
12
13
  export declare const MCP_OAUTH_REGISTER_MAX = 60;
13
14
  export declare const MCP_OAUTH_REGISTER_WINDOW_MS = 60000;
14
15
  export declare function ensureTable(): Promise<void>;
16
+ export declare const ensureOAuthTables: typeof ensureTable;
15
17
  export interface OAuthClientRow {
16
18
  clientId: string;
17
19
  clientName: string | null;
@@ -29,6 +31,7 @@ export interface OAuthCodeRow {
29
31
  codeChallenge: string;
30
32
  codeChallengeMethod: string;
31
33
  ownerEmail: string;
34
+ issuedForEmail: string | null;
32
35
  orgId: string | null;
33
36
  orgDomain: string | null;
34
37
  scope: string;
@@ -42,6 +45,7 @@ export interface OAuthRefreshTokenRow {
42
45
  tokenHash: string;
43
46
  clientId: string;
44
47
  ownerEmail: string;
48
+ issuedForEmail: string | null;
45
49
  orgId: string | null;
46
50
  orgDomain: string | null;
47
51
  scope: string;
@@ -73,9 +77,9 @@ export declare function createOAuthCode(params: {
73
77
  orgDomain?: string | null;
74
78
  scope: string;
75
79
  resource: string;
76
- }): Promise<OAuthCodeRow>;
80
+ }, db?: DbExec): Promise<OAuthCodeRow>;
77
81
  export declare function getOAuthCode(code: string): Promise<OAuthCodeRow | null>;
78
- export declare function consumeOAuthCode(code: string): Promise<OAuthCodeRow | null>;
82
+ export declare function consumeOAuthCode(code: string, expectedOwnerEmail?: string, db?: DbExec): Promise<OAuthCodeRow | null>;
79
83
  export declare function createOAuthRefreshToken(params: {
80
84
  refreshToken: string;
81
85
  clientId: string;
@@ -84,15 +88,21 @@ export declare function createOAuthRefreshToken(params: {
84
88
  orgDomain?: string | null;
85
89
  scope: string;
86
90
  resource: string;
87
- }): Promise<OAuthRefreshTokenRow>;
91
+ }, db?: DbExec): Promise<OAuthRefreshTokenRow>;
88
92
  export declare function rotateOAuthRefreshToken(params: {
89
93
  oldRefreshToken: string;
90
94
  newRefreshToken: string;
91
- }): Promise<OAuthRefreshTokenRow | null>;
95
+ }, db?: DbExec): Promise<OAuthRefreshTokenRow | null>;
92
96
  export declare function getOAuthRefreshToken(refreshToken: string): Promise<OAuthRefreshTokenRow | null>;
93
97
  /**
94
98
  * Slide the refresh-token's expiry window on each successful use so that active
95
99
  * users never hit the TTL. Records `last_used_at` and extends `expires_at` to
96
100
  * `now + MCP_OAUTH_REFRESH_TOKEN_TTL_MS`.
101
+ * The caller supplies the owner whose membership it verified.
97
102
  */
98
- export declare function touchOAuthRefreshToken(refreshToken: string): Promise<void>;
103
+ export declare function touchOAuthRefreshToken(refreshToken: string, expectedOwnerEmail: string, db?: DbExec): Promise<"renewed" | "invalid">;
104
+ /**
105
+ * Revoke one refresh token so it can never mint another access token.
106
+ * Idempotent: an already-revoked token keeps its first timestamp.
107
+ */
108
+ export declare function revokeOAuthRefreshToken(refreshToken: string, expectedOwnerEmail?: string): Promise<void>;
@@ -75,6 +75,7 @@ export async function ensureTable() {
75
75
  code_challenge TEXT NOT NULL,
76
76
  code_challenge_method TEXT NOT NULL,
77
77
  owner_email TEXT NOT NULL,
78
+ issued_for_email TEXT,
78
79
  org_id TEXT,
79
80
  org_domain TEXT,
80
81
  scope TEXT NOT NULL,
@@ -90,6 +91,7 @@ export async function ensureTable() {
90
91
  token_hash TEXT UNIQUE NOT NULL,
91
92
  client_id TEXT NOT NULL,
92
93
  owner_email TEXT NOT NULL,
94
+ issued_for_email TEXT,
93
95
  org_id TEXT,
94
96
  org_domain TEXT,
95
97
  scope TEXT NOT NULL,
@@ -104,7 +106,10 @@ export async function ensureTable() {
104
106
  await ensureTableExists("mcp_oauth_clients", createClientsSql);
105
107
  await ensureColumnExists("mcp_oauth_clients", "application_type", `ALTER TABLE mcp_oauth_clients ADD COLUMN IF NOT EXISTS application_type TEXT`);
106
108
  await ensureTableExists("mcp_oauth_codes", createCodesSql);
109
+ // Legacy owners may already have been transferred; leave their bindings unset.
110
+ await ensureColumnExists("mcp_oauth_codes", "issued_for_email", `ALTER TABLE mcp_oauth_codes ADD COLUMN IF NOT EXISTS issued_for_email TEXT`);
107
111
  await ensureTableExists("mcp_oauth_refresh_tokens", createRefreshTokensSql);
112
+ await ensureColumnExists("mcp_oauth_refresh_tokens", "issued_for_email", `ALTER TABLE mcp_oauth_refresh_tokens ADD COLUMN IF NOT EXISTS issued_for_email TEXT`);
108
113
  })().catch((err) => {
109
114
  _initPromise = undefined;
110
115
  throw err;
@@ -112,6 +117,7 @@ export async function ensureTable() {
112
117
  }
113
118
  return _initPromise;
114
119
  }
120
+ export const ensureOAuthTables = ensureTable;
115
121
  export function generateOpaqueToken() {
116
122
  return randomBytes(32).toString("base64url");
117
123
  }
@@ -164,6 +170,7 @@ function mapCodeRow(row) {
164
170
  codeChallenge: row.code_challenge ?? row.codeChallenge,
165
171
  codeChallengeMethod: row.code_challenge_method ?? row.codeChallengeMethod,
166
172
  ownerEmail: row.owner_email ?? row.ownerEmail,
173
+ issuedForEmail: row.issued_for_email ?? row.issuedForEmail ?? null,
167
174
  orgId: row.org_id ?? row.orgId ?? null,
168
175
  orgDomain: row.org_domain ?? row.orgDomain ?? null,
169
176
  scope: row.scope,
@@ -179,6 +186,7 @@ function mapRefreshRow(row) {
179
186
  tokenHash: row.token_hash ?? row.tokenHash,
180
187
  clientId: row.client_id ?? row.clientId,
181
188
  ownerEmail: row.owner_email ?? row.ownerEmail,
189
+ issuedForEmail: row.issued_for_email ?? row.issuedForEmail ?? null,
182
190
  orgId: row.org_id ?? row.orgId ?? null,
183
191
  orgDomain: row.org_domain ?? row.orgDomain ?? null,
184
192
  scope: row.scope,
@@ -190,6 +198,11 @@ function mapRefreshRow(row) {
190
198
  replacedByHash: row.replaced_by_hash ?? row.replacedByHash ?? null,
191
199
  };
192
200
  }
201
+ function hasIssuanceOwner(row) {
202
+ return (typeof row.issuedForEmail === "string" &&
203
+ row.issuedForEmail.trim().length > 0 &&
204
+ row.issuedForEmail === row.ownerEmail);
205
+ }
193
206
  export async function registerOAuthClient(params) {
194
207
  await ensureTable();
195
208
  const client = getDbExec();
@@ -259,14 +272,15 @@ export async function getOAuthClient(clientId) {
259
272
  throw err;
260
273
  }
261
274
  }
262
- export async function createOAuthCode(params) {
263
- await ensureTable();
264
- const client = getDbExec();
275
+ export async function createOAuthCode(params, db) {
276
+ if (!db)
277
+ await ensureTable();
278
+ const client = db ?? getDbExec();
265
279
  const code = generateOpaqueToken();
266
280
  const now = Date.now();
267
281
  const expiresAt = now + MCP_OAUTH_CODE_TTL_MS;
268
- await client.execute({
269
- sql: `INSERT INTO mcp_oauth_codes (code, client_id, redirect_uri, code_challenge, code_challenge_method, owner_email, org_id, org_domain, scope, resource, created_at, expires_at, consumed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
282
+ const result = await client.execute({
283
+ sql: `INSERT INTO mcp_oauth_codes (code, client_id, redirect_uri, code_challenge, code_challenge_method, owner_email, issued_for_email, org_id, org_domain, scope, resource, created_at, expires_at, consumed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
270
284
  args: [
271
285
  code,
272
286
  params.clientId,
@@ -274,6 +288,7 @@ export async function createOAuthCode(params) {
274
288
  params.codeChallenge,
275
289
  params.codeChallengeMethod,
276
290
  params.ownerEmail,
291
+ params.ownerEmail,
277
292
  params.orgId ?? null,
278
293
  params.orgDomain ?? null,
279
294
  params.scope,
@@ -283,6 +298,9 @@ export async function createOAuthCode(params) {
283
298
  null,
284
299
  ],
285
300
  });
301
+ if (result.rowsAffected !== 1) {
302
+ throw new Error("Authorization-code creation returned an invalid row count");
303
+ }
286
304
  return {
287
305
  code,
288
306
  clientId: params.clientId,
@@ -290,6 +308,7 @@ export async function createOAuthCode(params) {
290
308
  codeChallenge: params.codeChallenge,
291
309
  codeChallengeMethod: params.codeChallengeMethod,
292
310
  ownerEmail: params.ownerEmail,
311
+ issuedForEmail: params.ownerEmail,
293
312
  orgId: params.orgId ?? null,
294
313
  orgDomain: params.orgDomain ?? null,
295
314
  scope: params.scope,
@@ -309,14 +328,17 @@ export async function getOAuthCode(code) {
309
328
  if (rows.length === 0)
310
329
  return null;
311
330
  const row = mapCodeRow(rows[0]);
312
- if (row.consumedAt != null || (row.expiresAt ?? 0) < Date.now()) {
331
+ if (!hasIssuanceOwner(row) ||
332
+ row.consumedAt != null ||
333
+ (row.expiresAt ?? 0) < Date.now()) {
313
334
  return null;
314
335
  }
315
336
  return row;
316
337
  }
317
- export async function consumeOAuthCode(code) {
318
- await ensureTable();
319
- const client = getDbExec();
338
+ export async function consumeOAuthCode(code, expectedOwnerEmail, db) {
339
+ if (!db)
340
+ await ensureTable();
341
+ const client = db ?? getDbExec();
320
342
  const { rows } = await client.execute({
321
343
  sql: `SELECT * FROM mcp_oauth_codes WHERE code = ?`,
322
344
  args: [code],
@@ -324,24 +346,35 @@ export async function consumeOAuthCode(code) {
324
346
  if (rows.length === 0)
325
347
  return null;
326
348
  const row = mapCodeRow(rows[0]);
327
- if (row.consumedAt != null || (row.expiresAt ?? 0) < Date.now()) {
349
+ const now = Date.now();
350
+ if (!hasIssuanceOwner(row) ||
351
+ (expectedOwnerEmail !== undefined &&
352
+ row.ownerEmail !== expectedOwnerEmail) ||
353
+ row.consumedAt != null ||
354
+ (row.expiresAt ?? 0) < now) {
328
355
  return null;
329
356
  }
330
357
  const result = await client.execute({
331
- sql: `UPDATE mcp_oauth_codes SET consumed_at = ? WHERE code = ? AND consumed_at IS NULL`,
332
- args: [Date.now(), code],
358
+ sql: `UPDATE mcp_oauth_codes SET consumed_at = ? WHERE code = ? AND consumed_at IS NULL AND expires_at >= ? AND owner_email = ? AND issued_for_email = ?`,
359
+ args: [now, code, now, row.ownerEmail, row.issuedForEmail],
333
360
  });
334
- return result.rowsAffected > 0 ? row : null;
361
+ if (result.rowsAffected === 0)
362
+ return null;
363
+ if (result.rowsAffected === 1)
364
+ return row;
365
+ throw new Error("Authorization-code consumption returned an invalid row count");
335
366
  }
336
- export async function createOAuthRefreshToken(params) {
337
- await ensureTable();
338
- const client = getDbExec();
367
+ export async function createOAuthRefreshToken(params, db) {
368
+ if (!db)
369
+ await ensureTable();
370
+ const client = db ?? getDbExec();
339
371
  const now = Date.now();
340
372
  const row = {
341
373
  id: randomUUID(),
342
374
  tokenHash: hashOAuthToken(params.refreshToken),
343
375
  clientId: params.clientId,
344
376
  ownerEmail: params.ownerEmail,
377
+ issuedForEmail: params.ownerEmail,
345
378
  orgId: params.orgId ?? null,
346
379
  orgDomain: params.orgDomain ?? null,
347
380
  scope: params.scope,
@@ -352,13 +385,14 @@ export async function createOAuthRefreshToken(params) {
352
385
  revokedAt: null,
353
386
  replacedByHash: null,
354
387
  };
355
- await client.execute({
356
- sql: `INSERT INTO mcp_oauth_refresh_tokens (id, token_hash, client_id, owner_email, org_id, org_domain, scope, resource, created_at, expires_at, last_used_at, revoked_at, replaced_by_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
388
+ const result = await client.execute({
389
+ sql: `INSERT INTO mcp_oauth_refresh_tokens (id, token_hash, client_id, owner_email, issued_for_email, org_id, org_domain, scope, resource, created_at, expires_at, last_used_at, revoked_at, replaced_by_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
357
390
  args: [
358
391
  row.id,
359
392
  row.tokenHash,
360
393
  row.clientId,
361
394
  row.ownerEmail,
395
+ row.issuedForEmail,
362
396
  row.orgId,
363
397
  row.orgDomain,
364
398
  row.scope,
@@ -370,11 +404,15 @@ export async function createOAuthRefreshToken(params) {
370
404
  row.replacedByHash,
371
405
  ],
372
406
  });
407
+ if (result.rowsAffected !== 1) {
408
+ throw new Error("Refresh-token creation returned an invalid row count");
409
+ }
373
410
  return row;
374
411
  }
375
- export async function rotateOAuthRefreshToken(params) {
376
- await ensureTable();
377
- const client = getDbExec();
412
+ export async function rotateOAuthRefreshToken(params, db) {
413
+ if (!db)
414
+ await ensureTable();
415
+ const client = db ?? getDbExec();
378
416
  const oldHash = hashOAuthToken(params.oldRefreshToken);
379
417
  const newHash = hashOAuthToken(params.newRefreshToken);
380
418
  const { rows } = await client.execute({
@@ -384,15 +422,20 @@ export async function rotateOAuthRefreshToken(params) {
384
422
  if (rows.length === 0)
385
423
  return null;
386
424
  const old = mapRefreshRow(rows[0]);
387
- if (old.revokedAt != null || (old.expiresAt ?? 0) < Date.now())
388
- return null;
389
425
  const now = Date.now();
426
+ if (!hasIssuanceOwner(old) ||
427
+ old.revokedAt != null ||
428
+ (old.expiresAt ?? 0) < now)
429
+ return null;
390
430
  const update = await client.execute({
391
- sql: `UPDATE mcp_oauth_refresh_tokens SET revoked_at = ?, last_used_at = ?, replaced_by_hash = ? WHERE token_hash = ? AND revoked_at IS NULL`,
392
- args: [now, now, newHash, oldHash],
431
+ sql: `UPDATE mcp_oauth_refresh_tokens SET revoked_at = ?, last_used_at = ?, replaced_by_hash = ? WHERE token_hash = ? AND revoked_at IS NULL AND expires_at >= ? AND owner_email = ? AND issued_for_email = ?`,
432
+ args: [now, now, newHash, oldHash, now, old.ownerEmail, old.issuedForEmail],
393
433
  });
394
434
  if (update.rowsAffected === 0)
395
435
  return null;
436
+ if (update.rowsAffected !== 1) {
437
+ throw new Error("Refresh-token rotation returned an invalid row count");
438
+ }
396
439
  const next = {
397
440
  ...old,
398
441
  id: randomUUID(),
@@ -403,13 +446,14 @@ export async function rotateOAuthRefreshToken(params) {
403
446
  revokedAt: null,
404
447
  replacedByHash: null,
405
448
  };
406
- await client.execute({
407
- sql: `INSERT INTO mcp_oauth_refresh_tokens (id, token_hash, client_id, owner_email, org_id, org_domain, scope, resource, created_at, expires_at, last_used_at, revoked_at, replaced_by_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
449
+ const insert = await client.execute({
450
+ sql: `INSERT INTO mcp_oauth_refresh_tokens (id, token_hash, client_id, owner_email, issued_for_email, org_id, org_domain, scope, resource, created_at, expires_at, last_used_at, revoked_at, replaced_by_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
408
451
  args: [
409
452
  next.id,
410
453
  next.tokenHash,
411
454
  next.clientId,
412
455
  next.ownerEmail,
456
+ next.issuedForEmail,
413
457
  next.orgId,
414
458
  next.orgDomain,
415
459
  next.scope,
@@ -421,6 +465,9 @@ export async function rotateOAuthRefreshToken(params) {
421
465
  next.replacedByHash,
422
466
  ],
423
467
  });
468
+ if (insert.rowsAffected !== 1) {
469
+ throw new Error("Refresh-token rotation returned an invalid row count");
470
+ }
424
471
  return next;
425
472
  }
426
473
  export async function getOAuthRefreshToken(refreshToken) {
@@ -434,7 +481,9 @@ export async function getOAuthRefreshToken(refreshToken) {
434
481
  if (rows.length === 0)
435
482
  return null;
436
483
  const row = mapRefreshRow(rows[0]);
437
- if (row.revokedAt != null || (row.expiresAt ?? 0) < Date.now()) {
484
+ if (!hasIssuanceOwner(row) ||
485
+ row.revokedAt != null ||
486
+ (row.expiresAt ?? 0) < Date.now()) {
438
487
  return null;
439
488
  }
440
489
  return row;
@@ -443,14 +492,50 @@ export async function getOAuthRefreshToken(refreshToken) {
443
492
  * Slide the refresh-token's expiry window on each successful use so that active
444
493
  * users never hit the TTL. Records `last_used_at` and extends `expires_at` to
445
494
  * `now + MCP_OAUTH_REFRESH_TOKEN_TTL_MS`.
495
+ * The caller supplies the owner whose membership it verified.
446
496
  */
447
- export async function touchOAuthRefreshToken(refreshToken) {
448
- await ensureTable();
449
- const client = getDbExec();
497
+ export async function touchOAuthRefreshToken(refreshToken, expectedOwnerEmail, db) {
498
+ if (!db)
499
+ await ensureTable();
500
+ const client = db ?? getDbExec();
450
501
  const tokenHash = hashOAuthToken(refreshToken);
451
502
  const now = Date.now();
452
- await client.execute({
453
- sql: `UPDATE mcp_oauth_refresh_tokens SET last_used_at = ?, expires_at = ? WHERE token_hash = ? AND revoked_at IS NULL`,
454
- args: [now, now + MCP_OAUTH_REFRESH_TOKEN_TTL_MS, tokenHash],
503
+ const result = await client.execute({
504
+ sql: `UPDATE mcp_oauth_refresh_tokens SET last_used_at = ?, expires_at = ? WHERE token_hash = ? AND revoked_at IS NULL AND expires_at >= ? AND owner_email = ? AND issued_for_email = ? AND BTRIM(issued_for_email) <> ''`,
505
+ args: [
506
+ now,
507
+ now + MCP_OAUTH_REFRESH_TOKEN_TTL_MS,
508
+ tokenHash,
509
+ now,
510
+ expectedOwnerEmail,
511
+ expectedOwnerEmail,
512
+ ],
513
+ });
514
+ if (result.rowsAffected === 0)
515
+ return "invalid";
516
+ if (result.rowsAffected === 1)
517
+ return "renewed";
518
+ throw new Error("Refresh-token renewal returned an invalid row count");
519
+ }
520
+ /**
521
+ * Revoke one refresh token so it can never mint another access token.
522
+ * Idempotent: an already-revoked token keeps its first timestamp.
523
+ */
524
+ export async function revokeOAuthRefreshToken(refreshToken, expectedOwnerEmail) {
525
+ await ensureTable();
526
+ const client = getDbExec();
527
+ const result = await client.execute({
528
+ sql: `UPDATE mcp_oauth_refresh_tokens SET revoked_at = ? WHERE token_hash = ? AND revoked_at IS NULL${expectedOwnerEmail !== undefined
529
+ ? " AND owner_email = ? AND issued_for_email = ?"
530
+ : ""}`,
531
+ args: [
532
+ Date.now(),
533
+ hashOAuthToken(refreshToken),
534
+ ...(expectedOwnerEmail !== undefined
535
+ ? [expectedOwnerEmail, expectedOwnerEmail]
536
+ : []),
537
+ ],
455
538
  });
539
+ if (result.rowsAffected !== 0 && result.rowsAffected !== 1)
540
+ throw new Error("Refresh-token revocation returned an invalid row count");
456
541
  }
@@ -2,6 +2,7 @@ import { MCP_OAUTH_ACCESS_TOKEN_TTL, MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS } from "
2
2
  export { MCP_OAUTH_ACCESS_TOKEN_TTL, MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS };
3
3
  export declare const MCP_OAUTH_SCOPES: readonly ["mcp:read", "mcp:write", "mcp:apps", "offline_access"];
4
4
  export declare const MCP_OAUTH_DEFAULT_SCOPE: string;
5
+ declare const MCP_OAUTH_CREDENTIAL_VERSION = 2;
5
6
  export interface McpOAuthAccessTokenClaims {
6
7
  sub: string;
7
8
  org_id?: string | null;
@@ -11,6 +12,7 @@ export interface McpOAuthAccessTokenClaims {
11
12
  resource: string;
12
13
  jti?: string;
13
14
  typ: "agent-native-mcp-oauth";
15
+ credential_version: typeof MCP_OAUTH_CREDENTIAL_VERSION;
14
16
  }
15
17
  export declare function normalizeOAuthScope(input: unknown): string | null;
16
18
  export declare function scopeList(scope: string | undefined): string[];
@@ -38,4 +40,6 @@ export declare function verifyMcpOAuthAccessToken(token: string, resource: strin
38
40
  clientId: string;
39
41
  jti?: string;
40
42
  catalogScope?: "full";
43
+ /** `iat`, in seconds. */
44
+ issuedAt?: number;
41
45
  } | null>;