@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.
Files changed (233) hide show
  1. package/AGENTSAM.md +6 -0
  2. package/README.md +16 -1
  3. package/bin/agentsam +16 -1
  4. package/docs/BRAND_INTELLIGENCE.md +1 -1
  5. package/docs/architecture/AGENTSAM_DISTRIBUTION_OWNERSHIP.md +176 -0
  6. package/docs/architecture/AGENTSAM_GO_RUNTIME.md +282 -0
  7. package/docs/architecture/CODEBASEINDEX_GUIDED_PATH.md +202 -0
  8. package/docs/architecture/FS_E2E_CLOSURE_RECEIPT.md +59 -0
  9. package/docs/architecture/LOCAL_FS_AUTHORITY.md +38 -0
  10. package/docs/architecture/LOCAL_STUDIO_CLOUDFLARE_OAUTH.md +28 -0
  11. package/docs/architecture/PLAN_CLI_AND_LOCAL_STUDIO_DESKTOP.md +96 -0
  12. package/docs/architecture/SAM_ACTIVITY_RECOVERY_RECEIPT.md +39 -0
  13. package/docs/architecture/SAM_DECISION_WORK_RECEIPT.md +54 -0
  14. package/docs/architecture/SAM_KERNEL.md +181 -0
  15. package/docs/architecture/SAM_MACHINE_NORMALIZATION_PRECOMMIT_REPORT.md +208 -0
  16. package/docs/architecture/SLASH_SKILLS_PORTABLE.md +116 -0
  17. package/docs/architecture/fs-e2e-receipt.latest.json +42 -0
  18. package/docs/architecture/previews/codebaseindex-guided-path/CODEBASEINDEX_GUIDED_PATH.md +202 -0
  19. package/docs/architecture/previews/codebaseindex-guided-path/index.html +321 -0
  20. package/migrations/d1/0010_portable_tickets_memory.sql +140 -0
  21. package/migrations/d1/0011_agentsam_skill_v2.sql +185 -0
  22. package/migrations/d1/0011b_agentsam_skill_v2_cutover.sql +22 -0
  23. package/migrations/d1/0011c_agentsam_skill_v2_backfill.sql +76 -0
  24. package/migrations/d1/0011d_agentsam_skill_v2_retrieval_revisions.sql +50 -0
  25. package/migrations/d1/0012_agentsam_tools_required_seed.sql +67 -0
  26. package/migrations/d1/0013_identity_oauth_states.sql +15 -0
  27. package/migrations/d1/0014_auth_event_log.sql +20 -0
  28. package/migrations/d1/0015_identity_oauth_state_app_id.sql +3 -0
  29. package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +16 -0
  30. package/migrations/sqlite/agentsam_skill_retrieval.portable.sql +42 -0
  31. package/package.json +19 -4
  32. package/packages/agentsam-contracts/src/errors.ts +16 -0
  33. package/packages/agentsam-errors/src/envelope.js +53 -0
  34. package/packages/agentsam-errors/src/index.js +1 -0
  35. package/packages/agentsam-errors/src/recovery.js +301 -0
  36. package/packages/agentsam-knowledge/src/providers/index.js +18 -6
  37. package/packages/connectors/cloudflare/src/routes.js +9 -0
  38. package/packages/connectors/cloudflare/tests/connector.test.mjs +26 -1
  39. package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
  40. package/packages/identity/.agentsam/features/oauth-login-portal/routes.json +11 -2
  41. package/packages/identity/docs/PORTABLE_IDENTITY_ARCHITECTURE.md +50 -0
  42. package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +31 -0
  43. package/packages/identity/migrations/sqlite/001_identity_core.sql +99 -0
  44. package/packages/identity/migrations/sqlite/002_identity_oauth_client.sql +38 -0
  45. package/packages/identity/migrations/sqlite/003_identity_oauth_server.sql +64 -0
  46. package/packages/identity/package.json +2 -2
  47. package/packages/identity/src/adapters/cloudflare-d1/index.js +122 -22
  48. package/packages/identity/src/adapters/sqlite/index.js +319 -0
  49. package/packages/identity/src/app/verify-app.js +95 -0
  50. package/packages/identity/src/contracts/identity-store.js +115 -0
  51. package/packages/identity/src/contracts/route-ids.js +30 -0
  52. package/packages/identity/src/contracts/route-projection.js +218 -0
  53. package/packages/identity/src/contracts/routes.js +11 -0
  54. package/packages/identity/src/core/browser-paths.js +4 -5
  55. package/packages/identity/src/core/constants.js +13 -8
  56. package/packages/identity/src/core/session-policy.js +32 -0
  57. package/packages/identity/src/frontend/auth-portal/pages/login.html +10 -10
  58. package/packages/identity/src/frontend/auth-portal/pages/reset.html +3 -3
  59. package/packages/identity/src/frontend/auth-portal/pages/signup.html +3 -3
  60. package/packages/identity/src/frontend/auth-portal/preview/dashboard-stub.html +1 -1
  61. package/packages/identity/src/index.js +25 -0
  62. package/packages/identity/src/oauth/README.md +10 -4
  63. package/packages/identity/src/oauth/credentials.js +20 -13
  64. package/packages/identity/src/oauth/finalize-inbound.js +1 -1
  65. package/packages/identity/src/oauth/iam-platform.js +8 -7
  66. package/packages/identity/src/oauth/redirect-paths.js +27 -36
  67. package/packages/identity/src/server/identity-service.js +30 -13
  68. package/packages/identity/src/server/mount-policy.js +30 -0
  69. package/packages/identity/src/server/post-auth.js +79 -0
  70. package/packages/identity/src/server/worker-router.js +104 -72
  71. package/packages/identity/tests/finalize-inbound-oauth.test.mjs +6 -6
  72. package/packages/identity/tests/iam-provider.test.mjs +1 -1
  73. package/packages/identity/tests/identity-service.test.mjs +36 -5
  74. package/packages/identity/tests/oauth-credentials.test.mjs +3 -1
  75. package/packages/identity/tests/portable-identity-architecture.test.mjs +157 -0
  76. package/packages/identity/tests/session-routes-policy.test.mjs +21 -0
  77. package/packages/theme-church-site/package.json +2 -1
  78. package/packages/theme-church-site/src/index.js +1 -0
  79. package/packages/theme-companions-site/package.json +2 -1
  80. package/packages/theme-companions-site/src/index.js +1 -0
  81. package/packages/theme-floors-site/package.json +2 -1
  82. package/packages/theme-floors-site/src/index.js +1 -0
  83. package/packages/theme-fuelnfree-site/package.json +2 -1
  84. package/packages/theme-fuelnfree-site/src/index.js +1 -0
  85. package/packages/theme-handyman-site/package.json +2 -1
  86. package/packages/theme-handyman-site/src/index.js +1 -0
  87. package/packages/theme-insurance-site/package.json +2 -1
  88. package/packages/theme-insurance-site/src/index.js +1 -0
  89. package/packages/theme-shinshu-site/package.json +2 -1
  90. package/packages/theme-shinshu-site/src/index.js +1 -0
  91. package/protocol/apps/agentsam.app.v1.schema.json +51 -0
  92. package/protocol/brand/brandpack.v1.schema.json +43 -0
  93. package/protocol/credentials/issue.v1.schema.json +38 -0
  94. package/protocol/database/connection.v1.schema.json +41 -0
  95. package/protocol/embeddings/embedding-profile.v1.schema.json +20 -0
  96. package/protocol/errors/error-envelope.schema.json +135 -1
  97. package/protocol/errors/recovery.v1.schema.json +50 -0
  98. package/protocol/runtime/workspace-fs.v1.schema.json +71 -0
  99. package/protocol/sam/activity.v1.schema.json +48 -0
  100. package/protocol/sam/answer.v1.schema.json +35 -0
  101. package/protocol/sam/calibration.v1.schema.json +21 -0
  102. package/protocol/sam/decision-receipt.v1.schema.json +29 -0
  103. package/protocol/sam/evaluation.v1.schema.json +19 -0
  104. package/protocol/sam/operation.schema.json +66 -0
  105. package/protocol/sam/outcome.v1.schema.json +36 -0
  106. package/protocol/sam/question.v1.schema.json +26 -0
  107. package/protocol/sam/registry.seed.json +153 -0
  108. package/protocol/sam/result.schema.json +53 -0
  109. package/protocol/sam/state.v1.schema.json +23 -0
  110. package/protocol/skills/agentsam.interaction.v1.schema.json +50 -0
  111. package/protocol/skills/agentsam.skill.v1.schema.json +57 -0
  112. package/protocol/ui/icon-registry.mjs +226 -0
  113. package/protocol/ui/icon.v1.schema.json +51 -0
  114. package/skills/README.md +22 -9
  115. package/skills/agentsam-codebaseindex/SKILL.md +225 -0
  116. package/skills/catalog.json +14 -0
  117. package/src/cli/command-catalog.js +130 -0
  118. package/src/cli/dispatch.js +48 -0
  119. package/src/cli.js +43 -1
  120. package/src/commands/api-key.js +244 -0
  121. package/src/commands/app.js +60 -28
  122. package/src/commands/brand.js +17 -19
  123. package/src/commands/codebaseindex.js +688 -0
  124. package/src/commands/env.js +152 -25
  125. package/src/commands/go.js +366 -53
  126. package/src/commands/interaction-clack.js +115 -0
  127. package/src/commands/models.js +1 -1
  128. package/src/commands/providers.js +62 -14
  129. package/src/commands/shell.js +43 -2
  130. package/src/commands/skill.js +248 -0
  131. package/src/commands/skills.js +1 -1
  132. package/src/commands/start-local.js +4 -0
  133. package/src/commands/whoami.js +90 -18
  134. package/src/go/build.js +229 -39
  135. package/src/go/cloudflare.js +506 -105
  136. package/src/go/container.js +120 -0
  137. package/src/go/contract.js +9 -4
  138. package/src/go/discover.js +149 -33
  139. package/src/go/index.js +15 -3
  140. package/src/go/native-probe-runner.mjs +119 -0
  141. package/src/go/{registry.js → official-registry.js} +64 -7
  142. package/src/go/official-release.js +10 -0
  143. package/src/go/receipts.js +77 -10
  144. package/src/go/verify.js +9 -2
  145. package/src/index.js +27 -0
  146. package/src/indexing/ingest/discover-models.js +298 -0
  147. package/src/indexing/ingest/inventory.js +243 -0
  148. package/src/indexing/ingest/job-graph.js +181 -0
  149. package/src/indexing/ingest/materials.js +210 -0
  150. package/src/lib/provider-credentials.js +63 -21
  151. package/src/lib/slash-commands.js +1 -0
  152. package/src/local-fs/capability.js +121 -0
  153. package/src/local-fs/freshness.js +75 -0
  154. package/src/local-fs/index.js +385 -0
  155. package/src/local-fs/paths.js +100 -0
  156. package/src/local-pty/server.js +295 -19
  157. package/src/mcp/client.js +2 -2
  158. package/src/models/ai-access-onboarding.js +112 -0
  159. package/src/models/discovery.js +10 -2
  160. package/src/models/inventory-core.js +9 -1
  161. package/src/sam/activity/index.js +183 -0
  162. package/src/sam/client.js +252 -0
  163. package/src/sam/decision/calibration.js +109 -0
  164. package/src/sam/decision/confidence.js +126 -0
  165. package/src/sam/decision/evaluate.js +157 -0
  166. package/src/sam/decision/evaluators/deterministic.js +341 -0
  167. package/src/sam/decision/evaluators/heuristic.js +61 -0
  168. package/src/sam/decision/evaluators/select.js +50 -0
  169. package/src/sam/decision/evaluators/semantic.js +149 -0
  170. package/src/sam/decision/hierarchical.js +61 -0
  171. package/src/sam/decision/index.js +53 -0
  172. package/src/sam/decision/policy.js +86 -0
  173. package/src/sam/decision/questions.js +120 -0
  174. package/src/sam/decision/receipt.js +148 -0
  175. package/src/sam/decision/state.js +117 -0
  176. package/src/sam/decision/types.js +22 -0
  177. package/src/sam/decision/validate.js +200 -0
  178. package/src/sam/define.js +51 -0
  179. package/src/sam/index.js +65 -0
  180. package/src/sam/operations/brand-scan.js +62 -0
  181. package/src/sam/operations/cad-blender-inspect.js +36 -0
  182. package/src/sam/operations/codebaseindex-ingest.js +49 -0
  183. package/src/sam/operations/decision-evaluate.js +59 -0
  184. package/src/sam/operations/planning-astar.js +77 -0
  185. package/src/sam/operations/planning-goap.js +60 -0
  186. package/src/sam/operations/repository-inspect.js +72 -0
  187. package/src/sam/operations/security-scan.js +33 -0
  188. package/src/sam/operations/terminal-exec.js +29 -0
  189. package/src/sam/planning/astar.js +311 -0
  190. package/src/sam/planning/goap.js +177 -0
  191. package/src/sam/planning/index.js +21 -0
  192. package/src/sam/planning/state.js +84 -0
  193. package/src/sam/registry.js +48 -0
  194. package/src/sam/result.js +77 -0
  195. package/src/sam/seed.js +44 -0
  196. package/src/sam/types.js +91 -0
  197. package/src/skills/catalog.js +64 -0
  198. package/src/skills/content-resolver.js +124 -0
  199. package/src/skills/hosted-store.js +37 -0
  200. package/src/skills/index.js +29 -64
  201. package/src/skills/interaction.js +102 -0
  202. package/src/skills/local-store.js +228 -0
  203. package/src/skills/manifest.js +104 -0
  204. package/src/skills/metrics.js +31 -0
  205. package/src/skills/registry.js +184 -0
  206. package/src/skills/runtime.js +287 -0
  207. package/src/skills/slash.js +44 -0
  208. package/src/ui/cli/help.js +94 -101
  209. package/test/cli/api-key-env-whoami.test.mjs +129 -0
  210. package/test/cli/codebaseindex-plan-ux.test.mjs +37 -0
  211. package/test/cli/go.test.mjs +62 -5
  212. package/test/cli/skill-npm-and-env.test.mjs +52 -0
  213. package/test/cli/wireframes-go-registry.test.mjs +87 -2
  214. package/test/go/build-source-identity.test.mjs +31 -0
  215. package/test/go/cloudflare-probe.test.mjs +274 -10
  216. package/test/go/distribution.test.mjs +28 -0
  217. package/test/integration/ai-access-onboarding.test.mjs +47 -0
  218. package/test/integration/cli-help.test.mjs +1 -1
  219. package/test/integration/cms-site-tenancy-contract.test.mjs +6 -6
  220. package/test/integration/codebaseindex-ingest.test.mjs +166 -0
  221. package/test/integration/icon-registry.test.mjs +67 -0
  222. package/test/integration/ingest-discover-models.test.mjs +30 -0
  223. package/test/integration/install-script.test.mjs +12 -9
  224. package/test/integration/local-fs.test.mjs +113 -0
  225. package/test/integration/provider-env-cli.test.mjs +2 -1
  226. package/test/integration/sam-activity-recovery.test.mjs +147 -0
  227. package/test/integration/sam-decision.test.mjs +584 -0
  228. package/test/integration/sam-kernel.test.mjs +99 -0
  229. package/test/integration/sam-planning-astar.test.mjs +279 -0
  230. package/test/integration/skill-runtime.test.mjs +240 -0
  231. package/test/integration/studio-fs-pty-e2e.test.mjs +294 -0
  232. package/test/models.test.mjs +14 -7
  233. 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
- const DEFAULT_DASHBOARD_FALLBACK = '/dashboard/agent';
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} [dashboardFallback]
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 dashboardFallback = options.dashboardFallback ?? DEFAULT_DASHBOARD_FALLBACK;
22
- const isAllowedLoginResumePath = options.isAllowedLoginResumePath ?? (() => false);
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 safeDashboardLoginRedirectPath(originBase, returnTo) {
49
- if (!returnTo || typeof returnTo !== 'string') return dashboardFallback;
50
- const t = returnTo.trim();
51
- if (!t) return dashboardFallback;
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
- try {
59
- const u = new URL(t);
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 = dashboardFallback;
66
+ let path;
73
67
  try {
74
68
  const u = new URL(returnToFullUrl);
75
69
  path = u.pathname + (u.search || '');
76
70
  } catch {
77
- /* keep default */
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
- return `${originBase}/auth/login?globe_exit=1&next=${encodeURIComponent(path)}`;
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, AUTH_SESSION_TTL_SECONDS } from '../core/constants.js';
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 { sanitizeBrowserNextPath } from '../core/browser-paths.js';
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 {ReturnType<import('../adapters/cloudflare-d1/index.js').createCloudflareD1Adapter>} IdentityStorageAdapter
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
- * @param {{ adapter: IdentityStorageAdapter, cookieName?: string, defaultRedirect?: string }} config
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
- // Direct sign-in has no navigation intent to restore. Land at the site root;
20
- // protected routes always provide their own validated `next` path.
21
- const defaultRedirect = sanitizeBrowserNextPath(config.defaultRedirect) || '/';
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 sanitizeBrowserNextPath(nextPath) || defaultRedirect;
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 = AUTH_SESSION_TTL_SECONDS) {
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
- defaultRedirect,
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 { AUTH_LOGIN_PATH } from '../core/constants.js';
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
- const identity = options.identity || createIdentityService({ adapter });
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: `${AUTH_LOGIN_PATH}?reset=success` });
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
- if (path === '/api/oauth/iam/callback' && method === 'GET') {
208
- return iamPlatformOAuthCallback(request, env, adapter, identity);
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/iam/start' && method === 'GET') {
211
- const lane = resolveOAuthCredentialLane(env, 'iam');
212
- if (!lane) return jsonResponse({ ok: false, error: 'iam_oauth_not_configured' }, 503);
213
- return iamPlatformOAuthStart(request, env, adapter, identity);
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}${AUTH_LOGIN_PATH}?error=oauth_not_configured`, 302);
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}${AUTH_LOGIN_PATH}?error=oauth_not_configured`, 302);
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}${AUTH_LOGIN_PATH}?error=oauth_not_configured`, 302);
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 — use extensionless paths; assets serves foo.html at /foo.
263
- const authPageMap = {
264
- '/auth/login': '/auth/login',
265
- '/auth/signup': '/auth/signup',
266
- '/auth/reset': '/auth/reset',
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' && authPageMap[path]) {
290
+ if (method === 'GET' && authPages.has(path)) {
270
291
  if (env.ASSETS?.fetch) {
271
- const assetUrl = new URL(authPageMap[path], url.origin);
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
- if (method === 'GET' && (path === '/dashboard' || path.startsWith('/dashboard/'))) {
278
- const ctx = await identity.sessionFromRequest(request);
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
- const state = randomOAuthState();
302
- const codeVerifier = pkceVerifier();
303
- const codeChallenge = await pkceChallenge(codeVerifier);
304
- const redirectTo = identity.resolvePostLoginPath(
305
- url.searchParams.get('next') || url.searchParams.get('return_to'),
306
- );
307
- await adapter.saveOAuthState({ state, provider, codeVerifier, redirectTo });
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
- codeChallenge,
317
- });
318
- } else if (provider === 'cloudflare') {
319
- authUrl = getCloudflareAuthUrl({
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}${AUTH_LOGIN_PATH}?error=oauth_failed`, 302);
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}${AUTH_LOGIN_PATH}?error=state_mismatch`, 302);
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}${AUTH_LOGIN_PATH}?error=token_exchange_failed`, 302);
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}${AUTH_LOGIN_PATH}?error=token_exchange_failed`, 302);
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}${AUTH_LOGIN_PATH}?error=token_exchange_failed`, 302);
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}${AUTH_LOGIN_PATH}?error=userinfo_failed`, 302);
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}${AUTH_LOGIN_PATH}?globe_exit=1&next=${encodeURIComponent(redirectTo)}`;
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 blocks integrations return path', () => {
103
- const { safeDashboardLoginRedirectPath } = createOAuthRedirectHelpers({
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.equal(
107
- safeDashboardLoginRedirectPath('https://inneranimalmedia.com', '/dashboard/settings/integrations'),
108
- '/dashboard/agent',
107
+ assert.throws(
108
+ () => safeLoginRedirectPath('https://inneranimalmedia.com', '/dashboard/settings/integrations'),
109
109
  );
110
110
  assert.equal(
111
- safeDashboardLoginRedirectPath('https://inneranimalmedia.com', '/mcp-oauth/resume'),
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/iam/callback',
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,