@inneranimalmedia/agentsam-sdk 2.6.3 → 2.6.4
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/AGENTSAM.md +6 -0
- package/README.md +16 -1
- package/bin/agentsam +16 -1
- package/docs/BRAND_INTELLIGENCE.md +1 -1
- package/docs/architecture/AGENTSAM_DISTRIBUTION_OWNERSHIP.md +176 -0
- package/docs/architecture/AGENTSAM_GO_RUNTIME.md +282 -0
- package/docs/architecture/CODEBASEINDEX_GUIDED_PATH.md +202 -0
- package/docs/architecture/FS_E2E_CLOSURE_RECEIPT.md +59 -0
- package/docs/architecture/LOCAL_FS_AUTHORITY.md +38 -0
- package/docs/architecture/LOCAL_STUDIO_CLOUDFLARE_OAUTH.md +28 -0
- package/docs/architecture/PLAN_CLI_AND_LOCAL_STUDIO_DESKTOP.md +96 -0
- package/docs/architecture/SAM_ACTIVITY_RECOVERY_RECEIPT.md +39 -0
- package/docs/architecture/SAM_DECISION_WORK_RECEIPT.md +54 -0
- package/docs/architecture/SAM_KERNEL.md +181 -0
- package/docs/architecture/SAM_MACHINE_NORMALIZATION_PRECOMMIT_REPORT.md +208 -0
- package/docs/architecture/SLASH_SKILLS_PORTABLE.md +116 -0
- package/docs/architecture/fs-e2e-receipt.latest.json +42 -0
- package/docs/architecture/previews/codebaseindex-guided-path/CODEBASEINDEX_GUIDED_PATH.md +202 -0
- package/docs/architecture/previews/codebaseindex-guided-path/index.html +321 -0
- package/migrations/d1/0010_portable_tickets_memory.sql +140 -0
- package/migrations/d1/0011_agentsam_skill_v2.sql +185 -0
- package/migrations/d1/0011b_agentsam_skill_v2_cutover.sql +22 -0
- package/migrations/d1/0011c_agentsam_skill_v2_backfill.sql +76 -0
- package/migrations/d1/0011d_agentsam_skill_v2_retrieval_revisions.sql +50 -0
- package/migrations/d1/0012_agentsam_tools_required_seed.sql +67 -0
- package/migrations/d1/0013_identity_oauth_states.sql +15 -0
- package/migrations/d1/0014_auth_event_log.sql +20 -0
- package/migrations/d1/0015_identity_oauth_state_app_id.sql +3 -0
- package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +16 -0
- package/migrations/sqlite/agentsam_skill_retrieval.portable.sql +42 -0
- package/package.json +19 -4
- package/packages/agentsam-contracts/src/errors.ts +16 -0
- package/packages/agentsam-errors/src/envelope.js +53 -0
- package/packages/agentsam-errors/src/index.js +1 -0
- package/packages/agentsam-errors/src/recovery.js +301 -0
- package/packages/agentsam-knowledge/src/providers/index.js +18 -6
- package/packages/connectors/cloudflare/src/routes.js +9 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +26 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/routes.json +11 -2
- package/packages/identity/docs/PORTABLE_IDENTITY_ARCHITECTURE.md +50 -0
- package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +31 -0
- package/packages/identity/migrations/sqlite/001_identity_core.sql +99 -0
- package/packages/identity/migrations/sqlite/002_identity_oauth_client.sql +38 -0
- package/packages/identity/migrations/sqlite/003_identity_oauth_server.sql +64 -0
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/adapters/cloudflare-d1/index.js +122 -22
- package/packages/identity/src/adapters/sqlite/index.js +319 -0
- package/packages/identity/src/app/verify-app.js +95 -0
- package/packages/identity/src/contracts/identity-store.js +115 -0
- package/packages/identity/src/contracts/route-ids.js +30 -0
- package/packages/identity/src/contracts/route-projection.js +218 -0
- package/packages/identity/src/contracts/routes.js +11 -0
- package/packages/identity/src/core/browser-paths.js +4 -5
- package/packages/identity/src/core/constants.js +13 -8
- package/packages/identity/src/core/session-policy.js +32 -0
- package/packages/identity/src/frontend/auth-portal/pages/login.html +10 -10
- package/packages/identity/src/frontend/auth-portal/pages/reset.html +3 -3
- package/packages/identity/src/frontend/auth-portal/pages/signup.html +3 -3
- package/packages/identity/src/frontend/auth-portal/preview/dashboard-stub.html +1 -1
- package/packages/identity/src/index.js +25 -0
- package/packages/identity/src/oauth/README.md +10 -4
- package/packages/identity/src/oauth/credentials.js +20 -13
- package/packages/identity/src/oauth/finalize-inbound.js +1 -1
- package/packages/identity/src/oauth/iam-platform.js +8 -7
- package/packages/identity/src/oauth/redirect-paths.js +27 -36
- package/packages/identity/src/server/identity-service.js +30 -13
- package/packages/identity/src/server/mount-policy.js +30 -0
- package/packages/identity/src/server/post-auth.js +79 -0
- package/packages/identity/src/server/worker-router.js +104 -72
- package/packages/identity/tests/finalize-inbound-oauth.test.mjs +6 -6
- package/packages/identity/tests/iam-provider.test.mjs +1 -1
- package/packages/identity/tests/identity-service.test.mjs +36 -5
- package/packages/identity/tests/oauth-credentials.test.mjs +3 -1
- package/packages/identity/tests/portable-identity-architecture.test.mjs +157 -0
- package/packages/identity/tests/session-routes-policy.test.mjs +21 -0
- package/packages/theme-church-site/package.json +2 -1
- package/packages/theme-church-site/src/index.js +1 -0
- package/packages/theme-companions-site/package.json +2 -1
- package/packages/theme-companions-site/src/index.js +1 -0
- package/packages/theme-floors-site/package.json +2 -1
- package/packages/theme-floors-site/src/index.js +1 -0
- package/packages/theme-fuelnfree-site/package.json +2 -1
- package/packages/theme-fuelnfree-site/src/index.js +1 -0
- package/packages/theme-handyman-site/package.json +2 -1
- package/packages/theme-handyman-site/src/index.js +1 -0
- package/packages/theme-insurance-site/package.json +2 -1
- package/packages/theme-insurance-site/src/index.js +1 -0
- package/packages/theme-shinshu-site/package.json +2 -1
- package/packages/theme-shinshu-site/src/index.js +1 -0
- package/protocol/apps/agentsam.app.v1.schema.json +51 -0
- package/protocol/brand/brandpack.v1.schema.json +43 -0
- package/protocol/credentials/issue.v1.schema.json +38 -0
- package/protocol/database/connection.v1.schema.json +41 -0
- package/protocol/embeddings/embedding-profile.v1.schema.json +20 -0
- package/protocol/errors/error-envelope.schema.json +135 -1
- package/protocol/errors/recovery.v1.schema.json +50 -0
- package/protocol/runtime/workspace-fs.v1.schema.json +71 -0
- package/protocol/sam/activity.v1.schema.json +48 -0
- package/protocol/sam/answer.v1.schema.json +35 -0
- package/protocol/sam/calibration.v1.schema.json +21 -0
- package/protocol/sam/decision-receipt.v1.schema.json +29 -0
- package/protocol/sam/evaluation.v1.schema.json +19 -0
- package/protocol/sam/operation.schema.json +66 -0
- package/protocol/sam/outcome.v1.schema.json +36 -0
- package/protocol/sam/question.v1.schema.json +26 -0
- package/protocol/sam/registry.seed.json +153 -0
- package/protocol/sam/result.schema.json +53 -0
- package/protocol/sam/state.v1.schema.json +23 -0
- package/protocol/skills/agentsam.interaction.v1.schema.json +50 -0
- package/protocol/skills/agentsam.skill.v1.schema.json +57 -0
- package/protocol/ui/icon-registry.mjs +226 -0
- package/protocol/ui/icon.v1.schema.json +51 -0
- package/skills/README.md +22 -9
- package/skills/agentsam-codebaseindex/SKILL.md +225 -0
- package/skills/catalog.json +14 -0
- package/src/cli/command-catalog.js +130 -0
- package/src/cli/dispatch.js +48 -0
- package/src/cli.js +43 -1
- package/src/commands/api-key.js +244 -0
- package/src/commands/app.js +60 -28
- package/src/commands/brand.js +17 -19
- package/src/commands/codebaseindex.js +688 -0
- package/src/commands/env.js +152 -25
- package/src/commands/go.js +366 -53
- package/src/commands/interaction-clack.js +115 -0
- package/src/commands/models.js +1 -1
- package/src/commands/providers.js +62 -14
- package/src/commands/shell.js +43 -2
- package/src/commands/skill.js +248 -0
- package/src/commands/skills.js +1 -1
- package/src/commands/start-local.js +4 -0
- package/src/commands/whoami.js +90 -18
- package/src/go/build.js +229 -39
- package/src/go/cloudflare.js +506 -105
- package/src/go/container.js +120 -0
- package/src/go/contract.js +9 -4
- package/src/go/discover.js +149 -33
- package/src/go/index.js +15 -3
- package/src/go/native-probe-runner.mjs +119 -0
- package/src/go/{registry.js → official-registry.js} +64 -7
- package/src/go/official-release.js +10 -0
- package/src/go/receipts.js +77 -10
- package/src/go/verify.js +9 -2
- package/src/index.js +27 -0
- package/src/indexing/ingest/discover-models.js +298 -0
- package/src/indexing/ingest/inventory.js +243 -0
- package/src/indexing/ingest/job-graph.js +181 -0
- package/src/indexing/ingest/materials.js +210 -0
- package/src/lib/provider-credentials.js +63 -21
- package/src/lib/slash-commands.js +1 -0
- package/src/local-fs/capability.js +121 -0
- package/src/local-fs/freshness.js +75 -0
- package/src/local-fs/index.js +385 -0
- package/src/local-fs/paths.js +100 -0
- package/src/local-pty/server.js +295 -19
- package/src/mcp/client.js +2 -2
- package/src/models/ai-access-onboarding.js +112 -0
- package/src/models/discovery.js +10 -2
- package/src/models/inventory-core.js +9 -1
- package/src/sam/activity/index.js +183 -0
- package/src/sam/client.js +252 -0
- package/src/sam/decision/calibration.js +109 -0
- package/src/sam/decision/confidence.js +126 -0
- package/src/sam/decision/evaluate.js +157 -0
- package/src/sam/decision/evaluators/deterministic.js +341 -0
- package/src/sam/decision/evaluators/heuristic.js +61 -0
- package/src/sam/decision/evaluators/select.js +50 -0
- package/src/sam/decision/evaluators/semantic.js +149 -0
- package/src/sam/decision/hierarchical.js +61 -0
- package/src/sam/decision/index.js +53 -0
- package/src/sam/decision/policy.js +86 -0
- package/src/sam/decision/questions.js +120 -0
- package/src/sam/decision/receipt.js +148 -0
- package/src/sam/decision/state.js +117 -0
- package/src/sam/decision/types.js +22 -0
- package/src/sam/decision/validate.js +200 -0
- package/src/sam/define.js +51 -0
- package/src/sam/index.js +65 -0
- package/src/sam/operations/brand-scan.js +62 -0
- package/src/sam/operations/cad-blender-inspect.js +36 -0
- package/src/sam/operations/codebaseindex-ingest.js +49 -0
- package/src/sam/operations/decision-evaluate.js +59 -0
- package/src/sam/operations/planning-astar.js +77 -0
- package/src/sam/operations/planning-goap.js +60 -0
- package/src/sam/operations/repository-inspect.js +72 -0
- package/src/sam/operations/security-scan.js +33 -0
- package/src/sam/operations/terminal-exec.js +29 -0
- package/src/sam/planning/astar.js +311 -0
- package/src/sam/planning/goap.js +177 -0
- package/src/sam/planning/index.js +21 -0
- package/src/sam/planning/state.js +84 -0
- package/src/sam/registry.js +48 -0
- package/src/sam/result.js +77 -0
- package/src/sam/seed.js +44 -0
- package/src/sam/types.js +91 -0
- package/src/skills/catalog.js +64 -0
- package/src/skills/content-resolver.js +124 -0
- package/src/skills/hosted-store.js +37 -0
- package/src/skills/index.js +29 -64
- package/src/skills/interaction.js +102 -0
- package/src/skills/local-store.js +228 -0
- package/src/skills/manifest.js +104 -0
- package/src/skills/metrics.js +31 -0
- package/src/skills/registry.js +184 -0
- package/src/skills/runtime.js +287 -0
- package/src/skills/slash.js +44 -0
- package/src/ui/cli/help.js +94 -101
- package/test/cli/api-key-env-whoami.test.mjs +129 -0
- package/test/cli/codebaseindex-plan-ux.test.mjs +37 -0
- package/test/cli/go.test.mjs +62 -5
- package/test/cli/skill-npm-and-env.test.mjs +52 -0
- package/test/cli/wireframes-go-registry.test.mjs +87 -2
- package/test/go/build-source-identity.test.mjs +31 -0
- package/test/go/cloudflare-probe.test.mjs +274 -10
- package/test/go/distribution.test.mjs +28 -0
- package/test/integration/ai-access-onboarding.test.mjs +47 -0
- package/test/integration/cli-help.test.mjs +1 -1
- package/test/integration/cms-site-tenancy-contract.test.mjs +6 -6
- package/test/integration/codebaseindex-ingest.test.mjs +166 -0
- package/test/integration/icon-registry.test.mjs +67 -0
- package/test/integration/ingest-discover-models.test.mjs +30 -0
- package/test/integration/install-script.test.mjs +12 -9
- package/test/integration/local-fs.test.mjs +113 -0
- package/test/integration/provider-env-cli.test.mjs +2 -1
- package/test/integration/sam-activity-recovery.test.mjs +147 -0
- package/test/integration/sam-decision.test.mjs +584 -0
- package/test/integration/sam-kernel.test.mjs +99 -0
- package/test/integration/sam-planning-astar.test.mjs +279 -0
- package/test/integration/skill-runtime.test.mjs +240 -0
- package/test/integration/studio-fs-pty-e2e.test.mjs +294 -0
- package/test/models.test.mjs +14 -7
- package/test/shell.test.mjs +4 -4
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Post-OAuth redirect helpers — portable path policy with host injectables.
|
|
3
|
+
* Hosts MUST pass loginPath from their route projection (never hardcode).
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
import { sanitizeBrowserNextPath } from '../core/browser-paths.js';
|
|
7
|
+
import { IdentityRoutingError } from '../contracts/identity-store.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* @typedef {object} OAuthRedirectPathOptions
|
|
9
11
|
* @property {string} [authCookieName]
|
|
10
|
-
* @property {string}
|
|
12
|
+
* @property {string} loginPath concrete projection of identity.login
|
|
13
|
+
* @property {string} [fallback] only used when isAllowedLoginResumePath rejects; prefer omit
|
|
11
14
|
* @property {(path: string) => boolean} [isAllowedLoginResumePath]
|
|
12
15
|
* @property {(env: unknown, rawCookie: string) => Promise<string|null>} [resolveSessionIdFromCookie]
|
|
13
16
|
* @property {(env: unknown, sessionId: string, reason: string, userId: string|null) => Promise<void>} [revokeAuthSession]
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
|
-
/**
|
|
17
|
-
* @param {OAuthRedirectPathOptions} options
|
|
18
|
-
*/
|
|
19
19
|
export function createOAuthRedirectHelpers(options = {}) {
|
|
20
20
|
const authCookieName = options.authCookieName ?? 'iam_session';
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const loginPath = options.loginPath;
|
|
22
|
+
if (!loginPath || !String(loginPath).startsWith('/')) {
|
|
23
|
+
throw new IdentityRoutingError(
|
|
24
|
+
'AUTH_ROUTE_MISSING',
|
|
25
|
+
'createOAuthRedirectHelpers requires loginPath from route projection',
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const isAllowedLoginResumePath = options.isAllowedLoginResumePath
|
|
29
|
+
?? ((path) => Boolean(sanitizeBrowserNextPath(path)));
|
|
23
30
|
const resolveSessionIdFromCookie = options.resolveSessionIdFromCookie;
|
|
24
31
|
const revokeAuthSession = options.revokeAuthSession;
|
|
25
32
|
|
|
26
|
-
/** Revoke browser cookie session before issuing a new login session. */
|
|
27
33
|
async function revokeIncomingCookieSession(request, env, reason = 'oauth_login_replaced') {
|
|
28
34
|
const cookie = request.headers.get('Cookie') || '';
|
|
29
35
|
const match = cookie.match(new RegExp(`(?:^|;\\s*)${authCookieName}=([^;]+)`));
|
|
@@ -45,48 +51,33 @@ export function createOAuthRedirectHelpers(options = {}) {
|
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (t.startsWith('/') && !t.startsWith('//') && !t.includes('://')) {
|
|
53
|
-
if (isAllowedLoginResumePath(t)) return t;
|
|
54
|
-
if (t.startsWith('/dashboard/settings/integrations')) return dashboardFallback;
|
|
55
|
-
if (!t.startsWith('/dashboard')) return dashboardFallback;
|
|
56
|
-
return t;
|
|
54
|
+
function safeLoginRedirectPath(_originBase, returnTo) {
|
|
55
|
+
const cleaned = sanitizeBrowserNextPath(returnTo);
|
|
56
|
+
if (!cleaned) {
|
|
57
|
+
throw new IdentityRoutingError('AUTH_DESTINATION_UNRESOLVED', 'return_to missing/invalid');
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const ob = new URL(originBase);
|
|
61
|
-
if (u.origin !== ob.origin) return dashboardFallback;
|
|
62
|
-
const p = u.pathname + (u.search || '');
|
|
63
|
-
if (p.startsWith('/dashboard/settings/integrations')) return dashboardFallback;
|
|
64
|
-
if (!p.startsWith('/dashboard')) return dashboardFallback;
|
|
65
|
-
return p;
|
|
66
|
-
} catch {
|
|
67
|
-
return dashboardFallback;
|
|
59
|
+
if (!isAllowedLoginResumePath(cleaned)) {
|
|
60
|
+
throw new IdentityRoutingError('AUTH_DESTINATION_UNRESOLVED', 'return_to not allowed', { returnTo: cleaned });
|
|
68
61
|
}
|
|
62
|
+
return cleaned;
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
function oauthPostLoginGlobeRedirectUrl(originBase, returnToFullUrl) {
|
|
72
|
-
let path
|
|
66
|
+
let path;
|
|
73
67
|
try {
|
|
74
68
|
const u = new URL(returnToFullUrl);
|
|
75
69
|
path = u.pathname + (u.search || '');
|
|
76
70
|
} catch {
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
if (!path.startsWith('/') || path.startsWith('//')) path = dashboardFallback;
|
|
80
|
-
if (path.startsWith('/dashboard/settings/integrations')) path = dashboardFallback;
|
|
81
|
-
if (!isAllowedLoginResumePath(path) && !path.startsWith('/dashboard')) {
|
|
82
|
-
path = dashboardFallback;
|
|
71
|
+
throw new IdentityRoutingError('AUTH_DESTINATION_UNRESOLVED', 'invalid returnToFullUrl');
|
|
83
72
|
}
|
|
84
|
-
|
|
73
|
+
path = safeLoginRedirectPath(originBase, path);
|
|
74
|
+
return `${originBase}${loginPath}?globe_exit=1&next=${encodeURIComponent(path)}`;
|
|
85
75
|
}
|
|
86
76
|
|
|
87
77
|
return {
|
|
88
78
|
revokeIncomingCookieSession,
|
|
89
|
-
safeDashboardLoginRedirectPath,
|
|
79
|
+
safeDashboardLoginRedirectPath: safeLoginRedirectPath,
|
|
80
|
+
safeLoginRedirectPath,
|
|
90
81
|
oauthPostLoginGlobeRedirectUrl,
|
|
91
82
|
};
|
|
92
83
|
}
|
|
@@ -1,33 +1,48 @@
|
|
|
1
|
-
import { AUTH_COOKIE_NAME,
|
|
1
|
+
import { AUTH_COOKIE_NAME, SESSION_POLICY } from '../core/constants.js';
|
|
2
2
|
import { hashPassword, verifyPassword } from '../core/password-crypto.js';
|
|
3
3
|
import { jsonResponse } from '../core/http-json.js';
|
|
4
|
-
import {
|
|
4
|
+
import { resolvePostAuthDestination } from './post-auth.js';
|
|
5
|
+
import { IdentityRoutingError } from '../contracts/identity-store.js';
|
|
5
6
|
import { newAuthUserId } from '../adapters/cloudflare-d1/ids.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* @typedef {
|
|
9
|
+
* @typedef {import('../contracts/identity-store.js').IdentityStore} IdentityStore
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Server-side identity orchestration (Worker / Node). No secrets in client bundle.
|
|
13
|
-
*
|
|
14
|
+
* Hosts must supply `app` + `routeRegistry` — no platform "/" fallback.
|
|
15
|
+
*
|
|
16
|
+
* @param {{
|
|
17
|
+
* adapter: IdentityStore,
|
|
18
|
+
* cookieName?: string,
|
|
19
|
+
* app: { id: string },
|
|
20
|
+
* routeRegistry: ReturnType<import('../contracts/route-projection.js').createRouteRegistry>,
|
|
21
|
+
* }} config
|
|
14
22
|
*/
|
|
15
23
|
export function createIdentityService(config) {
|
|
16
24
|
const adapter = config.adapter;
|
|
17
25
|
if (!adapter) throw new Error('identity_service_requires_adapter');
|
|
18
26
|
const cookieName = config.cookieName || AUTH_COOKIE_NAME;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
const app = config.app || null;
|
|
28
|
+
const routeRegistry = config.routeRegistry || null;
|
|
29
|
+
if (!app?.id || !routeRegistry) {
|
|
30
|
+
throw new IdentityRoutingError(
|
|
31
|
+
'AUTH_APP_UNRESOLVED',
|
|
32
|
+
'createIdentityService requires app + routeRegistry (no platform fallback)',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
routeRegistry.assertAuthCapable(app.id);
|
|
22
36
|
|
|
23
|
-
// OAuth state is durable and may outlive a deployment. Normalize both the
|
|
24
|
-
// incoming request value and a value read back from prior state so an old or
|
|
25
|
-
// malformed value can never become an open redirect.
|
|
26
37
|
function resolvePostLoginPath(nextPath) {
|
|
27
|
-
return
|
|
38
|
+
return resolvePostAuthDestination({
|
|
39
|
+
transaction: { return_to: nextPath, app_id: app.id },
|
|
40
|
+
app,
|
|
41
|
+
routeRegistry,
|
|
42
|
+
});
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
function sessionCookieHeader(sessionId, requestUrl, maxAge =
|
|
45
|
+
function sessionCookieHeader(sessionId, requestUrl, maxAge = SESSION_POLICY.browser.ttlSeconds) {
|
|
31
46
|
const secure = new URL(requestUrl).protocol === 'https:';
|
|
32
47
|
const parts = [
|
|
33
48
|
`${cookieName}=${sessionId}`,
|
|
@@ -48,7 +63,9 @@ export function createIdentityService(config) {
|
|
|
48
63
|
|
|
49
64
|
return Object.freeze({
|
|
50
65
|
cookieName,
|
|
51
|
-
|
|
66
|
+
app,
|
|
67
|
+
routeRegistry,
|
|
68
|
+
defaultRedirect: resolvePostLoginPath(null),
|
|
52
69
|
|
|
53
70
|
async signup({ email, password, displayName }) {
|
|
54
71
|
const normalized = String(email || '').trim().toLowerCase();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount-level auth policy helpers (host applies; identity does not invent mounts).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ path: string, auth?: 'required'|'optional'|'public', spa?: boolean, shell?: string }} AppMount
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function pathMatchesMount(pathname, mount) {
|
|
10
|
+
const base = String(mount?.path || '');
|
|
11
|
+
if (!base) return false;
|
|
12
|
+
const path = String(pathname || '');
|
|
13
|
+
return path === base || path.startsWith(`${base}/`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveMountForPath(pathname, app) {
|
|
17
|
+
const mounts = Array.isArray(app?.routes?.mounts) ? app.routes.mounts : [];
|
|
18
|
+
let best = null;
|
|
19
|
+
for (const mount of mounts) {
|
|
20
|
+
if (!pathMatchesMount(pathname, mount)) continue;
|
|
21
|
+
if (!best || String(mount.path).length > String(best.path).length) best = mount;
|
|
22
|
+
}
|
|
23
|
+
return best;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function mountRequiresAuth(pathname, app) {
|
|
27
|
+
const mount = resolveMountForPath(pathname, app);
|
|
28
|
+
if (!mount) return false;
|
|
29
|
+
return (mount.auth || 'public') === 'required';
|
|
30
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed post-auth destination resolver.
|
|
3
|
+
* Never redirects to "/" / marketing / invented dashboard paths.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IDENTITY_ROUTE_IDS } from '../contracts/route-ids.js';
|
|
7
|
+
import { IdentityRoutingError } from '../contracts/identity-store.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{ app_id?: string|null, return_to?: string|null }} AuthTransaction
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{
|
|
15
|
+
* transaction?: AuthTransaction|null,
|
|
16
|
+
* app?: { id: string }|null,
|
|
17
|
+
* routeRegistry: ReturnType<import('../contracts/route-projection.js').createRouteRegistry>,
|
|
18
|
+
* }} opts
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function resolvePostAuthDestination({ transaction, app, routeRegistry }) {
|
|
22
|
+
if (!routeRegistry) {
|
|
23
|
+
throw new IdentityRoutingError('AUTH_ROUTE_REGISTRY_REQUIRED');
|
|
24
|
+
}
|
|
25
|
+
if (!app?.id) {
|
|
26
|
+
throw new IdentityRoutingError('AUTH_APP_UNRESOLVED');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
routeRegistry.assertAuthCapable(app.id);
|
|
30
|
+
|
|
31
|
+
const returnTo = routeRegistry.resolveReturnTo({
|
|
32
|
+
appId: app.id,
|
|
33
|
+
value: transaction?.return_to,
|
|
34
|
+
});
|
|
35
|
+
if (returnTo) return returnTo;
|
|
36
|
+
|
|
37
|
+
const entry = routeRegistry.resolve(app.id, IDENTITY_ROUTE_IDS.APP_AUTHENTICATED)
|
|
38
|
+
|| routeRegistry.resolve(app.id, IDENTITY_ROUTE_IDS.APP_HOME);
|
|
39
|
+
if (entry) return entry;
|
|
40
|
+
|
|
41
|
+
const recovery = routeRegistry.resolve(app.id, IDENTITY_ROUTE_IDS.RECOVERY)
|
|
42
|
+
|| routeRegistry.resolve(app.id, IDENTITY_ROUTE_IDS.LOGIN);
|
|
43
|
+
if (recovery) {
|
|
44
|
+
return `${recovery}?error=missing_authenticated_entry`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new IdentityRoutingError('AUTH_DESTINATION_UNRESOLVED', undefined, {
|
|
48
|
+
appId: app.id,
|
|
49
|
+
returnTo: transaction?.return_to || null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Begin login redirect URL for an unauthenticated request into an app mount.
|
|
55
|
+
* @param {{
|
|
56
|
+
* appId: string,
|
|
57
|
+
* returnTo: string,
|
|
58
|
+
* routeRegistry: ReturnType<import('../contracts/route-projection.js').createRouteRegistry>,
|
|
59
|
+
* origin: string,
|
|
60
|
+
* }} opts
|
|
61
|
+
*/
|
|
62
|
+
export function beginLoginRedirect({ appId, returnTo, routeRegistry, origin }) {
|
|
63
|
+
routeRegistry.assertAuthCapable(appId);
|
|
64
|
+
const login = routeRegistry.resolve(appId, IDENTITY_ROUTE_IDS.LOGIN);
|
|
65
|
+
if (!login) {
|
|
66
|
+
throw new IdentityRoutingError('AUTH_ROUTE_MISSING', 'identity.login projection missing', { appId });
|
|
67
|
+
}
|
|
68
|
+
const safeReturn = routeRegistry.resolveReturnTo({ appId, value: returnTo }) || '';
|
|
69
|
+
const url = new URL(login, origin);
|
|
70
|
+
if (safeReturn) url.searchParams.set('next', safeReturn);
|
|
71
|
+
return url.toString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @deprecated Mount helpers moved with host registry — kept for host gate wiring. */
|
|
75
|
+
export {
|
|
76
|
+
pathMatchesMount,
|
|
77
|
+
resolveMountForPath,
|
|
78
|
+
mountRequiresAuth,
|
|
79
|
+
} from './mount-policy.js';
|
|
@@ -9,7 +9,8 @@ import { getGithubAuthUrl, exchangeGithubCode } from '../providers/github/oauth.
|
|
|
9
9
|
import { fetchGithubProfile } from '../providers/github/profile.js';
|
|
10
10
|
import { getCloudflareAuthUrl, exchangeCloudflareCode } from '../providers/cloudflare/oauth.js';
|
|
11
11
|
import { fetchCloudflareProfile } from '../providers/cloudflare/profile.js';
|
|
12
|
-
import {
|
|
12
|
+
import { IDENTITY_ROUTE_IDS } from '../contracts/route-ids.js';
|
|
13
|
+
import { IdentityRoutingError } from '../contracts/identity-store.js';
|
|
13
14
|
import { resolveOAuthCredentialLane } from '../oauth/credentials.js';
|
|
14
15
|
import { iamPlatformOAuthCallback, iamPlatformOAuthStart } from '../oauth/iam-platform.js';
|
|
15
16
|
import { pkceChallenge, pkceVerifier, randomOAuthState } from '../oauth/pkce.js';
|
|
@@ -80,7 +81,22 @@ export async function handleIdentityWorkerRequest(request, env, options = {}) {
|
|
|
80
81
|
const method = request.method.toUpperCase();
|
|
81
82
|
|
|
82
83
|
const adapter = createCloudflareD1Adapter(env.DB);
|
|
83
|
-
|
|
84
|
+
if (!options.identity && (!options.app?.id || !options.routeRegistry)) {
|
|
85
|
+
throw new IdentityRoutingError(
|
|
86
|
+
'AUTH_APP_UNRESOLVED',
|
|
87
|
+
'handleIdentityWorkerRequest requires options.app + options.routeRegistry',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const identity = options.identity || createIdentityService({
|
|
91
|
+
adapter,
|
|
92
|
+
app: options.app,
|
|
93
|
+
routeRegistry: options.routeRegistry,
|
|
94
|
+
});
|
|
95
|
+
const loginPath = () => identity.routeRegistry.resolve(identity.app.id, IDENTITY_ROUTE_IDS.LOGIN);
|
|
96
|
+
const signupPath = () => identity.routeRegistry.resolve(identity.app.id, IDENTITY_ROUTE_IDS.SIGNUP)
|
|
97
|
+
|| loginPath();
|
|
98
|
+
const resetPath = () => identity.routeRegistry.resolve(identity.app.id, IDENTITY_ROUTE_IDS.RESET)
|
|
99
|
+
|| loginPath();
|
|
84
100
|
const passwordReset = buildPasswordResetService(env, adapter, options);
|
|
85
101
|
|
|
86
102
|
// ── API: email auth ─────────────────────────────────────────────────────
|
|
@@ -198,43 +214,48 @@ export async function handleIdentityWorkerRequest(request, env, options = {}) {
|
|
|
198
214
|
if (!result.ok) {
|
|
199
215
|
return jsonResponse({ error: result.error }, result.status || 400);
|
|
200
216
|
}
|
|
201
|
-
return jsonResponse({ ok: true, redirect: `${
|
|
217
|
+
return jsonResponse({ ok: true, redirect: `${loginPath()}?reset=success` });
|
|
202
218
|
}
|
|
203
219
|
|
|
204
220
|
// ── OAuth ────────────────────────────────────────────────────────────────
|
|
205
221
|
// Default: IAM_CLIENT_* (minted). Developer BYOK GOOGLE_*/GITHUB_* take the
|
|
206
222
|
// matching /api/oauth/{provider}/start button when set.
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
// Canonical platform id = inneranimalmedia (legacy /api/oauth/iam/* still accepted).
|
|
224
|
+
if (path === '/api/oauth/inneranimalmedia/callback' || path === '/api/oauth/iam/callback') {
|
|
225
|
+
if (method === 'GET') {
|
|
226
|
+
return iamPlatformOAuthCallback(request, env, adapter, identity);
|
|
227
|
+
}
|
|
209
228
|
}
|
|
210
|
-
if (path === '/api/oauth/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
229
|
+
if (path === '/api/oauth/inneranimalmedia/start' || path === '/api/oauth/iam/start') {
|
|
230
|
+
if (method === 'GET') {
|
|
231
|
+
const lane = resolveOAuthCredentialLane(env, 'inneranimalmedia');
|
|
232
|
+
if (!lane) return jsonResponse({ ok: false, error: 'inneranimalmedia_oauth_not_configured' }, 503);
|
|
233
|
+
return iamPlatformOAuthStart(request, env, adapter, identity);
|
|
234
|
+
}
|
|
214
235
|
}
|
|
215
236
|
|
|
216
237
|
if (path === '/api/oauth/google/start' && method === 'GET') {
|
|
217
238
|
const lane = resolveOAuthCredentialLane(env, 'google');
|
|
218
239
|
if (!lane) return jsonResponse({ ok: false, error: 'google_oauth_not_configured' }, 503);
|
|
219
240
|
if (lane.lane === 'iam_platform') return iamPlatformOAuthStart(request, env, adapter, identity);
|
|
220
|
-
return oauthStart(request, env, adapter, 'google', lane);
|
|
241
|
+
return oauthStart(request, env, identity, adapter, 'google', lane);
|
|
221
242
|
}
|
|
222
243
|
if (path === '/api/oauth/github/start' && method === 'GET') {
|
|
223
244
|
const lane = resolveOAuthCredentialLane(env, 'github');
|
|
224
245
|
if (!lane) return jsonResponse({ ok: false, error: 'github_oauth_not_configured' }, 503);
|
|
225
246
|
if (lane.lane === 'iam_platform') return iamPlatformOAuthStart(request, env, adapter, identity);
|
|
226
|
-
return oauthStart(request, env, adapter, 'github', lane);
|
|
247
|
+
return oauthStart(request, env, identity, adapter, 'github', lane);
|
|
227
248
|
}
|
|
228
249
|
if (path === '/api/oauth/cloudflare/start' && method === 'GET') {
|
|
229
250
|
const lane = resolveOAuthCredentialLane(env, 'cloudflare');
|
|
230
251
|
if (!lane) return jsonResponse({ ok: false, error: 'cloudflare_oauth_not_configured' }, 503);
|
|
231
|
-
return oauthStart(request, env, adapter, 'cloudflare', lane);
|
|
252
|
+
return oauthStart(request, env, identity, adapter, 'cloudflare', lane);
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
if (path === '/api/oauth/google/callback' && method === 'GET') {
|
|
235
256
|
const lane = resolveOAuthCredentialLane(env, 'google');
|
|
236
257
|
if (!lane) {
|
|
237
|
-
return Response.redirect(`${url.origin}${
|
|
258
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=oauth_not_configured`, 302);
|
|
238
259
|
}
|
|
239
260
|
if (lane.lane === 'iam_platform') {
|
|
240
261
|
return iamPlatformOAuthCallback(request, env, adapter, identity);
|
|
@@ -244,7 +265,7 @@ export async function handleIdentityWorkerRequest(request, env, options = {}) {
|
|
|
244
265
|
if (path === '/api/oauth/github/callback' && method === 'GET') {
|
|
245
266
|
const lane = resolveOAuthCredentialLane(env, 'github');
|
|
246
267
|
if (!lane) {
|
|
247
|
-
return Response.redirect(`${url.origin}${
|
|
268
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=oauth_not_configured`, 302);
|
|
248
269
|
}
|
|
249
270
|
if (lane.lane === 'iam_platform') {
|
|
250
271
|
return iamPlatformOAuthCallback(request, env, adapter, identity);
|
|
@@ -254,37 +275,28 @@ export async function handleIdentityWorkerRequest(request, env, options = {}) {
|
|
|
254
275
|
if (path === '/api/oauth/cloudflare/callback' && method === 'GET') {
|
|
255
276
|
const lane = resolveOAuthCredentialLane(env, 'cloudflare');
|
|
256
277
|
if (!lane) {
|
|
257
|
-
return Response.redirect(`${url.origin}${
|
|
278
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=oauth_not_configured`, 302);
|
|
258
279
|
}
|
|
259
280
|
return oauthCallback(request, env, identity, adapter, 'cloudflare', lane);
|
|
260
281
|
}
|
|
261
282
|
|
|
262
|
-
// Auth HTML shells —
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
283
|
+
// Auth HTML shells — identity pages only (Worker ASSETS binding, not R2).
|
|
284
|
+
const authPages = new Set([
|
|
285
|
+
loginPath(),
|
|
286
|
+
signupPath(),
|
|
287
|
+
resetPath(),
|
|
288
|
+
]);
|
|
268
289
|
|
|
269
|
-
if (method === 'GET' &&
|
|
290
|
+
if (method === 'GET' && authPages.has(path)) {
|
|
270
291
|
if (env.ASSETS?.fetch) {
|
|
271
|
-
const assetUrl = new URL(
|
|
292
|
+
const assetUrl = new URL(path, url.origin);
|
|
272
293
|
return env.ASSETS.fetch(new Request(assetUrl, request));
|
|
273
294
|
}
|
|
274
295
|
return jsonResponse({ error: 'assets_binding_required', path }, 500);
|
|
275
296
|
}
|
|
276
297
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (!ctx) {
|
|
280
|
-
const next = encodeURIComponent(path + url.search);
|
|
281
|
-
return Response.redirect(`${url.origin}${AUTH_LOGIN_PATH}?next=${next}`, 302);
|
|
282
|
-
}
|
|
283
|
-
if (env.ASSETS?.fetch) {
|
|
284
|
-
const assetUrl = new URL('/dashboard/index.html', url.origin);
|
|
285
|
-
return env.ASSETS.fetch(new Request(assetUrl, request));
|
|
286
|
-
}
|
|
287
|
-
}
|
|
298
|
+
// Product SPA mounts (/agentsam, /admin, /cad, …) are owned by the host app
|
|
299
|
+
// registry — identity does not invent or gate those routes here.
|
|
288
300
|
|
|
289
301
|
if (env.ASSETS?.fetch) {
|
|
290
302
|
return env.ASSETS.fetch(request);
|
|
@@ -293,44 +305,64 @@ export async function handleIdentityWorkerRequest(request, env, options = {}) {
|
|
|
293
305
|
return jsonResponse({ error: 'not_found', path }, 404);
|
|
294
306
|
}
|
|
295
307
|
|
|
296
|
-
async function oauthStart(request, env, adapter, provider, creds) {
|
|
308
|
+
async function oauthStart(request, env, identity, adapter, provider, creds) {
|
|
297
309
|
const url = new URL(request.url);
|
|
298
310
|
if (!creds.clientId) {
|
|
299
311
|
return jsonResponse({ ok: false, error: `${provider}_oauth_not_configured` }, 503);
|
|
300
312
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const redirectUri = `${url.origin}/api/oauth/${provider}/callback`;
|
|
310
|
-
let authUrl;
|
|
311
|
-
if (provider === 'google') {
|
|
312
|
-
authUrl = getGoogleAuthUrl({
|
|
313
|
-
clientId: creds.clientId,
|
|
314
|
-
redirectUri,
|
|
313
|
+
try {
|
|
314
|
+
const state = randomOAuthState();
|
|
315
|
+
const codeVerifier = pkceVerifier();
|
|
316
|
+
const codeChallenge = await pkceChallenge(codeVerifier);
|
|
317
|
+
const redirectTo = identity.resolvePostLoginPath(
|
|
318
|
+
url.searchParams.get('next') || url.searchParams.get('return_to'),
|
|
319
|
+
);
|
|
320
|
+
await adapter.saveOAuthState({
|
|
315
321
|
state,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
clientId: creds.clientId,
|
|
321
|
-
redirectUri,
|
|
322
|
-
state,
|
|
323
|
-
codeChallenge,
|
|
324
|
-
});
|
|
325
|
-
} else {
|
|
326
|
-
authUrl = getGithubAuthUrl({
|
|
327
|
-
clientId: creds.clientId,
|
|
328
|
-
redirectUri,
|
|
329
|
-
state,
|
|
330
|
-
codeChallenge,
|
|
322
|
+
provider,
|
|
323
|
+
codeVerifier,
|
|
324
|
+
redirectTo,
|
|
325
|
+
appId: identity.app?.id || null,
|
|
331
326
|
});
|
|
327
|
+
|
|
328
|
+
const redirectUri = `${url.origin}/api/oauth/${provider}/callback`;
|
|
329
|
+
let authUrl;
|
|
330
|
+
if (provider === 'google') {
|
|
331
|
+
authUrl = getGoogleAuthUrl({
|
|
332
|
+
clientId: creds.clientId,
|
|
333
|
+
redirectUri,
|
|
334
|
+
state,
|
|
335
|
+
codeChallenge,
|
|
336
|
+
});
|
|
337
|
+
} else if (provider === 'cloudflare') {
|
|
338
|
+
authUrl = getCloudflareAuthUrl({
|
|
339
|
+
clientId: creds.clientId,
|
|
340
|
+
redirectUri,
|
|
341
|
+
state,
|
|
342
|
+
codeChallenge,
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
authUrl = getGithubAuthUrl({
|
|
346
|
+
clientId: creds.clientId,
|
|
347
|
+
redirectUri,
|
|
348
|
+
state,
|
|
349
|
+
codeChallenge,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return Response.redirect(authUrl, 302);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
const message = String(error?.message || error || 'oauth_start_failed');
|
|
355
|
+
console.error('oauth_start_failed', provider, message);
|
|
356
|
+
// Prefer redirect to login with error over bare Worker 1101 for browsers.
|
|
357
|
+
const lp = identity.routeRegistry.resolve(identity.app.id, IDENTITY_ROUTE_IDS.LOGIN);
|
|
358
|
+
const login = new URL(lp, url.origin);
|
|
359
|
+
login.searchParams.set('error', 'oauth_start_failed');
|
|
360
|
+
login.searchParams.set('provider', provider);
|
|
361
|
+
login.searchParams.set('detail', message.slice(0, 120));
|
|
362
|
+
const next = url.searchParams.get('next') || url.searchParams.get('return_to');
|
|
363
|
+
if (next) login.searchParams.set('next', next);
|
|
364
|
+
return Response.redirect(login.toString(), 302);
|
|
332
365
|
}
|
|
333
|
-
return Response.redirect(authUrl, 302);
|
|
334
366
|
}
|
|
335
367
|
|
|
336
368
|
async function oauthCallback(request, env, identity, adapter, provider, creds) {
|
|
@@ -340,12 +372,12 @@ async function oauthCallback(request, env, identity, adapter, provider, creds) {
|
|
|
340
372
|
const err = url.searchParams.get('error');
|
|
341
373
|
if (err || !code || !state) {
|
|
342
374
|
await adapter.logAuthEvent({ eventType: 'login', status: 'failed', provider, request, metadata: { reason: 'oauth_failed' } });
|
|
343
|
-
return Response.redirect(`${url.origin}${
|
|
375
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=oauth_failed`, 302);
|
|
344
376
|
}
|
|
345
377
|
const saved = await adapter.consumeOAuthState(state);
|
|
346
378
|
if (!saved || saved.provider !== provider) {
|
|
347
379
|
await adapter.logAuthEvent({ eventType: 'login', status: 'failed', provider, request, metadata: { reason: 'state_mismatch' } });
|
|
348
|
-
return Response.redirect(`${url.origin}${
|
|
380
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=state_mismatch`, 302);
|
|
349
381
|
}
|
|
350
382
|
const redirectUri = `${url.origin}/api/oauth/${provider}/callback`;
|
|
351
383
|
let token;
|
|
@@ -359,7 +391,7 @@ async function oauthCallback(request, env, identity, adapter, provider, creds) {
|
|
|
359
391
|
redirectUri,
|
|
360
392
|
});
|
|
361
393
|
if (!token?.access_token) {
|
|
362
|
-
return Response.redirect(`${url.origin}${
|
|
394
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=token_exchange_failed`, 302);
|
|
363
395
|
}
|
|
364
396
|
profile = await fetchGoogleProfile(token.access_token);
|
|
365
397
|
} else if (provider === 'cloudflare') {
|
|
@@ -371,7 +403,7 @@ async function oauthCallback(request, env, identity, adapter, provider, creds) {
|
|
|
371
403
|
redirectUri,
|
|
372
404
|
});
|
|
373
405
|
if (!token?.access_token) {
|
|
374
|
-
return Response.redirect(`${url.origin}${
|
|
406
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=token_exchange_failed`, 302);
|
|
375
407
|
}
|
|
376
408
|
profile = await fetchCloudflareProfile(token.access_token);
|
|
377
409
|
} else {
|
|
@@ -383,12 +415,12 @@ async function oauthCallback(request, env, identity, adapter, provider, creds) {
|
|
|
383
415
|
redirectUri,
|
|
384
416
|
});
|
|
385
417
|
if (!token?.access_token) {
|
|
386
|
-
return Response.redirect(`${url.origin}${
|
|
418
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=token_exchange_failed`, 302);
|
|
387
419
|
}
|
|
388
420
|
profile = await fetchGithubProfile(token.access_token);
|
|
389
421
|
}
|
|
390
422
|
if (!profile) {
|
|
391
|
-
return Response.redirect(`${url.origin}${
|
|
423
|
+
return Response.redirect(`${url.origin}${loginPath()}?error=userinfo_failed`, 302);
|
|
392
424
|
}
|
|
393
425
|
|
|
394
426
|
let normalized;
|
|
@@ -412,7 +444,7 @@ async function oauthCallback(request, env, identity, adapter, provider, creds) {
|
|
|
412
444
|
|
|
413
445
|
const redirectTo = identity.resolvePostLoginPath(saved.redirect_to);
|
|
414
446
|
const res = identity.buildLoginSuccessResponse(request, result.sessionId, redirectTo);
|
|
415
|
-
const globeUrl = `${url.origin}${
|
|
447
|
+
const globeUrl = `${url.origin}${loginPath()}?globe_exit=1&next=${encodeURIComponent(redirectTo)}`;
|
|
416
448
|
return new Response(null, {
|
|
417
449
|
status: 302,
|
|
418
450
|
headers: {
|
|
@@ -99,16 +99,16 @@ test('createFinalizeInboundOAuth fails closed when identity plane fails for new
|
|
|
99
99
|
assert.deepEqual(result, { ok: false, error: 'provision_failed' });
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
test('createOAuthRedirectHelpers
|
|
103
|
-
const {
|
|
102
|
+
test('createOAuthRedirectHelpers rejects disallowed return paths', () => {
|
|
103
|
+
const { safeLoginRedirectPath } = createOAuthRedirectHelpers({
|
|
104
|
+
loginPath: '/auth/login',
|
|
104
105
|
isAllowedLoginResumePath: (p) => p.startsWith('/mcp-oauth'),
|
|
105
106
|
});
|
|
106
|
-
assert.
|
|
107
|
-
|
|
108
|
-
'/dashboard/agent',
|
|
107
|
+
assert.throws(
|
|
108
|
+
() => safeLoginRedirectPath('https://inneranimalmedia.com', '/dashboard/settings/integrations'),
|
|
109
109
|
);
|
|
110
110
|
assert.equal(
|
|
111
|
-
|
|
111
|
+
safeLoginRedirectPath('https://inneranimalmedia.com', '/mcp-oauth/resume'),
|
|
112
112
|
'/mcp-oauth/resume',
|
|
113
113
|
);
|
|
114
114
|
});
|
|
@@ -16,7 +16,7 @@ describe('iam identity provider', () => {
|
|
|
16
16
|
getIamAuthUrl({
|
|
17
17
|
issuer: 'https://inneranimalmedia.com',
|
|
18
18
|
clientId: 'iam_identity_test',
|
|
19
|
-
redirectUri: 'https://legendary.example/api/oauth/
|
|
19
|
+
redirectUri: 'https://legendary.example/api/oauth/inneranimalmedia/callback',
|
|
20
20
|
state: 'st_test',
|
|
21
21
|
codeChallenge: 'challenge',
|
|
22
22
|
scope: IAM_DEFAULT_OIDC_SCOPE,
|