@kya-os/mcp-i-cloudflare 1.14.7 → 1.15.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.
- package/README.md +8 -8
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +8 -4
- package/dist/adapter.js.map +1 -1
- package/dist/agent.d.ts +22 -5
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +88 -46
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +15 -8
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts +101 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -12
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +5 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +6 -4
- package/dist/constants.js.map +1 -1
- package/dist/delegation-http/anchor-registration.d.ts +2 -2
- package/dist/delegation-http/anchor-registration.js +4 -4
- package/dist/delegation-http/anchor-registration.js.map +1 -1
- package/dist/delegation-http/consent-anchor-bridge.js +3 -3
- package/dist/delegation-http/consent-anchor-bridge.js.map +1 -1
- package/dist/delegation-http/credential-consent.d.ts +2 -2
- package/dist/delegation-http/credential-consent.js +14 -14
- package/dist/delegation-http/credential-consent.js.map +1 -1
- package/dist/delegation-http/native-http.js +5 -5
- package/dist/delegation-http/native-http.js.map +1 -1
- package/dist/delegation-http/native-oauth-state-do.d.ts +43 -0
- package/dist/delegation-http/native-oauth-state-do.d.ts.map +1 -0
- package/dist/delegation-http/native-oauth-state-do.js +175 -0
- package/dist/delegation-http/native-oauth-state-do.js.map +1 -0
- package/dist/delegation-http/native-oauth-state.d.ts +12 -0
- package/dist/delegation-http/native-oauth-state.d.ts.map +1 -0
- package/dist/delegation-http/native-oauth-state.js +131 -0
- package/dist/delegation-http/native-oauth-state.js.map +1 -0
- package/dist/delegation-http/native-oauth.d.ts.map +1 -1
- package/dist/delegation-http/native-oauth.js +22 -17
- package/dist/delegation-http/native-oauth.js.map +1 -1
- package/dist/delegation-http/register.js +1 -1
- package/dist/delegation-http/register.js.map +1 -1
- package/dist/delegation-http/registrar-auth.d.ts +1 -1
- package/dist/delegation-http/registrar-auth.js +2 -2
- package/dist/delegation-http/registrar-auth.js.map +1 -1
- package/dist/delegation-http/remote-issuer.d.ts +1 -1
- package/dist/delegation-http/remote-issuer.d.ts.map +1 -1
- package/dist/delegation-http/remote-issuer.js +4 -4
- package/dist/delegation-http/remote-issuer.js.map +1 -1
- package/dist/helpers/env-mapper.d.ts +2 -2
- package/dist/helpers/env-mapper.d.ts.map +1 -1
- package/dist/helpers/env-mapper.js +21 -14
- package/dist/helpers/env-mapper.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/needs-authorization-meta.d.ts +5 -0
- package/dist/needs-authorization-meta.d.ts.map +1 -1
- package/dist/needs-authorization-meta.js +9 -0
- package/dist/needs-authorization-meta.js.map +1 -1
- package/dist/proof-generator.d.ts +3 -3
- package/dist/proof-generator.js +4 -4
- package/dist/proof-generator.js.map +1 -1
- package/dist/runtime/invoke-tool-handler.d.ts +26 -1
- package/dist/runtime/invoke-tool-handler.d.ts.map +1 -1
- package/dist/runtime/invoke-tool-handler.js +32 -0
- package/dist/runtime/invoke-tool-handler.js.map +1 -1
- package/dist/runtime/oauth-handler.d.ts +9 -7
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +53 -49
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/runtime/oidc/authorize-url.js +2 -2
- package/dist/runtime/oidc/authorize-url.js.map +1 -1
- package/dist/runtime/oidc/token-exchange.d.ts +1 -1
- package/dist/runtime/oidc/token-exchange.d.ts.map +1 -1
- package/dist/runtime/tool-activity.d.ts +56 -0
- package/dist/runtime/tool-activity.d.ts.map +1 -0
- package/dist/runtime/tool-activity.js +55 -0
- package/dist/runtime/tool-activity.js.map +1 -0
- package/dist/runtime.d.ts +30 -13
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +71 -18
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +17 -15
- package/dist/server.js.map +1 -1
- package/dist/services/admin.service.d.ts +1 -1
- package/dist/services/admin.service.js +13 -13
- package/dist/services/admin.service.js.map +1 -1
- package/dist/services/consent-config.service.d.ts +5 -5
- package/dist/services/consent-config.service.js +12 -12
- package/dist/services/consent-config.service.js.map +1 -1
- package/dist/services/consent-templates/base/components.d.ts +1 -1
- package/dist/services/consent-templates/base/components.js +1 -1
- package/dist/services/consent-templates/base/styles.d.ts +2 -2
- package/dist/services/consent-templates/base/styles.js +4 -4
- package/dist/services/consent-templates/base/styles.js.map +1 -1
- package/dist/services/consent-templates/helpers.d.ts +3 -3
- package/dist/services/consent-templates/helpers.js +4 -4
- package/dist/services/consent-templates/helpers.js.map +1 -1
- package/dist/services/consent-templates/index.d.ts +1 -1
- package/dist/services/consent-templates/index.js +1 -1
- package/dist/services/consent-templates/modes/credentials.template.d.ts +5 -5
- package/dist/services/consent-templates/modes/credentials.template.js +9 -9
- package/dist/services/consent-templates/modes/credentials.template.js.map +1 -1
- package/dist/services/consent-templates/registry.d.ts +3 -3
- package/dist/services/consent-templates/registry.js +3 -3
- package/dist/services/consent-templates/template-renderer.d.ts +5 -3
- package/dist/services/consent-templates/template-renderer.d.ts.map +1 -1
- package/dist/services/consent-templates/template-renderer.js +5 -3
- package/dist/services/consent-templates/template-renderer.js.map +1 -1
- package/dist/services/consent-templates/types.d.ts +4 -4
- package/dist/services/consent-templates/types.js +2 -2
- package/dist/services/consent.service.d.ts +44 -24
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +197 -119
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/delegation-mandate.d.ts +40 -0
- package/dist/services/delegation-mandate.d.ts.map +1 -0
- package/dist/services/delegation-mandate.js +46 -0
- package/dist/services/delegation-mandate.js.map +1 -0
- package/dist/services/delegation.service.d.ts +28 -12
- package/dist/services/delegation.service.d.ts.map +1 -1
- package/dist/services/delegation.service.js +88 -99
- package/dist/services/delegation.service.js.map +1 -1
- package/dist/services/kya-os-events.service.d.ts +15 -3
- package/dist/services/kya-os-events.service.d.ts.map +1 -1
- package/dist/services/kya-os-events.service.js +14 -4
- package/dist/services/kya-os-events.service.js.map +1 -1
- package/dist/services/proof-batch-queue.d.ts +4 -4
- package/dist/services/proof-batch-queue.d.ts.map +1 -1
- package/dist/services/proof-batch-queue.js +10 -10
- package/dist/services/proof-batch-queue.js.map +1 -1
- package/dist/services/proof.service.d.ts +64 -5
- package/dist/services/proof.service.d.ts.map +1 -1
- package/dist/services/proof.service.js +127 -40
- package/dist/services/proof.service.js.map +1 -1
- package/dist/services/provider-loader.service.d.ts +6 -6
- package/dist/services/provider-loader.service.d.ts.map +1 -1
- package/dist/services/provider-loader.service.js +4 -4
- package/dist/services/provider-loader.service.js.map +1 -1
- package/dist/services/vault-resolver.d.ts +4 -4
- package/dist/services/vault-resolver.d.ts.map +1 -1
- package/dist/services/vault-resolver.js +2 -2
- package/dist/types.d.ts +6 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/day0-config.d.ts +1 -1
- package/dist/utils/day0-config.js +1 -1
- package/dist/utils/env-secret-resolver.d.ts +1 -1
- package/dist/utils/env-secret-resolver.js +1 -1
- package/package.json +5 -5
|
@@ -8,19 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ConsentConfigService } from "./consent-config.service";
|
|
10
10
|
import { TemplateRenderer } from "./consent-templates/template-renderer";
|
|
11
|
-
import {
|
|
11
|
+
import { DEFAULT_CHECKPOINT_URL, DEFAULT_SESSION_CACHE_TTL, KEY_PAIR_TTL_SECONDS, CONSENT_PROVIDER_TYPES, } from "../constants";
|
|
12
12
|
import { STORAGE_KEYS, isPendingDelegationId } from "../constants/storage-keys";
|
|
13
13
|
import { loadDay0Config, getDelegationFieldName } from "../utils/day0-config";
|
|
14
14
|
import { buildProviderAuthorizeUrl } from "../runtime/oidc/authorize-url.js";
|
|
15
15
|
import { resolveIdpScopes } from "../runtime/oidc/idp-scopes.js";
|
|
16
16
|
import { validateConsentApprovalRequest, } from "@kya-os/contracts/consent";
|
|
17
|
-
import {
|
|
17
|
+
import { CHECKPOINT_ENDPOINTS, createDelegationAPIResponseSchema, createDelegationResponseSchema, } from "@kya-os/contracts/checkpoint-api";
|
|
18
18
|
import { createUnsignedVCJWT, completeVCJWT, parseVCJWT, generateDidKeyFromBase64, createDelegationVerifier, createDidKeyResolver, logger, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, wrapDelegationAsVC, } from "@kya-os/mcp";
|
|
19
19
|
import { fetchRemoteConfig, normalizeAuthType, } from "@kya-os/mcp-i-runtime"; // product-layer symbols stay on OLD (C2 #2918)
|
|
20
20
|
import { WebCryptoProvider } from "../providers/crypto";
|
|
21
21
|
import { ConsentAuditService } from "./consent-audit.service";
|
|
22
22
|
import { CloudflareProofGenerator } from "../proof-generator";
|
|
23
23
|
import { ProofService } from "./proof.service";
|
|
24
|
+
import { withMandateConstraints } from "./delegation-mandate";
|
|
24
25
|
import { createCredentialAuthHandler } from "./credential-auth.handler";
|
|
25
26
|
import { IdpTokenStorage } from "./idp-token-storage";
|
|
26
27
|
import { OAuthSecurityService } from "./oauth-security.service";
|
|
@@ -54,6 +55,10 @@ function friendlyAgentNameFromDid(did) {
|
|
|
54
55
|
.replace(/[-_]+/g, " ")
|
|
55
56
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
56
57
|
}
|
|
58
|
+
const MANDATE_ERROR = {
|
|
59
|
+
error: "Delegation limits could not be resolved",
|
|
60
|
+
error_code: "mandate_error",
|
|
61
|
+
};
|
|
57
62
|
export class ConsentService {
|
|
58
63
|
configService;
|
|
59
64
|
renderer;
|
|
@@ -66,19 +71,56 @@ export class ConsentService {
|
|
|
66
71
|
// Phase 2: Provider resolution services (optional for backward compatibility)
|
|
67
72
|
providerResolver;
|
|
68
73
|
providerRegistry;
|
|
74
|
+
delegationMandate;
|
|
69
75
|
/**
|
|
70
76
|
* ✅ FIXED: Constructor takes env: CloudflareEnv, not config
|
|
71
77
|
* Phase 2: Optional providerResolver and providerRegistry for tool-specific providers
|
|
72
78
|
*/
|
|
73
|
-
constructor(env, runtime, providerResolver, providerRegistry) {
|
|
79
|
+
constructor(env, runtime, providerResolver, providerRegistry, options = {}) {
|
|
74
80
|
this.env = env;
|
|
75
81
|
this.runtime = runtime;
|
|
76
82
|
this.configService = new ConsentConfigService(env);
|
|
77
83
|
this.renderer = new TemplateRenderer({ useCSR: true });
|
|
78
84
|
this.providerResolver = providerResolver;
|
|
79
85
|
this.providerRegistry = providerRegistry;
|
|
86
|
+
this.delegationMandate = options.delegationMandate;
|
|
80
87
|
// No initialization here - keep constructor synchronous
|
|
81
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Ask the server's mandate provider about these scopes.
|
|
91
|
+
*
|
|
92
|
+
* Keyed on scopes (what the grant authorizes), never on a tool name, so a
|
|
93
|
+
* request naming a different tool with the same scopes gets the same
|
|
94
|
+
* limits. The provider gets the runtime identity DID, so a mandate can name
|
|
95
|
+
* this server as the merchant without trusting any request field.
|
|
96
|
+
*
|
|
97
|
+
* Without a provider this always returns `{ ok: true, mandate: undefined }`.
|
|
98
|
+
* With one, a missing identity or a throw returns `{ ok: false }`.
|
|
99
|
+
*/
|
|
100
|
+
async resolveMandate(scopes, projectId) {
|
|
101
|
+
const provider = this.delegationMandate;
|
|
102
|
+
if (!provider)
|
|
103
|
+
return { ok: true, mandate: undefined };
|
|
104
|
+
try {
|
|
105
|
+
const serverDid = (await this.runtime?.getIdentity())?.did;
|
|
106
|
+
if (!serverDid) {
|
|
107
|
+
logger.error("[ConsentService] Delegation mandate configured but the server identity is unavailable");
|
|
108
|
+
return { ok: false };
|
|
109
|
+
}
|
|
110
|
+
const mandate = provider({ scopes: [...scopes], serverDid, projectId });
|
|
111
|
+
return { ok: true, mandate };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger.error("[ConsentService] Delegation mandate provider failed:", error);
|
|
115
|
+
return { ok: false };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
mandateErrorResponse() {
|
|
119
|
+
return new Response(JSON.stringify({ success: false, ...MANDATE_ERROR }), {
|
|
120
|
+
status: 500,
|
|
121
|
+
headers: { "Content-Type": "application/json" },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
82
124
|
/**
|
|
83
125
|
* Get or initialize audit service (lazy initialization)
|
|
84
126
|
*
|
|
@@ -182,7 +224,7 @@ export class ConsentService {
|
|
|
182
224
|
}
|
|
183
225
|
}
|
|
184
226
|
/**
|
|
185
|
-
* Fetch CloudflareRuntimeConfig from remote API (
|
|
227
|
+
* Fetch CloudflareRuntimeConfig from remote API (Checkpoint)
|
|
186
228
|
*
|
|
187
229
|
* ⚠️ CRITICAL: Config MUST be fetched from remote API, not constructed from env.
|
|
188
230
|
*
|
|
@@ -193,7 +235,7 @@ export class ConsentService {
|
|
|
193
235
|
* @returns Runtime config or undefined if unavailable
|
|
194
236
|
*/
|
|
195
237
|
async getConfigFromRemoteAPI(projectId) {
|
|
196
|
-
if (!this.env.
|
|
238
|
+
if (!this.env.CHECKPOINT_API_KEY) {
|
|
197
239
|
logger.warn("[ConsentService] No API key for runtime config fetch");
|
|
198
240
|
return undefined;
|
|
199
241
|
}
|
|
@@ -212,15 +254,15 @@ export class ConsentService {
|
|
|
212
254
|
}
|
|
213
255
|
: undefined;
|
|
214
256
|
const config = await fetchRemoteConfig({
|
|
215
|
-
apiUrl: this.env.
|
|
216
|
-
apiKey: this.env.
|
|
257
|
+
apiUrl: this.env.CHECKPOINT_API_URL || "https://kya.vouched.id",
|
|
258
|
+
apiKey: this.env.CHECKPOINT_API_KEY,
|
|
217
259
|
projectId, // ✅ Use projectId from consent request
|
|
218
260
|
cacheTtl: 300000, // 5 minutes
|
|
219
261
|
fetchProvider: fetch,
|
|
220
262
|
}, cache);
|
|
221
263
|
// Populate serverDid from runtime identity if missing or empty
|
|
222
|
-
// This prevents
|
|
223
|
-
// Note: MCPIConfig.identity is RuntimeIdentityConfig (no serverDid), but
|
|
264
|
+
// This prevents Checkpoint validation errors (serverDid must be at least 1 character)
|
|
265
|
+
// Note: MCPIConfig.identity is RuntimeIdentityConfig (no serverDid), but Checkpoint
|
|
224
266
|
// may return MCPIServerConfig format (has serverDid). We need to handle both.
|
|
225
267
|
if (config && config.identity) {
|
|
226
268
|
const identityConfig = config.identity; // Type assertion for serverDid field
|
|
@@ -374,11 +416,11 @@ export class ConsentService {
|
|
|
374
416
|
/**
|
|
375
417
|
* Update session with resolved identity (Phase 5)
|
|
376
418
|
*
|
|
377
|
-
* Called after
|
|
419
|
+
* Called after Checkpoint identity/resolve returns a persistent user DID.
|
|
378
420
|
* Stores the DID in session storage and creates OAuth mapping.
|
|
379
421
|
*
|
|
380
422
|
* @param sessionId - Session ID
|
|
381
|
-
* @param userDid - Persistent user DID from
|
|
423
|
+
* @param userDid - Persistent user DID from Checkpoint
|
|
382
424
|
* @param oauthIdentity - OAuth identity for creating persistent mapping
|
|
383
425
|
*/
|
|
384
426
|
async updateSessionWithIdentity(sessionId, userDid, oauthIdentity) {
|
|
@@ -537,8 +579,8 @@ export class ConsentService {
|
|
|
537
579
|
* GENERATES a new key pair if one doesn't exist.
|
|
538
580
|
*
|
|
539
581
|
* NOTE: The generated key pair has its own did:key which may differ
|
|
540
|
-
* from
|
|
541
|
-
* OAuth identity. See docs/proposals/VC/
|
|
582
|
+
* from Checkpoint's persistentUserDid. Both are tied to the same
|
|
583
|
+
* OAuth identity. See docs/proposals/VC/CHECKPOINT_SIGNING_ENDPOINT.md
|
|
542
584
|
* for the future cleaner architecture.
|
|
543
585
|
*
|
|
544
586
|
* @param sessionId - Session ID for caching
|
|
@@ -889,7 +931,7 @@ export class ConsentService {
|
|
|
889
931
|
*
|
|
890
932
|
* Determines if OAuth flow should be used instead of direct delegation creation.
|
|
891
933
|
* OAuth is required if:
|
|
892
|
-
* - Project has OAuth provider configured in
|
|
934
|
+
* - Project has OAuth provider configured in Checkpoint
|
|
893
935
|
* - No OAuth identity is present in the request
|
|
894
936
|
*
|
|
895
937
|
* @param projectId - Project ID to check
|
|
@@ -960,14 +1002,14 @@ export class ConsentService {
|
|
|
960
1002
|
}
|
|
961
1003
|
// Check project config to see if OAuth is configured
|
|
962
1004
|
try {
|
|
963
|
-
const
|
|
964
|
-
const apiKey = this.env.
|
|
1005
|
+
const checkpointUrl = this.env.CHECKPOINT_API_URL || DEFAULT_CHECKPOINT_URL;
|
|
1006
|
+
const apiKey = this.env.CHECKPOINT_API_KEY;
|
|
965
1007
|
if (!apiKey) {
|
|
966
1008
|
// No API key - can't check, assume OAuth not required
|
|
967
1009
|
return false;
|
|
968
1010
|
}
|
|
969
1011
|
// Fetch project config to check for OAuth provider
|
|
970
|
-
const configUrl = `${
|
|
1012
|
+
const configUrl = `${checkpointUrl}/api/v1/bouncer/projects/${projectId}/config`;
|
|
971
1013
|
const response = await fetch(configUrl, {
|
|
972
1014
|
headers: {
|
|
973
1015
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -1007,15 +1049,15 @@ export class ConsentService {
|
|
|
1007
1049
|
* @param serverUrl - Server URL for callback
|
|
1008
1050
|
* @param provider - Optional provider name (resolved if not provided)
|
|
1009
1051
|
* @param oauthSecurityService - Optional OAuthSecurityService for CSRF protection
|
|
1010
|
-
* @param agentName - Optional human-readable agent name for
|
|
1052
|
+
* @param agentName - Optional human-readable agent name for Checkpoint dashboard display
|
|
1011
1053
|
* @param tool - Optional tool name to preserve through OAuth flow for clickwrap redirect
|
|
1012
1054
|
* @returns OAuth authorization URL
|
|
1013
1055
|
*/
|
|
1014
1056
|
async buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl, provider, // Phase 2: Optional provider name (resolved if not provided)
|
|
1015
|
-
oauthSecurityService, agentName, // Human-readable agent name for
|
|
1057
|
+
oauthSecurityService, agentName, // Human-readable agent name for Checkpoint dashboard
|
|
1016
1058
|
tool // Tool name to preserve through OAuth callback for clickwrap redirect
|
|
1017
1059
|
) {
|
|
1018
|
-
const
|
|
1060
|
+
const checkpointUrl = this.env.CHECKPOINT_API_URL || DEFAULT_CHECKPOINT_URL;
|
|
1019
1061
|
// Generate a temporary delegation ID for state (will be created after OAuth)
|
|
1020
1062
|
const delegationId = `temp-${Date.now()}`;
|
|
1021
1063
|
// ✅ Phase 1: Generate PKCE challenge if OAuthSecurityService is available
|
|
@@ -1086,17 +1128,17 @@ export class ConsentService {
|
|
|
1086
1128
|
stateParam = btoa(JSON.stringify(stateData));
|
|
1087
1129
|
}
|
|
1088
1130
|
// Phase 3: Resolve provider config and extract custom parameters
|
|
1089
|
-
// Load providers from
|
|
1131
|
+
// Load providers from Checkpoint API if not already loaded
|
|
1090
1132
|
let providerConfig = null;
|
|
1091
1133
|
if (provider && this.providerRegistry) {
|
|
1092
1134
|
// Lazy load providers if registry is empty (first request for this project)
|
|
1093
1135
|
if (this.providerRegistry.getProviderNames().length === 0) {
|
|
1094
|
-
logger.debug("[ConsentService] Loading providers from
|
|
1136
|
+
logger.debug("[ConsentService] Loading providers from Checkpoint", {
|
|
1095
1137
|
projectId,
|
|
1096
1138
|
provider,
|
|
1097
1139
|
});
|
|
1098
1140
|
try {
|
|
1099
|
-
await this.providerRegistry.
|
|
1141
|
+
await this.providerRegistry.loadFromCheckpoint(projectId);
|
|
1100
1142
|
logger.debug("[ConsentService] Providers loaded successfully", {
|
|
1101
1143
|
projectId,
|
|
1102
1144
|
providers: this.providerRegistry.getProviderNames(),
|
|
@@ -1104,7 +1146,7 @@ export class ConsentService {
|
|
|
1104
1146
|
});
|
|
1105
1147
|
}
|
|
1106
1148
|
catch (loadError) {
|
|
1107
|
-
logger.warn("[ConsentService] Failed to load providers from
|
|
1149
|
+
logger.warn("[ConsentService] Failed to load providers from Checkpoint", {
|
|
1108
1150
|
projectId,
|
|
1109
1151
|
error: loadError instanceof Error
|
|
1110
1152
|
? loadError.message
|
|
@@ -1136,7 +1178,7 @@ export class ConsentService {
|
|
|
1136
1178
|
: "using direct PKCE mode",
|
|
1137
1179
|
});
|
|
1138
1180
|
// Direct OAuth Mode: Use provider's authorization URL directly for PKCE-capable providers
|
|
1139
|
-
// This bypasses
|
|
1181
|
+
// This bypasses Checkpoint bouncer and redirects straight to GitHub/Google/etc.
|
|
1140
1182
|
//
|
|
1141
1183
|
// Flow: User → GitHub OAuth → MCP-I /oauth/callback → Success page
|
|
1142
1184
|
//
|
|
@@ -1145,7 +1187,7 @@ export class ConsentService {
|
|
|
1145
1187
|
// - proxyMode: false = no client secret needed, direct exchange with provider
|
|
1146
1188
|
//
|
|
1147
1189
|
// When proxyMode is true (e.g., GitHub OAuth Apps with client_secret), we must
|
|
1148
|
-
// use
|
|
1190
|
+
// use Checkpoint bouncer to exchange tokens because it has the client_secret.
|
|
1149
1191
|
// See oauth-service.ts lines 82, 372 for the same pattern.
|
|
1150
1192
|
//
|
|
1151
1193
|
// Examples: GitHub Apps (PKCE, no proxy), Google (PKCE, no proxy)
|
|
@@ -1153,7 +1195,7 @@ export class ConsentService {
|
|
|
1153
1195
|
if (providerConfig &&
|
|
1154
1196
|
providerConfig.supportsPKCE &&
|
|
1155
1197
|
!providerConfig.proxyMode) {
|
|
1156
|
-
// Use providerConfig.clientId from
|
|
1198
|
+
// Use providerConfig.clientId from Checkpoint dashboard config
|
|
1157
1199
|
const oauthClientId = providerConfig.clientId || projectId;
|
|
1158
1200
|
// The `scopes` argument holds MCP-I delegation scopes (e.g. "greet:execute"),
|
|
1159
1201
|
// which the identity provider has never heard of. Only the provider's own
|
|
@@ -1194,11 +1236,11 @@ export class ConsentService {
|
|
|
1194
1236
|
}
|
|
1195
1237
|
}
|
|
1196
1238
|
}
|
|
1197
|
-
// Build OAuth authorization URL (
|
|
1198
|
-
// NOTE: This is a legacy proxy mode path that requires
|
|
1239
|
+
// Build OAuth authorization URL (Checkpoint bouncer endpoint)
|
|
1240
|
+
// NOTE: This is a legacy proxy mode path that requires Checkpoint to act as an OAuth proxy.
|
|
1199
1241
|
// Most providers (GitHub, Google) use direct PKCE mode instead (see buildDirectOAuthUrl).
|
|
1200
1242
|
// If this path is hit and results in a 404, check if the provider supports PKCE.
|
|
1201
|
-
const oauthUrl = new URL(`${
|
|
1243
|
+
const oauthUrl = new URL(`${checkpointUrl}/bouncer/oauth/authorize`);
|
|
1202
1244
|
oauthUrl.searchParams.set("response_type", "code");
|
|
1203
1245
|
oauthUrl.searchParams.set("client_id", projectId); // Use projectId as client_id
|
|
1204
1246
|
oauthUrl.searchParams.set("redirect_uri", `${serverUrl}/oauth/callback`);
|
|
@@ -1206,14 +1248,14 @@ export class ConsentService {
|
|
|
1206
1248
|
// delegation scopes, not what belongs in the IdP-facing `scope` param.
|
|
1207
1249
|
oauthUrl.searchParams.set("scope", resolveIdpScopes(providerConfig).join(" "));
|
|
1208
1250
|
oauthUrl.searchParams.set("state", stateParam);
|
|
1209
|
-
// ✅ Pass provider to
|
|
1251
|
+
// ✅ Pass provider to Checkpoint bouncer so it selects the correct auth method
|
|
1210
1252
|
// This is critical when multiple providers are configured for a project
|
|
1211
1253
|
// e.g., "credentials" for credential login or "github" for GitHub OAuth
|
|
1212
|
-
// Without this,
|
|
1254
|
+
// Without this, Checkpoint picks the first provider which may be incorrect
|
|
1213
1255
|
if (provider) {
|
|
1214
1256
|
oauthUrl.searchParams.set("provider", provider);
|
|
1215
1257
|
}
|
|
1216
|
-
// ✅ Add agent_name for
|
|
1258
|
+
// ✅ Add agent_name for Checkpoint dashboard display
|
|
1217
1259
|
// This provides a human-readable name instead of just the agent DID
|
|
1218
1260
|
if (agentName) {
|
|
1219
1261
|
oauthUrl.searchParams.set("agent_name", agentName);
|
|
@@ -1232,7 +1274,7 @@ export class ConsentService {
|
|
|
1232
1274
|
return oauthUrl.toString();
|
|
1233
1275
|
}
|
|
1234
1276
|
/**
|
|
1235
|
-
* Build direct OAuth URL (bypasses
|
|
1277
|
+
* Build direct OAuth URL (bypasses Checkpoint bouncer endpoint)
|
|
1236
1278
|
*
|
|
1237
1279
|
* Used for custom providers that need direct OAuth URLs (e.g., self-hosted OAuth servers).
|
|
1238
1280
|
* Triggered when providerConfig.proxyMode === true && !providerConfig.supportsPKCE
|
|
@@ -1264,7 +1306,7 @@ export class ConsentService {
|
|
|
1264
1306
|
* Link OAuth identity to User DID
|
|
1265
1307
|
*
|
|
1266
1308
|
* Maps OAuth provider identity (provider + subject) to a persistent User DID.
|
|
1267
|
-
* Phase 5: Requires existing userDid from
|
|
1309
|
+
* Phase 5: Requires existing userDid from Checkpoint /identity/resolve endpoint.
|
|
1268
1310
|
* Throws error if session has no user DID (enforces OAuth → identity flow).
|
|
1269
1311
|
*
|
|
1270
1312
|
* @param oauthIdentity - OAuth provider identity
|
|
@@ -1297,8 +1339,8 @@ export class ConsentService {
|
|
|
1297
1339
|
// Phase 5: Check for existing user DID in session
|
|
1298
1340
|
const userDid = await this.getUserDidForSession(sessionId);
|
|
1299
1341
|
if (!userDid) {
|
|
1300
|
-
// Phase 5: No existing DID - user must complete OAuth via
|
|
1301
|
-
throw new Error("[ConsentService] No user DID found for session - OAuth identity resolution required via
|
|
1342
|
+
// Phase 5: No existing DID - user must complete OAuth via Checkpoint first
|
|
1343
|
+
throw new Error("[ConsentService] No user DID found for session - OAuth identity resolution required via Checkpoint");
|
|
1302
1344
|
}
|
|
1303
1345
|
// Store OAuth identity mapping (persistent - 90 days)
|
|
1304
1346
|
try {
|
|
@@ -1323,14 +1365,14 @@ export class ConsentService {
|
|
|
1323
1365
|
return userDid;
|
|
1324
1366
|
}
|
|
1325
1367
|
/**
|
|
1326
|
-
* Cache an externally-resolved User DID (from
|
|
1368
|
+
* Cache an externally-resolved User DID (from Checkpoint identity resolution)
|
|
1327
1369
|
*
|
|
1328
|
-
* Phase 5: When
|
|
1370
|
+
* Phase 5: When Checkpoint provides a persistent user_did, we cache the
|
|
1329
1371
|
* OAuth identity → userDid mapping in KV so future lookups don't require
|
|
1330
|
-
* calling
|
|
1372
|
+
* calling Checkpoint again.
|
|
1331
1373
|
*
|
|
1332
1374
|
* @param oauthIdentity - OAuth identity from provider
|
|
1333
|
-
* @param userDid - Persistent user DID from
|
|
1375
|
+
* @param userDid - Persistent user DID from Checkpoint
|
|
1334
1376
|
* @returns void
|
|
1335
1377
|
*/
|
|
1336
1378
|
async cacheExternalUserDid(oauthIdentity, userDid) {
|
|
@@ -1351,7 +1393,7 @@ export class ConsentService {
|
|
|
1351
1393
|
existingUserDid: existingUserDid.substring(0, 30) + "...",
|
|
1352
1394
|
newUserDid: userDid.substring(0, 30) + "...",
|
|
1353
1395
|
});
|
|
1354
|
-
// Don't overwrite -
|
|
1396
|
+
// Don't overwrite - Checkpoint is source of truth
|
|
1355
1397
|
}
|
|
1356
1398
|
return;
|
|
1357
1399
|
}
|
|
@@ -1370,11 +1412,11 @@ export class ConsentService {
|
|
|
1370
1412
|
await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
|
|
1371
1413
|
expirationTtl: 90 * 24 * 60 * 60, // 90 days
|
|
1372
1414
|
});
|
|
1373
|
-
logger.debug("[ConsentService] Cached external User DID from
|
|
1415
|
+
logger.debug("[ConsentService] Cached external User DID from Checkpoint:", {
|
|
1374
1416
|
provider: oauthIdentity.provider,
|
|
1375
1417
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1376
1418
|
userDid: userDid.substring(0, 30) + "...",
|
|
1377
|
-
source: "
|
|
1419
|
+
source: "checkpoint_identity_resolution",
|
|
1378
1420
|
});
|
|
1379
1421
|
}
|
|
1380
1422
|
catch (error) {
|
|
@@ -1530,7 +1572,7 @@ export class ConsentService {
|
|
|
1530
1572
|
// e.g., ?provider_type=password indicates credential auth is expected
|
|
1531
1573
|
const urlProviderType = params.get("provider_type");
|
|
1532
1574
|
// ✅ Extract agent_name from URL query param (passed from buildConsentUrl)
|
|
1533
|
-
// Human-readable name for
|
|
1575
|
+
// Human-readable name for Checkpoint dashboard display
|
|
1534
1576
|
const agentName = params.get("agent_name");
|
|
1535
1577
|
// ✅ Extract credential auth params for 3-screen flow (Auth → Clickwrap → Success)
|
|
1536
1578
|
// These are set when credential auth completes and redirects to clickwrap page
|
|
@@ -1542,7 +1584,7 @@ export class ConsentService {
|
|
|
1542
1584
|
// This ensures storeDelegationToken can use the correct user+agent key immediately
|
|
1543
1585
|
const userDidFromParams = params.get("user_did");
|
|
1544
1586
|
// ✅ Extract user info from credential provider for delegation metadata
|
|
1545
|
-
// These are used for human-readable display in
|
|
1587
|
+
// These are used for human-readable display in Checkpoint dashboard
|
|
1546
1588
|
const credentialUserEmail = params.get("credential_user_email");
|
|
1547
1589
|
const credentialProviderUserId = params.get("credential_provider_user_id");
|
|
1548
1590
|
// ✅ Extract mode param for post-auth clickwrap detection
|
|
@@ -1584,6 +1626,13 @@ export class ConsentService {
|
|
|
1584
1626
|
const scopes = scopesParam
|
|
1585
1627
|
? scopesParam.split(",").map((s) => s.trim())
|
|
1586
1628
|
: [];
|
|
1629
|
+
// Resolve the server-bound mandate before anything renders: with a
|
|
1630
|
+
// provider configured, a failure must never produce an approvable page.
|
|
1631
|
+
const mandateResolution = await this.resolveMandate(scopes, projectId);
|
|
1632
|
+
if (!mandateResolution.ok) {
|
|
1633
|
+
return this.mandateErrorResponse();
|
|
1634
|
+
}
|
|
1635
|
+
const mandate = mandateResolution.mandate;
|
|
1587
1636
|
// Get tool description (try to fetch from runtime or use default)
|
|
1588
1637
|
let toolDescription = `Execute ${tool}`;
|
|
1589
1638
|
if (this.runtime) {
|
|
@@ -1596,7 +1645,7 @@ export class ConsentService {
|
|
|
1596
1645
|
// Fallback to default
|
|
1597
1646
|
}
|
|
1598
1647
|
}
|
|
1599
|
-
// Get consent config from
|
|
1648
|
+
// Get consent config from Checkpoint or defaults
|
|
1600
1649
|
const consentConfig = await this.configService.getConsentConfig(projectId);
|
|
1601
1650
|
// Build server URL from request origin
|
|
1602
1651
|
// Priority: 1) env var, 2) request origin, 3) error if neither available
|
|
@@ -1835,7 +1884,7 @@ export class ConsentService {
|
|
|
1835
1884
|
// Note: We're building it here, but we'll pass it to the renderer
|
|
1836
1885
|
// instead of redirecting immediately, so the user sees the consent page first.
|
|
1837
1886
|
resolvedOAuthUrl = await this.buildOAuthUrl(projectId, agentDid, sessionId, scopes, serverUrl, provider, // Pass resolved provider for direct OAuth mode (PKCE)
|
|
1838
|
-
oauthSecurityService, agentName || undefined, // Human-readable agent name for
|
|
1887
|
+
oauthSecurityService, agentName || undefined, // Human-readable agent name for Checkpoint dashboard
|
|
1839
1888
|
tool || undefined // Tool name to preserve through OAuth callback for clickwrap redirect
|
|
1840
1889
|
);
|
|
1841
1890
|
isOAuthRequired = true;
|
|
@@ -1937,7 +1986,7 @@ export class ConsentService {
|
|
|
1937
1986
|
// Build consent page config
|
|
1938
1987
|
// ✅ CRITICAL: Pass the FULL consent config (ui, credentials, etc.) not just branding/terms
|
|
1939
1988
|
// Type assertion to handle updated schema before rebuild
|
|
1940
|
-
// The remote consent config (from
|
|
1989
|
+
// The remote consent config (from checkpoint) carries fields that
|
|
1941
1990
|
// aren't in the trimmed contracts ConsentPageConfig type. Read them off
|
|
1942
1991
|
// a Record<string, unknown> view so we can plumb them through without
|
|
1943
1992
|
// touching contracts.
|
|
@@ -2058,8 +2107,13 @@ export class ConsentService {
|
|
|
2058
2107
|
ui: consentConfig.ui,
|
|
2059
2108
|
// ✅ FIX: Pass credentials config (username/password labels, forgot password, etc.)
|
|
2060
2109
|
credentials: consentConfig.credentials,
|
|
2061
|
-
// ✅ FIX: Pass expiration days
|
|
2062
|
-
|
|
2110
|
+
// ✅ FIX: Pass expiration days. A mandate's fixed lifetime wins, and is
|
|
2111
|
+
// also stated as fixedExpiryDays so the footer says "N days after
|
|
2112
|
+
// approval" instead of the (false, for these grants) inactivity copy.
|
|
2113
|
+
expirationDays: mandate?.expiresInDays ?? consentConfig.expirationDays,
|
|
2114
|
+
...(mandate?.expiresInDays !== undefined
|
|
2115
|
+
? { fixedExpiryDays: mandate.expiresInDays }
|
|
2116
|
+
: {}),
|
|
2063
2117
|
// Pass OAuth details for client-side handling
|
|
2064
2118
|
oauthRequired: isOAuthRequired,
|
|
2065
2119
|
oauthUrl: resolvedOAuthUrl,
|
|
@@ -2083,12 +2137,14 @@ export class ConsentService {
|
|
|
2083
2137
|
// from the bouncer config; falls back to the @kya-os/consent default
|
|
2084
2138
|
// registry resolved from the URL's scopes, so the new layout renders
|
|
2085
2139
|
// even when the dashboard hasn't pushed capability metadata yet.
|
|
2086
|
-
capabilities
|
|
2140
|
+
// A mandate's capabilities describe the limits the grant will carry,
|
|
2141
|
+
// so they replace remote and registry rows.
|
|
2142
|
+
capabilities: mandate?.capabilities ?? resolvedCapabilities,
|
|
2087
2143
|
agentMetadata: resolvedAgentMetadata,
|
|
2088
2144
|
consentTheme: remoteConsentTheme,
|
|
2089
2145
|
orgName: resolvedOrgName,
|
|
2090
2146
|
headlineVerb: remoteHeadlineVerb,
|
|
2091
|
-
revocationPath: remoteRevocationPath,
|
|
2147
|
+
revocationPath: mandate?.revocationPath ?? remoteRevocationPath,
|
|
2092
2148
|
inactivityDays: remoteInactivityDays,
|
|
2093
2149
|
howItWorksUrl: remoteHowItWorksUrl,
|
|
2094
2150
|
// Bind operator-authored Cedar fragment placeholders at render time
|
|
@@ -2109,7 +2165,7 @@ export class ConsentService {
|
|
|
2109
2165
|
// 2. Must NOT be in post-credential clickwrap mode (would cause infinite loop)
|
|
2110
2166
|
if (credentialProviderConfig && !isPostCredentialClickwrap) {
|
|
2111
2167
|
// Determine authorization type from tool protection config
|
|
2112
|
-
// Default to 'password' for credential flows (matches
|
|
2168
|
+
// Default to 'password' for credential flows (matches Checkpoint expectations)
|
|
2113
2169
|
// The authorization.type from tool protection tells us what type of auth the tool expects
|
|
2114
2170
|
const authorizationType = protection?.authorization?.type ?? "password";
|
|
2115
2171
|
logger.debug("[ConsentService] Credential provider detected, rendering credential page", {
|
|
@@ -3269,7 +3325,7 @@ export class ConsentService {
|
|
|
3269
3325
|
/**
|
|
3270
3326
|
* Handle consent approval
|
|
3271
3327
|
*
|
|
3272
|
-
* Validates request, creates delegation via
|
|
3328
|
+
* Validates request, creates delegation via Checkpoint API,
|
|
3273
3329
|
* stores token in KV, and returns success response.
|
|
3274
3330
|
*
|
|
3275
3331
|
* @param request - Approval request
|
|
@@ -3303,7 +3359,7 @@ export class ConsentService {
|
|
|
3303
3359
|
let trustedUserDid;
|
|
3304
3360
|
// CRED-003: Check for credential provider submission
|
|
3305
3361
|
// Credential submissions include `provider_type: 'credential'` or 'password' and are handled separately
|
|
3306
|
-
// IMPORTANT:
|
|
3362
|
+
// IMPORTANT: Checkpoint tool protection uses 'password' for credential-based auth validation,
|
|
3307
3363
|
// but we also accept 'credential' for backward compatibility
|
|
3308
3364
|
if (providerType === CONSENT_PROVIDER_TYPES.CREDENTIAL ||
|
|
3309
3365
|
providerType === CONSENT_PROVIDER_TYPES.PASSWORD) {
|
|
@@ -3632,7 +3688,7 @@ export class ConsentService {
|
|
|
3632
3688
|
headers: { "Content-Type": "application/json" },
|
|
3633
3689
|
});
|
|
3634
3690
|
}
|
|
3635
|
-
// Create delegation via
|
|
3691
|
+
// Create delegation via Checkpoint API
|
|
3636
3692
|
logger.debug("[ConsentService] Creating delegation...");
|
|
3637
3693
|
const delegationResult = await this.createDelegation(approvalRequest, resolveExpirationDays(consentConfig.expirationDays), resolvedUserDid ?? null);
|
|
3638
3694
|
if (!delegationResult.success) {
|
|
@@ -3732,14 +3788,21 @@ export class ConsentService {
|
|
|
3732
3788
|
}
|
|
3733
3789
|
}
|
|
3734
3790
|
/**
|
|
3735
|
-
* Create delegation via
|
|
3791
|
+
* Create delegation via Checkpoint API
|
|
3736
3792
|
*
|
|
3737
3793
|
* @param request - Approval request
|
|
3738
3794
|
* @returns Delegation creation result
|
|
3739
3795
|
*/
|
|
3740
3796
|
async createDelegation(request, expirationDaysOverride, resolvedUserDid) {
|
|
3741
|
-
|
|
3742
|
-
|
|
3797
|
+
// Resolve the mandate from the APPROVED scopes first: nothing (no VC, no
|
|
3798
|
+
// POST) may happen when a configured provider cannot answer.
|
|
3799
|
+
const mandateResolution = await this.resolveMandate(request.scopes ?? [], request.project_id);
|
|
3800
|
+
if (!mandateResolution.ok) {
|
|
3801
|
+
return { success: false, ...MANDATE_ERROR };
|
|
3802
|
+
}
|
|
3803
|
+
const mandate = mandateResolution.mandate;
|
|
3804
|
+
const checkpointUrl = this.env.CHECKPOINT_API_URL || DEFAULT_CHECKPOINT_URL;
|
|
3805
|
+
const apiKey = this.env.CHECKPOINT_API_KEY;
|
|
3743
3806
|
if (!apiKey) {
|
|
3744
3807
|
logger.warn("[ConsentService] No API key configured, cannot create delegation");
|
|
3745
3808
|
return {
|
|
@@ -3806,7 +3869,11 @@ export class ConsentService {
|
|
|
3806
3869
|
logger.debug("[ConsentService] No session_id provided - skipping User DID generation");
|
|
3807
3870
|
}
|
|
3808
3871
|
}
|
|
3809
|
-
|
|
3872
|
+
// A mandate's fixed lifetime is part of the grant the user saw, so it
|
|
3873
|
+
// wins over the consent-config expiry.
|
|
3874
|
+
const expiresInDays = mandate?.expiresInDays ??
|
|
3875
|
+
expirationDaysOverride ??
|
|
3876
|
+
DEFAULT_EXPIRATION_DAYS;
|
|
3810
3877
|
// Phase 2 VC-Only: Issue Delegation VC if we have a session and userDid
|
|
3811
3878
|
let credentialJwt;
|
|
3812
3879
|
if (request.session_id && userDid) {
|
|
@@ -3827,6 +3894,8 @@ export class ConsentService {
|
|
|
3827
3894
|
scopes,
|
|
3828
3895
|
notAfter,
|
|
3829
3896
|
notBefore: nowSec,
|
|
3897
|
+
// The user signs the same limits Checkpoint is asked to hold.
|
|
3898
|
+
...(mandate ? { crisp: mandate.constraints.crisp } : {}),
|
|
3830
3899
|
},
|
|
3831
3900
|
signature: "vc-jwt-signed", // Placeholder - actual signature is in the VC-JWT
|
|
3832
3901
|
status: "active",
|
|
@@ -3848,9 +3917,14 @@ export class ConsentService {
|
|
|
3848
3917
|
}
|
|
3849
3918
|
// Build delegation request with error-based format detection
|
|
3850
3919
|
// Try full format first, fallback to simplified format on error
|
|
3851
|
-
const
|
|
3920
|
+
const builtRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName, credentialJwt);
|
|
3921
|
+
// Every body tryAPICall may POST carries the mandate's constraints, so
|
|
3922
|
+
// a format fallback can never create an unconstrained grant.
|
|
3923
|
+
const delegationRequest = mandate
|
|
3924
|
+
? withMandateConstraints(builtRequest, mandate.constraints)
|
|
3925
|
+
: builtRequest;
|
|
3852
3926
|
logger.debug("[ConsentService] Creating delegation:", {
|
|
3853
|
-
url: `${
|
|
3927
|
+
url: `${checkpointUrl}${CHECKPOINT_ENDPOINTS.DELEGATIONS_CREATE}`,
|
|
3854
3928
|
agentDid: request.agent_did.substring(0, 20) + "...",
|
|
3855
3929
|
scopes: request.scopes,
|
|
3856
3930
|
expiresInDays,
|
|
@@ -3859,12 +3933,12 @@ export class ConsentService {
|
|
|
3859
3933
|
hasUserDid: !!userDid,
|
|
3860
3934
|
});
|
|
3861
3935
|
// Error-based format detection: try request format, fallback on error
|
|
3862
|
-
const response = await this.tryAPICall(
|
|
3936
|
+
const response = await this.tryAPICall(checkpointUrl, apiKey, delegationRequest);
|
|
3863
3937
|
if (!response.success) {
|
|
3864
3938
|
return response;
|
|
3865
3939
|
}
|
|
3866
3940
|
const responseData = response.data;
|
|
3867
|
-
// Canonical format per @kya-os/contracts/
|
|
3941
|
+
// Canonical format per @kya-os/contracts/checkpoint-api:
|
|
3868
3942
|
// Wrapped: { success: true, data: { delegation_id: string, agent_did: string, ... } }
|
|
3869
3943
|
// Unwrapped: { delegation_id: string, agent_did: string, ... }
|
|
3870
3944
|
//
|
|
@@ -3877,7 +3951,7 @@ export class ConsentService {
|
|
|
3877
3951
|
const wrappedResponse = wrappedParse.data;
|
|
3878
3952
|
const delegationId = wrappedResponse.data.delegation_id;
|
|
3879
3953
|
// Extract delegation_token (JWT) if present in response
|
|
3880
|
-
//
|
|
3954
|
+
// Checkpoint now returns delegation_token for stateless verification
|
|
3881
3955
|
const responseDataObj = wrappedResponse.data;
|
|
3882
3956
|
const delegationToken = responseDataObj.delegation_token;
|
|
3883
3957
|
logger.info("[ConsentService] ✅ Delegation created successfully:", {
|
|
@@ -4236,11 +4310,11 @@ export class ConsentService {
|
|
|
4236
4310
|
// Ignore cache errors
|
|
4237
4311
|
}
|
|
4238
4312
|
}
|
|
4239
|
-
//
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4313
|
+
// A cached "simplified" skips the full POST. A cached "full" gets the same
|
|
4314
|
+
// try-both request as no cache: a stale or wrongly cached "full" must never
|
|
4315
|
+
// remove the simplified fallback, because an API that rejects the full
|
|
4316
|
+
// format would then fail every approval until the cache expired.
|
|
4317
|
+
if (cachedFormat === "simplified") {
|
|
4244
4318
|
return this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName, credentialJwt);
|
|
4245
4319
|
}
|
|
4246
4320
|
// No cache - return request that will be tried with error-based detection
|
|
@@ -4257,7 +4331,7 @@ export class ConsentService {
|
|
|
4257
4331
|
* Handle credential-based authentication submission
|
|
4258
4332
|
*
|
|
4259
4333
|
* Authenticates user credentials against customer's endpoint, resolves identity
|
|
4260
|
-
* via
|
|
4334
|
+
* via Checkpoint, stores token with usage metadata, and creates delegation.
|
|
4261
4335
|
*
|
|
4262
4336
|
* @param body - Raw request body with credential fields
|
|
4263
4337
|
* @param request - Original request (for URL derivation if MCP_SERVER_URL not set)
|
|
@@ -4327,7 +4401,7 @@ export class ConsentService {
|
|
|
4327
4401
|
}
|
|
4328
4402
|
}
|
|
4329
4403
|
try {
|
|
4330
|
-
// 1. Fetch credential provider config from
|
|
4404
|
+
// 1. Fetch credential provider config from Checkpoint
|
|
4331
4405
|
const providerConfig = await this.getCredentialProviderConfig(project_id, provider);
|
|
4332
4406
|
if (!providerConfig) {
|
|
4333
4407
|
logger.debug("[ConsentService] Credential provider config not found", {
|
|
@@ -4377,7 +4451,7 @@ export class ConsentService {
|
|
|
4377
4451
|
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
4378
4452
|
}
|
|
4379
4453
|
logger.info("[ConsentService] ✅ Credential authentication successful");
|
|
4380
|
-
// 3. Resolve identity via
|
|
4454
|
+
// 3. Resolve identity via Checkpoint
|
|
4381
4455
|
const identityResult = await this.resolveCredentialIdentity({
|
|
4382
4456
|
projectId: project_id,
|
|
4383
4457
|
provider: provider,
|
|
@@ -4578,7 +4652,7 @@ export class ConsentService {
|
|
|
4578
4652
|
// credential_provider_user_id → user_id (provider's internal ID for business reference)
|
|
4579
4653
|
// user_did → user_did (cryptographic identity, always present)
|
|
4580
4654
|
if (authResult.userEmail) {
|
|
4581
|
-
// Email for human-readable display in
|
|
4655
|
+
// Email for human-readable display in Checkpoint dashboard
|
|
4582
4656
|
clickwrapUrl.searchParams.set("credential_user_email", authResult.userEmail);
|
|
4583
4657
|
}
|
|
4584
4658
|
if (authResult.userId) {
|
|
@@ -4680,7 +4754,7 @@ export class ConsentService {
|
|
|
4680
4754
|
return [];
|
|
4681
4755
|
}
|
|
4682
4756
|
/**
|
|
4683
|
-
* Fetch credential provider config from
|
|
4757
|
+
* Fetch credential provider config from Checkpoint
|
|
4684
4758
|
*
|
|
4685
4759
|
* Uses the V1 /providers endpoint which returns all configured providers
|
|
4686
4760
|
* including credential providers with type: 'credential'.
|
|
@@ -4688,17 +4762,17 @@ export class ConsentService {
|
|
|
4688
4762
|
* @see OAuthConfigService.getOAuthConfig() for the same endpoint usage
|
|
4689
4763
|
*/
|
|
4690
4764
|
async getCredentialProviderConfig(projectId, providerName) {
|
|
4691
|
-
const
|
|
4692
|
-
const apiKey = this.env.
|
|
4765
|
+
const checkpointUrl = this.env.CHECKPOINT_API_URL || DEFAULT_CHECKPOINT_URL;
|
|
4766
|
+
const apiKey = this.env.CHECKPOINT_API_KEY;
|
|
4693
4767
|
if (!apiKey) {
|
|
4694
|
-
logger.warn("[ConsentService] No
|
|
4768
|
+
logger.warn("[ConsentService] No Checkpoint API key configured");
|
|
4695
4769
|
return null;
|
|
4696
4770
|
}
|
|
4697
4771
|
try {
|
|
4698
4772
|
// Use the /providers endpoint - same as OAuthConfigService.getOAuthConfig()
|
|
4699
4773
|
// This endpoint was updated in fix/provider-config to read from oauthProviders JSONB
|
|
4700
4774
|
// Authentication: Use Authorization: Bearer header for consistency with OAuthConfigService
|
|
4701
|
-
const response = await fetch(`${
|
|
4775
|
+
const response = await fetch(`${checkpointUrl}/api/v1/bouncer/projects/${encodeURIComponent(projectId)}/providers`, {
|
|
4702
4776
|
headers: {
|
|
4703
4777
|
Authorization: `Bearer ${apiKey}`,
|
|
4704
4778
|
"Content-Type": "application/json",
|
|
@@ -4726,13 +4800,13 @@ export class ConsentService {
|
|
|
4726
4800
|
if (providerName in providers) {
|
|
4727
4801
|
const rawProvider = providers[providerName];
|
|
4728
4802
|
// AUTH-TYPE-UNIFICATION: 'password' is the canonical type for form-based auth
|
|
4729
|
-
//
|
|
4803
|
+
// Checkpoint now returns type: 'password' (updated 2025-12-29)
|
|
4730
4804
|
// Also accept 'credential' for backward compatibility with existing configurations
|
|
4731
4805
|
// See: proposals/2025-01-delegation-schema-fix/AUTH-TYPE-UNIFICATION.md
|
|
4732
4806
|
if (rawProvider.type === CONSENT_PROVIDER_TYPES.PASSWORD ||
|
|
4733
4807
|
rawProvider.type === CONSENT_PROVIDER_TYPES.CREDENTIAL) {
|
|
4734
4808
|
// Map snake_case fields to camelCase (same pattern as OAuthConfigService)
|
|
4735
|
-
//
|
|
4809
|
+
// Checkpoint API may return snake_case fields from database
|
|
4736
4810
|
const provider = this.mapCredentialProviderFields(rawProvider, providerName);
|
|
4737
4811
|
if (!provider) {
|
|
4738
4812
|
logger.debug("[ConsentService] Credential provider validation failed:", {
|
|
@@ -4759,7 +4833,7 @@ export class ConsentService {
|
|
|
4759
4833
|
/**
|
|
4760
4834
|
* Map snake_case fields to camelCase for CredentialProviderConfig
|
|
4761
4835
|
*
|
|
4762
|
-
*
|
|
4836
|
+
* Checkpoint API may return snake_case fields from the database.
|
|
4763
4837
|
* This follows the same pattern as OAuthConfigService.getOAuthConfig().
|
|
4764
4838
|
*
|
|
4765
4839
|
* @param raw - Raw provider object from API response
|
|
@@ -4845,17 +4919,17 @@ export class ConsentService {
|
|
|
4845
4919
|
};
|
|
4846
4920
|
}
|
|
4847
4921
|
/**
|
|
4848
|
-
* Resolve identity via
|
|
4922
|
+
* Resolve identity via Checkpoint for credential-based auth
|
|
4849
4923
|
*/
|
|
4850
4924
|
async resolveCredentialIdentity(params) {
|
|
4851
|
-
const
|
|
4852
|
-
const apiKey = this.env.
|
|
4925
|
+
const checkpointUrl = this.env.CHECKPOINT_API_URL || DEFAULT_CHECKPOINT_URL;
|
|
4926
|
+
const apiKey = this.env.CHECKPOINT_API_KEY;
|
|
4853
4927
|
if (!apiKey) {
|
|
4854
|
-
logger.warn("[ConsentService] No
|
|
4928
|
+
logger.warn("[ConsentService] No Checkpoint API key for identity resolution");
|
|
4855
4929
|
return { success: false };
|
|
4856
4930
|
}
|
|
4857
4931
|
try {
|
|
4858
|
-
const response = await fetch(`${
|
|
4932
|
+
const response = await fetch(`${checkpointUrl}/api/v1/bouncer/identity/resolve`, {
|
|
4859
4933
|
method: "POST",
|
|
4860
4934
|
headers: {
|
|
4861
4935
|
"X-API-Key": apiKey,
|
|
@@ -5289,12 +5363,12 @@ export class ConsentService {
|
|
|
5289
5363
|
//
|
|
5290
5364
|
// Fallback Strategy:
|
|
5291
5365
|
// 1. If email available → user_identifier = email (preferred)
|
|
5292
|
-
// 2. If no email → user_identifier = userDid (fallback so
|
|
5293
|
-
//
|
|
5366
|
+
// 2. If no email → user_identifier = userDid (fallback so Checkpoint has something to display)
|
|
5367
|
+
// Checkpoint will also check metadata.user_email for extraction
|
|
5294
5368
|
if (userDid) {
|
|
5295
5369
|
// user_did is the primary identifier - the actual DID
|
|
5296
5370
|
result.user_did = userDid;
|
|
5297
|
-
// user_identifier: Human-readable identifier (email/username) for
|
|
5371
|
+
// user_identifier: Human-readable identifier (email/username) for Checkpoint dashboard display
|
|
5298
5372
|
// user_id: Provider's internal ID (e.g., customer ID 696395) for business reference
|
|
5299
5373
|
// user_did: Cryptographic DID for identity verification
|
|
5300
5374
|
if (request.credential_user_email) {
|
|
@@ -5303,7 +5377,7 @@ export class ConsentService {
|
|
|
5303
5377
|
}
|
|
5304
5378
|
else {
|
|
5305
5379
|
// No email - fall back to DID so dashboard has something to display
|
|
5306
|
-
//
|
|
5380
|
+
// Checkpoint will detect this is a DID and may display differently
|
|
5307
5381
|
result.user_identifier = userDid;
|
|
5308
5382
|
}
|
|
5309
5383
|
// user_id is the provider's internal ID (separate from email)
|
|
@@ -5331,7 +5405,7 @@ export class ConsentService {
|
|
|
5331
5405
|
}
|
|
5332
5406
|
// MCP-I Compatibility: Include user_email and user_id in custom_fields (becomes metadata)
|
|
5333
5407
|
// This matches buildSimplifiedFormatRequest behavior for consistency
|
|
5334
|
-
//
|
|
5408
|
+
// Checkpoint extracts these from metadata when user_identifier is not provided
|
|
5335
5409
|
//
|
|
5336
5410
|
// user_email = human-readable email (for display)
|
|
5337
5411
|
// user_id = provider's internal ID (for business reference)
|
|
@@ -5351,7 +5425,7 @@ export class ConsentService {
|
|
|
5351
5425
|
format_version: "full_v1",
|
|
5352
5426
|
...(providerType ? { provider_type: providerType } : {}),
|
|
5353
5427
|
// MCP-I Compatibility: Include user_email and user_id in metadata
|
|
5354
|
-
//
|
|
5428
|
+
// Checkpoint will extract these if user_identifier is not provided
|
|
5355
5429
|
// user_email = human-readable email (for display)
|
|
5356
5430
|
// user_id = provider's internal ID (for business reference, e.g., customer ID 696395)
|
|
5357
5431
|
...(fullFormatUserEmail ? { user_email: fullFormatUserEmail } : {}),
|
|
@@ -5362,7 +5436,7 @@ export class ConsentService {
|
|
|
5362
5436
|
...customFieldsFromRequest,
|
|
5363
5437
|
};
|
|
5364
5438
|
if (fullFormatUserEmail || fullFormatProviderUserId) {
|
|
5365
|
-
logger.debug("[ConsentService] Full format: Including user info in custom_fields for
|
|
5439
|
+
logger.debug("[ConsentService] Full format: Including user info in custom_fields for Checkpoint compatibility:", {
|
|
5366
5440
|
user_email: fullFormatUserEmail
|
|
5367
5441
|
? fullFormatUserEmail.substring(0, 20) + "..."
|
|
5368
5442
|
: undefined,
|
|
@@ -5398,14 +5472,14 @@ export class ConsentService {
|
|
|
5398
5472
|
* Build simplified format request with proper field name
|
|
5399
5473
|
*
|
|
5400
5474
|
* CRITICAL: This method MUST NOT include session_id or project_id in the request body.
|
|
5401
|
-
* These fields are NOT part of
|
|
5402
|
-
* - project_id is extracted from API key context by
|
|
5475
|
+
* These fields are NOT part of Checkpoint's createDelegationSchema:
|
|
5476
|
+
* - project_id is extracted from API key context by Checkpoint middleware
|
|
5403
5477
|
* - session_id is not needed for delegation creation
|
|
5404
5478
|
*
|
|
5405
5479
|
* Including these fields will cause validation errors (400 Bad Request).
|
|
5406
5480
|
*/
|
|
5407
5481
|
buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName, credentialJwt) {
|
|
5408
|
-
// Build request with ONLY fields that are in
|
|
5482
|
+
// Build request with ONLY fields that are in Checkpoint's schema
|
|
5409
5483
|
// Defensive check: ensure scopes is always defined (should be guaranteed by validation)
|
|
5410
5484
|
const scopes = request.scopes ?? [];
|
|
5411
5485
|
const simplifiedRequest = {
|
|
@@ -5420,12 +5494,12 @@ export class ConsentService {
|
|
|
5420
5494
|
//
|
|
5421
5495
|
// Fallback Strategy:
|
|
5422
5496
|
// 1. If email available → user_identifier = email (preferred)
|
|
5423
|
-
// 2. If no email → user_identifier = userDid (fallback so
|
|
5424
|
-
//
|
|
5497
|
+
// 2. If no email → user_identifier = userDid (fallback so Checkpoint has something to display)
|
|
5498
|
+
// Checkpoint will also check metadata.user_email for extraction
|
|
5425
5499
|
if (userDid) {
|
|
5426
5500
|
// user_did is the primary identifier - the actual DID
|
|
5427
5501
|
simplifiedRequest.user_did = userDid;
|
|
5428
|
-
// user_identifier: Human-readable identifier (email/username) for
|
|
5502
|
+
// user_identifier: Human-readable identifier (email/username) for Checkpoint dashboard display
|
|
5429
5503
|
// user_id: Provider's internal ID (e.g., customer ID 696395) for business reference
|
|
5430
5504
|
// user_did: Cryptographic DID for identity verification
|
|
5431
5505
|
if (request.credential_user_email) {
|
|
@@ -5434,7 +5508,7 @@ export class ConsentService {
|
|
|
5434
5508
|
}
|
|
5435
5509
|
else {
|
|
5436
5510
|
// No email - fall back to DID so dashboard has something to display
|
|
5437
|
-
//
|
|
5511
|
+
// Checkpoint will detect this is a DID and may display differently
|
|
5438
5512
|
simplifiedRequest.user_identifier = userDid;
|
|
5439
5513
|
}
|
|
5440
5514
|
// user_id is the provider's internal ID (separate from email)
|
|
@@ -5465,7 +5539,7 @@ export class ConsentService {
|
|
|
5465
5539
|
simplifiedRequest.credential_jwt = credentialJwt;
|
|
5466
5540
|
logger.debug("[ConsentService] Including credential_jwt in delegation request");
|
|
5467
5541
|
}
|
|
5468
|
-
//
|
|
5542
|
+
// Checkpoint API only accepts "custom_fields", not "metadata"
|
|
5469
5543
|
// Always use "custom_fields" regardless of Day0 config
|
|
5470
5544
|
// Include provider_type and provider for dashboard authorization display
|
|
5471
5545
|
//
|
|
@@ -5480,7 +5554,7 @@ export class ConsentService {
|
|
|
5480
5554
|
const provider = customFieldsFromRequest.provider;
|
|
5481
5555
|
const hasProviderInfo = providerType || provider;
|
|
5482
5556
|
// MCP-I Compatibility: Include user_email and user_id in custom_fields (becomes metadata)
|
|
5483
|
-
//
|
|
5557
|
+
// Checkpoint extracts these from metadata when user_identifier is a DID or not provided
|
|
5484
5558
|
// This ensures human-readable identifiers are always available for dashboard display
|
|
5485
5559
|
//
|
|
5486
5560
|
// user_email = human-readable email (for display)
|
|
@@ -5490,8 +5564,8 @@ export class ConsentService {
|
|
|
5490
5564
|
.credential_provider_user_id || null;
|
|
5491
5565
|
if (userDid || hasProviderInfo || userEmail) {
|
|
5492
5566
|
// Include issuer_did and subject_did in custom_fields when we have userDid
|
|
5493
|
-
// Also include provider_type and provider for
|
|
5494
|
-
// CRITICAL:
|
|
5567
|
+
// Also include provider_type and provider for Checkpoint dashboard
|
|
5568
|
+
// CRITICAL: Checkpoint derives authorization from metadata.provider_type:
|
|
5495
5569
|
// - provider_type === 'password' → authorization.type = 'password' (credential auth)
|
|
5496
5570
|
// - provider_type === 'none' → authorization.type = 'none' (consent-only)
|
|
5497
5571
|
// - oauth_provider present → authorization.type = 'oauth'
|
|
@@ -5504,10 +5578,10 @@ export class ConsentService {
|
|
|
5504
5578
|
}
|
|
5505
5579
|
: {}),
|
|
5506
5580
|
format_version: "simplified_v1",
|
|
5507
|
-
// Include provider_type at the custom_fields level (where
|
|
5581
|
+
// Include provider_type at the custom_fields level (where Checkpoint reads it)
|
|
5508
5582
|
...(providerType ? { provider_type: providerType } : {}),
|
|
5509
5583
|
// MCP-I Compatibility: Include user_email and user_id in metadata
|
|
5510
|
-
//
|
|
5584
|
+
// Checkpoint will extract these if user_identifier is not provided or is a DID
|
|
5511
5585
|
// user_email = human-readable email (for display)
|
|
5512
5586
|
// user_id = provider's internal ID (for business reference, e.g., customer ID 696395)
|
|
5513
5587
|
...(userEmail ? { user_email: userEmail } : {}),
|
|
@@ -5525,7 +5599,7 @@ export class ConsentService {
|
|
|
5525
5599
|
});
|
|
5526
5600
|
}
|
|
5527
5601
|
if (userEmail || providerUserId) {
|
|
5528
|
-
logger.debug("[ConsentService] Including user info in custom_fields for
|
|
5602
|
+
logger.debug("[ConsentService] Including user info in custom_fields for Checkpoint compatibility:", {
|
|
5529
5603
|
user_email: userEmail
|
|
5530
5604
|
? userEmail.substring(0, 20) + "..."
|
|
5531
5605
|
: undefined,
|
|
@@ -5550,35 +5624,39 @@ export class ConsentService {
|
|
|
5550
5624
|
/**
|
|
5551
5625
|
* Try API call with error-based format detection
|
|
5552
5626
|
*/
|
|
5553
|
-
async tryAPICall(
|
|
5627
|
+
async tryAPICall(checkpointUrl, apiKey, request) {
|
|
5554
5628
|
// Handle format detection
|
|
5555
5629
|
if (request._tryFormats && request.fullFormat && request.simplifiedFormat) {
|
|
5556
5630
|
// Try full format first
|
|
5557
|
-
const fullResponse = await this.makeAPICall(
|
|
5558
|
-
if (fullResponse.success
|
|
5559
|
-
fullResponse.error_code !== "validation_error") {
|
|
5560
|
-
// Full format worked or failed for non-format reasons
|
|
5631
|
+
const fullResponse = await this.makeAPICall(checkpointUrl, apiKey, request.fullFormat);
|
|
5632
|
+
if (fullResponse.success) {
|
|
5561
5633
|
await this.cacheFormatPreference("full");
|
|
5562
5634
|
return fullResponse;
|
|
5563
5635
|
}
|
|
5636
|
+
// A failure that says nothing about the format (5xx, 401, network) is
|
|
5637
|
+
// returned as is and never cached, so the next approval tries both
|
|
5638
|
+
// formats again instead of skipping the simplified fallback for an hour.
|
|
5639
|
+
if (fullResponse.error_code !== "validation_error") {
|
|
5640
|
+
return fullResponse;
|
|
5641
|
+
}
|
|
5564
5642
|
// Full format failed with validation error, try simplified
|
|
5565
5643
|
logger.debug("[ConsentService] Full format failed, trying simplified format...");
|
|
5566
|
-
const simplifiedResponse = await this.makeAPICall(
|
|
5644
|
+
const simplifiedResponse = await this.makeAPICall(checkpointUrl, apiKey, request.simplifiedFormat);
|
|
5567
5645
|
if (simplifiedResponse.success) {
|
|
5568
5646
|
await this.cacheFormatPreference("simplified");
|
|
5569
5647
|
}
|
|
5570
5648
|
return simplifiedResponse;
|
|
5571
5649
|
}
|
|
5572
5650
|
// Direct call (format already determined)
|
|
5573
|
-
return this.makeAPICall(
|
|
5651
|
+
return this.makeAPICall(checkpointUrl, apiKey, request);
|
|
5574
5652
|
}
|
|
5575
5653
|
/**
|
|
5576
5654
|
* Make API call and parse response
|
|
5577
5655
|
*
|
|
5578
|
-
* CRITICAL: This method ensures session_id and project_id are never sent to
|
|
5656
|
+
* CRITICAL: This method ensures session_id and project_id are never sent to Checkpoint.
|
|
5579
5657
|
* These fields are NOT part of the createDelegationSchema and will cause validation errors.
|
|
5580
5658
|
*/
|
|
5581
|
-
async makeAPICall(
|
|
5659
|
+
async makeAPICall(checkpointUrl, apiKey, requestBody) {
|
|
5582
5660
|
try {
|
|
5583
5661
|
// FINAL SAFEGUARD: Remove session_id and project_id if they somehow got added
|
|
5584
5662
|
// This provides defense-in-depth protection
|
|
@@ -5591,7 +5669,7 @@ export class ConsentService {
|
|
|
5591
5669
|
logger.warn("[ConsentService] ⚠️ project_id detected in request body - removing (not in schema)");
|
|
5592
5670
|
delete sanitizedBody.project_id;
|
|
5593
5671
|
}
|
|
5594
|
-
const response = await fetch(`${
|
|
5672
|
+
const response = await fetch(`${checkpointUrl}${CHECKPOINT_ENDPOINTS.DELEGATIONS_CREATE}`, {
|
|
5595
5673
|
method: "POST",
|
|
5596
5674
|
headers: {
|
|
5597
5675
|
Authorization: `Bearer ${apiKey}`,
|