@raybase/core 0.0.2 → 0.3.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/LICENSE +21 -0
- package/dist/.admin-build/AdminApp.css +2 -0
- package/dist/.admin-build/AdminApp.js +77 -0
- package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
- package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
- package/dist/actions.d.ts +347 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +661 -0
- package/dist/actions.js.map +1 -0
- package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
- package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
- package/dist/admin-boot.d.ts +94 -0
- package/dist/admin-boot.d.ts.map +1 -0
- package/dist/admin-boot.js +139 -0
- package/dist/admin-boot.js.map +1 -0
- package/dist/admin-docs-content.d.ts +95 -0
- package/dist/admin-docs-content.d.ts.map +1 -0
- package/dist/admin-docs-content.js +463 -0
- package/dist/admin-docs-content.js.map +1 -0
- package/dist/admin-docs-ui.d.ts +11 -0
- package/dist/admin-docs-ui.d.ts.map +1 -0
- package/dist/admin-docs-ui.js +23 -0
- package/dist/admin-docs-ui.js.map +1 -0
- package/dist/admin-kit.d.ts +29 -0
- package/dist/admin-kit.js +1557 -0
- package/dist/admin-role.d.ts +8 -0
- package/dist/admin-role.d.ts.map +1 -0
- package/dist/admin-role.js +22 -0
- package/dist/admin-role.js.map +1 -0
- package/dist/admin-ui.d.ts +3 -0
- package/dist/admin-ui.d.ts.map +1 -0
- package/dist/admin-ui.js +16 -0
- package/dist/admin-ui.js.map +1 -0
- package/dist/agent-ai-chat.d.ts +127 -0
- package/dist/agent-ai-chat.d.ts.map +1 -0
- package/dist/agent-ai-chat.js +5975 -0
- package/dist/agent-ai-chat.js.map +1 -0
- package/dist/agent-chapter-archive.d.ts +214 -0
- package/dist/agent-chapter-archive.d.ts.map +1 -0
- package/dist/agent-chapter-archive.js +696 -0
- package/dist/agent-chapter-archive.js.map +1 -0
- package/dist/agent-chapter-search.d.ts +116 -0
- package/dist/agent-chapter-search.d.ts.map +1 -0
- package/dist/agent-chapter-search.js +605 -0
- package/dist/agent-chapter-search.js.map +1 -0
- package/dist/agent-chapter-tools.d.ts +45 -0
- package/dist/agent-chapter-tools.d.ts.map +1 -0
- package/dist/agent-chapter-tools.js +344 -0
- package/dist/agent-chapter-tools.js.map +1 -0
- package/dist/agent-chapters.d.ts +228 -0
- package/dist/agent-chapters.d.ts.map +1 -0
- package/dist/agent-chapters.js +737 -0
- package/dist/agent-chapters.js.map +1 -0
- package/dist/agent-data-parts.d.ts +167 -0
- package/dist/agent-data-parts.d.ts.map +1 -0
- package/dist/agent-data-parts.js +68 -0
- package/dist/agent-data-parts.js.map +1 -0
- package/dist/agent-errors.d.ts +41 -0
- package/dist/agent-errors.d.ts.map +1 -0
- package/dist/agent-errors.js +191 -0
- package/dist/agent-errors.js.map +1 -0
- package/dist/agent-follow-up-store.d.ts +273 -0
- package/dist/agent-follow-up-store.d.ts.map +1 -0
- package/dist/agent-follow-up-store.js +703 -0
- package/dist/agent-follow-up-store.js.map +1 -0
- package/dist/agent-follow-ups.d.ts +314 -0
- package/dist/agent-follow-ups.d.ts.map +1 -0
- package/dist/agent-follow-ups.js +840 -0
- package/dist/agent-follow-ups.js.map +1 -0
- package/dist/agent-memory.d.ts +323 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +1270 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/agent-message-parts.d.ts +58 -0
- package/dist/agent-message-parts.d.ts.map +1 -0
- package/dist/agent-message-parts.js +163 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-remote-mcp-credentials.d.ts +154 -0
- package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
- package/dist/agent-remote-mcp-credentials.js +287 -0
- package/dist/agent-remote-mcp-credentials.js.map +1 -0
- package/dist/agent-remote-mcp-crypto.d.ts +32 -0
- package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
- package/dist/agent-remote-mcp-crypto.js +101 -0
- package/dist/agent-remote-mcp-crypto.js.map +1 -0
- package/dist/agent-remote-mcp-install.d.ts +299 -0
- package/dist/agent-remote-mcp-install.d.ts.map +1 -0
- package/dist/agent-remote-mcp-install.js +785 -0
- package/dist/agent-remote-mcp-install.js.map +1 -0
- package/dist/agent-remote-mcp-oauth.d.ts +73 -0
- package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
- package/dist/agent-remote-mcp-oauth.js +344 -0
- package/dist/agent-remote-mcp-oauth.js.map +1 -0
- package/dist/agent-remote-mcp-transport.d.ts +46 -0
- package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
- package/dist/agent-remote-mcp-transport.js +225 -0
- package/dist/agent-remote-mcp-transport.js.map +1 -0
- package/dist/agent-remote-mcp.d.ts +147 -0
- package/dist/agent-remote-mcp.d.ts.map +1 -0
- package/dist/agent-remote-mcp.js +681 -0
- package/dist/agent-remote-mcp.js.map +1 -0
- package/dist/agent-room-redaction.d.ts +66 -0
- package/dist/agent-room-redaction.d.ts.map +1 -0
- package/dist/agent-room-redaction.js +264 -0
- package/dist/agent-room-redaction.js.map +1 -0
- package/dist/agent-rooms.d.ts +209 -0
- package/dist/agent-rooms.d.ts.map +1 -0
- package/dist/agent-rooms.js +244 -0
- package/dist/agent-rooms.js.map +1 -0
- package/dist/agent-runtime.d.ts +541 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +4260 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent-skill-archive.d.ts +20 -0
- package/dist/agent-skill-archive.d.ts.map +1 -0
- package/dist/agent-skill-archive.js +316 -0
- package/dist/agent-skill-archive.js.map +1 -0
- package/dist/agent-skill-discovery.d.ts +96 -0
- package/dist/agent-skill-discovery.d.ts.map +1 -0
- package/dist/agent-skill-discovery.js +464 -0
- package/dist/agent-skill-discovery.js.map +1 -0
- package/dist/agent-skill-format.d.ts +57 -0
- package/dist/agent-skill-format.d.ts.map +1 -0
- package/dist/agent-skill-format.js +238 -0
- package/dist/agent-skill-format.js.map +1 -0
- package/dist/agent-skill-import.d.ts +64 -0
- package/dist/agent-skill-import.d.ts.map +1 -0
- package/dist/agent-skill-import.js +295 -0
- package/dist/agent-skill-import.js.map +1 -0
- package/dist/agent-skill-publish.d.ts +51 -0
- package/dist/agent-skill-publish.d.ts.map +1 -0
- package/dist/agent-skill-publish.js +136 -0
- package/dist/agent-skill-publish.js.map +1 -0
- package/dist/agent-skill-store.d.ts +252 -0
- package/dist/agent-skill-store.d.ts.map +1 -0
- package/dist/agent-skill-store.js +514 -0
- package/dist/agent-skill-store.js.map +1 -0
- package/dist/agent-skills.d.ts +85 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +291 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-transcript-pager.d.ts +77 -0
- package/dist/agent-transcript-pager.d.ts.map +1 -0
- package/dist/agent-transcript-pager.js +210 -0
- package/dist/agent-transcript-pager.js.map +1 -0
- package/dist/agent-user-data-parts.d.ts +20 -0
- package/dist/agent-user-data-parts.d.ts.map +1 -0
- package/dist/agent-user-data-parts.js +161 -0
- package/dist/agent-user-data-parts.js.map +1 -0
- package/dist/agent-user-transcript.d.ts +46 -0
- package/dist/agent-user-transcript.d.ts.map +1 -0
- package/dist/agent-user-transcript.js +106 -0
- package/dist/agent-user-transcript.js.map +1 -0
- package/dist/agent-voice-notes.d.ts +15 -0
- package/dist/agent-voice-notes.d.ts.map +1 -0
- package/dist/agent-voice-notes.js +17 -0
- package/dist/agent-voice-notes.js.map +1 -0
- package/dist/agent-wakes.d.ts +213 -0
- package/dist/agent-wakes.d.ts.map +1 -0
- package/dist/agent-wakes.js +534 -0
- package/dist/agent-wakes.js.map +1 -0
- package/dist/agent.d.ts +1780 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +3123 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai.d.ts +583 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1858 -0
- package/dist/ai.js.map +1 -0
- package/dist/app-manifest.d.ts +255 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +372 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/app-users.d.ts +30 -0
- package/dist/app-users.d.ts.map +1 -0
- package/dist/app-users.js +91 -0
- package/dist/app-users.js.map +1 -0
- package/dist/audit-archive.d.ts +85 -0
- package/dist/audit-archive.d.ts.map +1 -0
- package/dist/audit-archive.js +178 -0
- package/dist/audit-archive.js.map +1 -0
- package/dist/audit.d.ts +54 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +95 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth-logger.d.ts +28 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +53 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-password-secret.d.ts +59 -0
- package/dist/auth-password-secret.d.ts.map +1 -0
- package/dist/auth-password-secret.js +409 -0
- package/dist/auth-password-secret.js.map +1 -0
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +285 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/auth.d.ts +321 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +1562 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup-archive.d.ts +257 -0
- package/dist/backup-archive.d.ts.map +1 -0
- package/dist/backup-archive.js +2068 -0
- package/dist/backup-archive.js.map +1 -0
- package/dist/backup-crypto.d.ts +313 -0
- package/dist/backup-crypto.d.ts.map +1 -0
- package/dist/backup-crypto.js +693 -0
- package/dist/backup-crypto.js.map +1 -0
- package/dist/backup-format.d.ts +200 -0
- package/dist/backup-format.d.ts.map +1 -0
- package/dist/backup-format.js +1247 -0
- package/dist/backup-format.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +35 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +83 -0
- package/dist/clock.js.map +1 -0
- package/dist/data-client.d.ts +187 -0
- package/dist/data-client.d.ts.map +1 -0
- package/dist/data-client.js +590 -0
- package/dist/data-client.js.map +1 -0
- package/dist/data-secrets.d.ts +164 -0
- package/dist/data-secrets.d.ts.map +1 -0
- package/dist/data-secrets.js +383 -0
- package/dist/data-secrets.js.map +1 -0
- package/dist/db.d.ts +453 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +324 -0
- package/dist/db.js.map +1 -0
- package/dist/dev-activity.d.ts +48 -0
- package/dist/dev-activity.d.ts.map +1 -0
- package/dist/dev-activity.js +50 -0
- package/dist/dev-activity.js.map +1 -0
- package/dist/do.d.ts +429 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +4526 -0
- package/dist/do.js.map +1 -0
- package/dist/email.d.ts +137 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +527 -0
- package/dist/email.js.map +1 -0
- package/dist/generated/admin-assets.d.ts +16 -0
- package/dist/generated/admin-assets.d.ts.map +1 -0
- package/dist/generated/admin-assets.js +16 -0
- package/dist/generated/admin-assets.js.map +1 -0
- package/dist/generated/admin-docs-assets.d.ts +3 -0
- package/dist/generated/admin-docs-assets.d.ts.map +1 -0
- package/dist/generated/admin-docs-assets.js +3 -0
- package/dist/generated/admin-docs-assets.js.map +1 -0
- package/dist/handoff-consumer.d.ts +79 -0
- package/dist/handoff-consumer.d.ts.map +1 -0
- package/dist/handoff-consumer.js +300 -0
- package/dist/handoff-consumer.js.map +1 -0
- package/dist/handoff.d.ts +70 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +228 -0
- package/dist/handoff.js.map +1 -0
- package/dist/host-auth.d.ts +47 -0
- package/dist/host-auth.d.ts.map +1 -0
- package/dist/host-auth.js +59 -0
- package/dist/host-auth.js.map +1 -0
- package/dist/host-user.d.ts +15 -0
- package/dist/host-user.d.ts.map +1 -0
- package/dist/host-user.js +23 -0
- package/dist/host-user.js.map +1 -0
- package/dist/html.d.ts +35 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +634 -0
- package/dist/html.js.map +1 -0
- package/dist/http-error.d.ts +13 -0
- package/dist/http-error.d.ts.map +1 -0
- package/dist/http-error.js +15 -0
- package/dist/http-error.js.map +1 -0
- package/dist/imports.d.ts +219 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +571 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-tape.d.ts +64 -0
- package/dist/inference-tape.d.ts.map +1 -0
- package/dist/inference-tape.js +278 -0
- package/dist/inference-tape.js.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/jobs.d.ts +796 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +538 -0
- package/dist/jobs.js.map +1 -0
- package/dist/json-schema.d.ts +91 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +884 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/knowledge.d.ts +33 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +348 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/local-dev-chrome.d.ts +29 -0
- package/dist/local-dev-chrome.d.ts.map +1 -0
- package/dist/local-dev-chrome.js +514 -0
- package/dist/local-dev-chrome.js.map +1 -0
- package/dist/local-dev-personas.d.ts +15 -0
- package/dist/local-dev-personas.d.ts.map +1 -0
- package/dist/local-dev-personas.js +46 -0
- package/dist/local-dev-personas.js.map +1 -0
- package/dist/mcp.d.ts +567 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2480 -0
- package/dist/mcp.js.map +1 -0
- package/dist/modules.d.ts +145 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +164 -0
- package/dist/modules.js.map +1 -0
- package/dist/notification-client.d.ts +94 -0
- package/dist/notification-client.d.ts.map +1 -0
- package/dist/notification-client.js +127 -0
- package/dist/notification-client.js.map +1 -0
- package/dist/notifications.d.ts +447 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +1625 -0
- package/dist/notifications.js.map +1 -0
- package/dist/operator-data-query.d.ts +117 -0
- package/dist/operator-data-query.d.ts.map +1 -0
- package/dist/operator-data-query.js +431 -0
- package/dist/operator-data-query.js.map +1 -0
- package/dist/operator-mcp.d.ts +63 -0
- package/dist/operator-mcp.d.ts.map +1 -0
- package/dist/operator-mcp.js +2828 -0
- package/dist/operator-mcp.js.map +1 -0
- package/dist/outbound-https.d.ts +24 -0
- package/dist/outbound-https.d.ts.map +1 -0
- package/dist/outbound-https.js +172 -0
- package/dist/outbound-https.js.map +1 -0
- package/dist/pitr.d.ts +60 -0
- package/dist/pitr.d.ts.map +1 -0
- package/dist/pitr.js +94 -0
- package/dist/pitr.js.map +1 -0
- package/dist/platform.d.ts +199 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +196 -0
- package/dist/platform.js.map +1 -0
- package/dist/policies.d.ts +115 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +158 -0
- package/dist/policies.js.map +1 -0
- package/dist/principal.d.ts +143 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +174 -0
- package/dist/principal.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +578 -0
- package/dist/realtime.js.map +1 -0
- package/dist/retrieval.d.ts +263 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +602 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-error-client.d.ts +10 -0
- package/dist/runtime-error-client.d.ts.map +1 -0
- package/dist/runtime-error-client.js +70 -0
- package/dist/runtime-error-client.js.map +1 -0
- package/dist/runtime-errors.d.ts +180 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +572 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/schema.d.ts +434 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +232 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +216 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +785 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +191 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +478 -0
- package/dist/settings.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +30 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +469 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +82 -0
- package/dist/storage.js.map +1 -0
- package/dist/subject-domains.d.ts +15 -0
- package/dist/subject-domains.d.ts.map +1 -0
- package/dist/subject-domains.js +38 -0
- package/dist/subject-domains.js.map +1 -0
- package/dist/support.d.ts +132 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +483 -0
- package/dist/support.js.map +1 -0
- package/dist/system-tables.d.ts +12 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +225 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/test.d.ts +57 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +454 -0
- package/dist/test.js.map +1 -0
- package/dist/vite.d.ts +82 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +82 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpush-crypto.d.ts +39 -0
- package/dist/webpush-crypto.d.ts.map +1 -0
- package/dist/webpush-crypto.js +213 -0
- package/dist/webpush-crypto.js.map +1 -0
- package/dist/webpush-private.d.ts +11 -0
- package/dist/webpush-private.d.ts.map +1 -0
- package/dist/webpush-private.js +11 -0
- package/dist/webpush-private.js.map +1 -0
- package/dist/webpush.d.ts +86 -0
- package/dist/webpush.d.ts.map +1 -0
- package/dist/webpush.js +375 -0
- package/dist/webpush.js.map +1 -0
- package/dist/worker.d.ts +185 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +13021 -0
- package/dist/worker.js.map +1 -0
- package/package.json +161 -8
- package/README.md +0 -3
package/dist/auth.js
ADDED
|
@@ -0,0 +1,1562 @@
|
|
|
1
|
+
import { passkey } from "@better-auth/passkey";
|
|
2
|
+
import { genericOAuth } from "better-auth/plugins/generic-oauth";
|
|
3
|
+
import { createAdapter } from "better-auth/adapters";
|
|
4
|
+
import { createAuthMiddleware } from "better-auth/api";
|
|
5
|
+
import { betterAuth, APIError } from "better-auth";
|
|
6
|
+
import { assertNotHostUser, withHostUserRoleCeiling } from "./host-user.js";
|
|
7
|
+
import { emailOTP, magicLink } from "better-auth/plugins";
|
|
8
|
+
import { isRaybaseAdminRole, normalizeStoredAdminRole } from "./admin-role.js";
|
|
9
|
+
import { actorAuditRef, recordAudit } from "./audit.js";
|
|
10
|
+
import { raybaseBetterAuthLogger } from "./auth-logger.js";
|
|
11
|
+
import { runRaybasePasswordSecretLifecycle } from "./auth-password-secret.js";
|
|
12
|
+
import { betterAuthSchema, raybaseSystemTableSql, systemTableSql } from "./auth-schema.js";
|
|
13
|
+
import { doExecutor } from "./db.js";
|
|
14
|
+
import { appUrlForEmail, isRaybaseAuthEmailFeatureEnabled, redactedEmailError, resolveRaybaseAuthEmailConfig, rewriteEmailUrl, sendRaybaseAuthEmail } from "./email.js";
|
|
15
|
+
import { raybaseDevPersonaOperatorRole, resolveRaybaseDevPersonas, resolveRequestedRaybaseDevPersona } from "./local-dev-personas.js";
|
|
16
|
+
import { principalId, principalOnBehalfOf } from "./principal.js";
|
|
17
|
+
import { publicSignupEnabled, raybaseSchema } from "./schema.js";
|
|
18
|
+
import { q, randomId } from "./sql.js";
|
|
19
|
+
import { wallNowIso as nowIso, wallNowMs } from "./clock.js";
|
|
20
|
+
export { raybaseSystemTableSql, systemTableSql };
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
const decoder = new TextDecoder();
|
|
23
|
+
export const betterAuthSecretName = "BETTER_AUTH_SECRET";
|
|
24
|
+
export const internalSignupHeader = "x-raybase-internal-signup";
|
|
25
|
+
export const raybaseSignInLinkDefaultExpiresIn = 10 * 60;
|
|
26
|
+
export const raybaseSignInLinkMaximumExpiresIn = 60 * 60;
|
|
27
|
+
const emptySchema = raybaseSchema({});
|
|
28
|
+
const externalProviderIdPattern = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
29
|
+
const pendingExternalAuthCookieName = "raybase.external_auth";
|
|
30
|
+
const pendingExternalAuthTtlSeconds = 10 * 60;
|
|
31
|
+
const passwordSecretLifecycleByAuth = new WeakMap();
|
|
32
|
+
function authDb(env) {
|
|
33
|
+
return doExecutor(env, emptySchema, []);
|
|
34
|
+
}
|
|
35
|
+
export function resolveRaybaseSignInLinksConfig(config) {
|
|
36
|
+
const configured = config.auth?.signInLinks;
|
|
37
|
+
const expiresIn = configured?.expiresIn ?? raybaseSignInLinkDefaultExpiresIn;
|
|
38
|
+
if (!Number.isInteger(expiresIn) || expiresIn <= 0 || expiresIn > raybaseSignInLinkMaximumExpiresIn) {
|
|
39
|
+
throw new Error(`auth.signInLinks.expiresIn must be a positive integer no greater than ${raybaseSignInLinkMaximumExpiresIn} seconds`);
|
|
40
|
+
}
|
|
41
|
+
return { enabled: configured?.enabled === true, expiresIn };
|
|
42
|
+
}
|
|
43
|
+
export function assertRaybaseExternalProviderId(id) {
|
|
44
|
+
const normalized = String(id ?? "").trim();
|
|
45
|
+
if (!externalProviderIdPattern.test(normalized) ||
|
|
46
|
+
normalized.includes("/") ||
|
|
47
|
+
normalized.includes("\\") ||
|
|
48
|
+
normalized.includes(".") ||
|
|
49
|
+
/\s/.test(normalized) ||
|
|
50
|
+
/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized)) {
|
|
51
|
+
throw new Error(`Invalid external auth provider id: ${id}`);
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
export function normalizeRaybaseExternalProviderMetadata(providers, runtimeProviders = []) {
|
|
56
|
+
const runtimeLabels = new Map(runtimeProviders.map((provider) => [assertRaybaseExternalProviderId(provider.id), provider.label]));
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const normalized = [];
|
|
59
|
+
for (const provider of providers ?? []) {
|
|
60
|
+
const id = assertRaybaseExternalProviderId(provider.id);
|
|
61
|
+
if (seen.has(id))
|
|
62
|
+
throw new Error(`Duplicate external auth provider id: ${id}`);
|
|
63
|
+
seen.add(id);
|
|
64
|
+
if (provider.enabled === false)
|
|
65
|
+
continue;
|
|
66
|
+
normalized.push({
|
|
67
|
+
...provider,
|
|
68
|
+
id,
|
|
69
|
+
label: provider.label || runtimeLabels.get(id) || id
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
export function defineRaybaseExternalAuth(runtime) {
|
|
75
|
+
return runtime;
|
|
76
|
+
}
|
|
77
|
+
function genericOAuthConfig(input) {
|
|
78
|
+
if (!input)
|
|
79
|
+
return [];
|
|
80
|
+
return Array.isArray(input) ? input : input.config;
|
|
81
|
+
}
|
|
82
|
+
function runtimeProviderIds(config) {
|
|
83
|
+
const ids = new Set();
|
|
84
|
+
for (const provider of config.providers ?? [])
|
|
85
|
+
ids.add(assertRaybaseExternalProviderId(provider.id));
|
|
86
|
+
for (const id of Object.keys(config.socialProviders ?? {}))
|
|
87
|
+
ids.add(assertRaybaseExternalProviderId(id));
|
|
88
|
+
for (const provider of genericOAuthConfig(config.genericOAuth))
|
|
89
|
+
ids.add(assertRaybaseExternalProviderId(String(provider.providerId)));
|
|
90
|
+
return Array.from(ids);
|
|
91
|
+
}
|
|
92
|
+
export async function resolveRaybaseExternalAuth(env, runtime, request) {
|
|
93
|
+
if (!runtime)
|
|
94
|
+
return undefined;
|
|
95
|
+
const configured = runtime.configure ? await runtime.configure({ env, request }) : {};
|
|
96
|
+
const providers = [...(runtime.providers ?? []), ...(configured.providers ?? [])];
|
|
97
|
+
const merged = {
|
|
98
|
+
...configured,
|
|
99
|
+
providers,
|
|
100
|
+
socialProviders: configured.socialProviders,
|
|
101
|
+
genericOAuth: configured.genericOAuth
|
|
102
|
+
};
|
|
103
|
+
return { ...merged, providerIds: runtimeProviderIds(merged) };
|
|
104
|
+
}
|
|
105
|
+
export function raybaseExternalProviderMetadata(config, externalAuth) {
|
|
106
|
+
return normalizeRaybaseExternalProviderMetadata(config.auth?.externalProviders, externalAuth?.providers);
|
|
107
|
+
}
|
|
108
|
+
export function isRaybaseExternalProviderConfigured(config, providerId, externalAuth) {
|
|
109
|
+
const id = assertRaybaseExternalProviderId(providerId);
|
|
110
|
+
const metadataIds = new Set(raybaseExternalProviderMetadata(config, externalAuth).map((provider) => provider.id));
|
|
111
|
+
const runtimeIds = new Set(externalAuth?.providerIds ?? []);
|
|
112
|
+
return metadataIds.has(id) && runtimeIds.has(id);
|
|
113
|
+
}
|
|
114
|
+
// What an account created by public signup starts with, whether it arrived
|
|
115
|
+
// through the email+password form or through an external provider. Roles for a
|
|
116
|
+
// public account are never taken from the provider; a provider only asserts an
|
|
117
|
+
// identity, never an entitlement.
|
|
118
|
+
export function defaultPublicAppRoles() {
|
|
119
|
+
return normalizeRoles(["user"]);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Decides whether an external provider may create a brand-new user, and with
|
|
123
|
+
* what access, for the `user.create.before` database hook.
|
|
124
|
+
*
|
|
125
|
+
* Two paths, in this order:
|
|
126
|
+
*
|
|
127
|
+
* 1. A pending row (owner setup or invite) minted by
|
|
128
|
+
* `/api/auth/raybase/provider/setup`. It carries the roles, the expected
|
|
129
|
+
* email and a single-use token, so it always decides when its cookie is
|
|
130
|
+
* present — including in public signup mode. A present-but-invalid pending
|
|
131
|
+
* cookie still fails as an invite rather than quietly downgrading to a plain
|
|
132
|
+
* public account, which also keeps the account-create hook's
|
|
133
|
+
* `finalizePendingExternalAuth` from ever meeting a broken pending row.
|
|
134
|
+
* 2. Public signup, when `auth.signupMode` is `"public"` or
|
|
135
|
+
* `"provider-only"`. The visitor arrives with no pending cookie at all and
|
|
136
|
+
* gets exactly what `signUpPublicUser` grants: no operator role and the
|
|
137
|
+
* default app roles. The provider must be one this app configured, and
|
|
138
|
+
* must assert a verified email — the same invariant the pending path
|
|
139
|
+
* enforces.
|
|
140
|
+
*/
|
|
141
|
+
export async function resolveProviderUserCreation(input) {
|
|
142
|
+
const providerId = assertRaybaseExternalProviderId(input.providerId);
|
|
143
|
+
if (await parsePendingExternalAuthCookie(input.secret, input.request)) {
|
|
144
|
+
const pending = await validatePendingExternalAuth(input.db, input.secret, input.request, providerId, input.user);
|
|
145
|
+
return { operatorRole: parsePendingOperatorRole(pending.operatorRole), appRoles: pending.appRolesList, source: "pending" };
|
|
146
|
+
}
|
|
147
|
+
if (!publicSignupEnabled(input.config))
|
|
148
|
+
throw new Error("Provider setup or invite is required");
|
|
149
|
+
if (!isRaybaseExternalProviderConfigured(input.config, providerId, input.externalAuth)) {
|
|
150
|
+
throw new Error("External auth provider is not configured");
|
|
151
|
+
}
|
|
152
|
+
if (input.user.emailVerified !== true && input.user.emailVerified !== 1)
|
|
153
|
+
throw new Error("Provider did not verify the email address");
|
|
154
|
+
if (!String(input.user.email ?? "").trim())
|
|
155
|
+
throw new Error("Provider email is required");
|
|
156
|
+
return { operatorRole: null, appRoles: defaultPublicAppRoles(), source: "public" };
|
|
157
|
+
}
|
|
158
|
+
function sqlLikePattern(value, mode) {
|
|
159
|
+
const text = String(value ?? "").replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
160
|
+
if (mode === "starts_with")
|
|
161
|
+
return `${text}%`;
|
|
162
|
+
if (mode === "ends_with")
|
|
163
|
+
return `%${text}`;
|
|
164
|
+
return `%${text}%`;
|
|
165
|
+
}
|
|
166
|
+
function whereSql(where = []) {
|
|
167
|
+
if (!where.length)
|
|
168
|
+
return { sql: "", params: [] };
|
|
169
|
+
const parts = [];
|
|
170
|
+
const params = [];
|
|
171
|
+
where.forEach((condition, index) => {
|
|
172
|
+
const connector = index === 0 ? "" : ` ${condition.connector} `;
|
|
173
|
+
const field = condition.mode === "insensitive" && typeof condition.value === "string" ? `LOWER(${q(condition.field)})` : q(condition.field);
|
|
174
|
+
const value = condition.mode === "insensitive" && typeof condition.value === "string" ? condition.value.toLowerCase() : condition.value;
|
|
175
|
+
switch (condition.operator) {
|
|
176
|
+
case "eq":
|
|
177
|
+
if (value === null) {
|
|
178
|
+
parts.push(`${connector}${q(condition.field)} IS NULL`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
parts.push(`${connector}${field} = ?`);
|
|
182
|
+
params.push(value);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case "ne":
|
|
186
|
+
if (value === null) {
|
|
187
|
+
parts.push(`${connector}${q(condition.field)} IS NOT NULL`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
parts.push(`${connector}${field} IS NOT ?`);
|
|
191
|
+
params.push(value);
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case "lt":
|
|
195
|
+
case "lte":
|
|
196
|
+
case "gt":
|
|
197
|
+
case "gte": {
|
|
198
|
+
const op = condition.operator === "lt" ? "<" : condition.operator === "lte" ? "<=" : condition.operator === "gt" ? ">" : ">=";
|
|
199
|
+
parts.push(`${connector}${field} ${op} ?`);
|
|
200
|
+
params.push(value);
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "in":
|
|
204
|
+
case "not_in": {
|
|
205
|
+
const values = Array.isArray(value) ? value : [];
|
|
206
|
+
if (!values.length) {
|
|
207
|
+
parts.push(`${connector}${condition.operator === "in" ? "0 = 1" : "1 = 1"}`);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
parts.push(`${connector}${field} ${condition.operator === "in" ? "IN" : "NOT IN"} (${values.map(() => "?").join(", ")})`);
|
|
211
|
+
params.push(...values);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case "contains":
|
|
215
|
+
case "starts_with":
|
|
216
|
+
case "ends_with":
|
|
217
|
+
parts.push(`${connector}${field} LIKE ? ESCAPE '\\'`);
|
|
218
|
+
params.push(sqlLikePattern(value, condition.operator));
|
|
219
|
+
break;
|
|
220
|
+
default:
|
|
221
|
+
throw new Error(`Unsupported Better Auth where operator: ${String(condition.operator)}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return { sql: ` WHERE ${parts.join("")}`, params };
|
|
225
|
+
}
|
|
226
|
+
function selectedColumns(select) {
|
|
227
|
+
return select?.length ? select.map(q).join(", ") : "*";
|
|
228
|
+
}
|
|
229
|
+
function orderSql(sortBy) {
|
|
230
|
+
if (!sortBy?.field)
|
|
231
|
+
return "";
|
|
232
|
+
return ` ORDER BY ${q(sortBy.field)} ${sortBy.direction === "desc" ? "DESC" : "ASC"}`;
|
|
233
|
+
}
|
|
234
|
+
function raybaseBetterAuthAdapter(db) {
|
|
235
|
+
const findOne = ({ model, where, select }) => {
|
|
236
|
+
const compiled = whereSql(where);
|
|
237
|
+
return db.first(`SELECT ${selectedColumns(select)} FROM ${q(model)}${compiled.sql} LIMIT 1`, compiled.params);
|
|
238
|
+
};
|
|
239
|
+
const countRows = async ({ model, where }) => {
|
|
240
|
+
const compiled = whereSql(where);
|
|
241
|
+
const row = await db.first(`SELECT COUNT(*) AS "count" FROM ${q(model)}${compiled.sql}`, compiled.params);
|
|
242
|
+
return Number(row?.count ?? 0);
|
|
243
|
+
};
|
|
244
|
+
const custom = {
|
|
245
|
+
async create({ model, data }) {
|
|
246
|
+
const columns = Object.keys(data);
|
|
247
|
+
if (!columns.length) {
|
|
248
|
+
await db.run(`INSERT INTO ${q(model)} DEFAULT VALUES`);
|
|
249
|
+
return data;
|
|
250
|
+
}
|
|
251
|
+
await db.run(`INSERT INTO ${q(model)} (${columns.map(q).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, columns.map((column) => data[column]));
|
|
252
|
+
if (typeof data.id === "string") {
|
|
253
|
+
return ((await db.first(`SELECT * FROM ${q(model)} WHERE "id" = ?`, [data.id])) ?? data);
|
|
254
|
+
}
|
|
255
|
+
return data;
|
|
256
|
+
},
|
|
257
|
+
async update({ model, where, update }) {
|
|
258
|
+
const columns = Object.keys(update);
|
|
259
|
+
if (!columns.length)
|
|
260
|
+
return findOne({ model, where });
|
|
261
|
+
const compiled = whereSql(where);
|
|
262
|
+
await db.run(`UPDATE ${q(model)} SET ${columns.map((column) => `${q(column)} = ?`).join(", ")}${compiled.sql}`, [
|
|
263
|
+
...columns.map((column) => update[column]),
|
|
264
|
+
...compiled.params
|
|
265
|
+
]);
|
|
266
|
+
return findOne({ model, where });
|
|
267
|
+
},
|
|
268
|
+
async updateMany({ model, where, update }) {
|
|
269
|
+
const before = await countRows({ model, where });
|
|
270
|
+
const columns = Object.keys(update);
|
|
271
|
+
if (!columns.length)
|
|
272
|
+
return before;
|
|
273
|
+
const compiled = whereSql(where);
|
|
274
|
+
await db.run(`UPDATE ${q(model)} SET ${columns.map((column) => `${q(column)} = ?`).join(", ")}${compiled.sql}`, [
|
|
275
|
+
...columns.map((column) => update[column]),
|
|
276
|
+
...compiled.params
|
|
277
|
+
]);
|
|
278
|
+
return before;
|
|
279
|
+
},
|
|
280
|
+
async findOne({ model, where, select }) {
|
|
281
|
+
return findOne({ model, where, select });
|
|
282
|
+
},
|
|
283
|
+
async findMany({ model, where, limit, select, sortBy, offset }) {
|
|
284
|
+
const compiled = whereSql(where);
|
|
285
|
+
const params = [...compiled.params, Math.max(1, Math.min(limit ?? 100, 500)), Math.max(offset ?? 0, 0)];
|
|
286
|
+
return db.all(`SELECT ${selectedColumns(select)} FROM ${q(model)}${compiled.sql}${orderSql(sortBy)} LIMIT ? OFFSET ?`, params);
|
|
287
|
+
},
|
|
288
|
+
async delete({ model, where }) {
|
|
289
|
+
const compiled = whereSql(where);
|
|
290
|
+
await db.run(`DELETE FROM ${q(model)}${compiled.sql}`, compiled.params);
|
|
291
|
+
},
|
|
292
|
+
async deleteMany({ model, where }) {
|
|
293
|
+
const before = await countRows({ model, where });
|
|
294
|
+
const compiled = whereSql(where);
|
|
295
|
+
await db.run(`DELETE FROM ${q(model)}${compiled.sql}`, compiled.params);
|
|
296
|
+
return before;
|
|
297
|
+
},
|
|
298
|
+
async consumeOne({ model, where }) {
|
|
299
|
+
const compiled = whereSql(where);
|
|
300
|
+
const row = await db.first(`SELECT rowid AS "__raybaseRowid", * FROM ${q(model)}${compiled.sql} LIMIT 1`, compiled.params);
|
|
301
|
+
if (!row)
|
|
302
|
+
return null;
|
|
303
|
+
await db.run(`DELETE FROM ${q(model)} WHERE rowid = ?`, [row.__raybaseRowid]);
|
|
304
|
+
delete row.__raybaseRowid;
|
|
305
|
+
return row;
|
|
306
|
+
},
|
|
307
|
+
async count({ model, where }) {
|
|
308
|
+
return countRows({ model, where });
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
return createAdapter({
|
|
312
|
+
config: {
|
|
313
|
+
adapterId: "raybase-do",
|
|
314
|
+
adapterName: "Raybase Durable Object",
|
|
315
|
+
supportsNumericIds: false,
|
|
316
|
+
supportsBooleans: false,
|
|
317
|
+
supportsDates: false,
|
|
318
|
+
supportsJSON: false,
|
|
319
|
+
supportsArrays: false,
|
|
320
|
+
transaction: false
|
|
321
|
+
},
|
|
322
|
+
adapter: () => custom
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
export function authSecret(env) {
|
|
326
|
+
if (env.BETTER_AUTH_SECRET)
|
|
327
|
+
return env.BETTER_AUTH_SECRET;
|
|
328
|
+
if (env.RAYBASE_AUTH_SECRET)
|
|
329
|
+
return env.RAYBASE_AUTH_SECRET;
|
|
330
|
+
if (env.RAYBASE_ENV === "production") {
|
|
331
|
+
throw new Error(`${betterAuthSecretName} is required in production`);
|
|
332
|
+
}
|
|
333
|
+
return "raybase-local-dev-secret";
|
|
334
|
+
}
|
|
335
|
+
function absoluteBaseUrl(value) {
|
|
336
|
+
const trimmed = value?.trim();
|
|
337
|
+
if (!trimmed)
|
|
338
|
+
return undefined;
|
|
339
|
+
return trimmed.startsWith("http://") || trimmed.startsWith("https://") ? trimmed : undefined;
|
|
340
|
+
}
|
|
341
|
+
function authBaseUrl(env) {
|
|
342
|
+
const candidates = [
|
|
343
|
+
env.BETTER_AUTH_URL,
|
|
344
|
+
env.PUBLIC_BETTER_AUTH_URL,
|
|
345
|
+
absoluteBaseUrl(env.BASE_URL)
|
|
346
|
+
];
|
|
347
|
+
for (const candidate of candidates) {
|
|
348
|
+
const value = candidate?.trim();
|
|
349
|
+
if (value)
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
function secureSocialProviders(providers) {
|
|
355
|
+
if (!providers)
|
|
356
|
+
return undefined;
|
|
357
|
+
return Object.fromEntries(Object.entries(providers).map(([rawId, value]) => {
|
|
358
|
+
const id = assertRaybaseExternalProviderId(rawId);
|
|
359
|
+
const secureValue = async () => {
|
|
360
|
+
const resolved = typeof value === "function" ? await value() : value;
|
|
361
|
+
if (!resolved)
|
|
362
|
+
return resolved;
|
|
363
|
+
return {
|
|
364
|
+
...resolved,
|
|
365
|
+
disableImplicitSignUp: true,
|
|
366
|
+
disableSignUp: resolved.disableSignUp === true
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
return [id, secureValue];
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
function secureGenericOAuth(config) {
|
|
373
|
+
return genericOAuthConfig(config).map((provider) => ({
|
|
374
|
+
...provider,
|
|
375
|
+
providerId: assertRaybaseExternalProviderId(String(provider.providerId)),
|
|
376
|
+
disableImplicitSignUp: true,
|
|
377
|
+
disableSignUp: provider.disableSignUp === true
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
function requestFromAuthContext(context) {
|
|
381
|
+
const candidate = context;
|
|
382
|
+
return candidate?.request instanceof Request ? candidate.request : undefined;
|
|
383
|
+
}
|
|
384
|
+
function authContextPath(context) {
|
|
385
|
+
const candidate = context;
|
|
386
|
+
if (typeof candidate?.path === "string")
|
|
387
|
+
return candidate.path;
|
|
388
|
+
const request = requestFromAuthContext(context);
|
|
389
|
+
return request ? new URL(request.url).pathname.replace(/^\/api\/auth/, "") : "";
|
|
390
|
+
}
|
|
391
|
+
function authContextBody(context) {
|
|
392
|
+
const candidate = context;
|
|
393
|
+
return candidate?.body && typeof candidate.body === "object" ? candidate.body : {};
|
|
394
|
+
}
|
|
395
|
+
function authContextParams(context) {
|
|
396
|
+
const candidate = context;
|
|
397
|
+
return candidate?.params && typeof candidate.params === "object" ? candidate.params : {};
|
|
398
|
+
}
|
|
399
|
+
function providerIdFromAuthContext(context, account) {
|
|
400
|
+
if (typeof account?.providerId === "string" && account.providerId !== "credential")
|
|
401
|
+
return account.providerId;
|
|
402
|
+
const params = authContextParams(context);
|
|
403
|
+
const body = authContextBody(context);
|
|
404
|
+
const path = authContextPath(context);
|
|
405
|
+
const id = (typeof params.id === "string" && params.id) ||
|
|
406
|
+
(typeof params.providerId === "string" && params.providerId) ||
|
|
407
|
+
(typeof body.provider === "string" && body.provider) ||
|
|
408
|
+
(typeof body.providerId === "string" && body.providerId) ||
|
|
409
|
+
path.match(/^\/callback\/([^/?#]+)/)?.[1] ||
|
|
410
|
+
path.match(/^\/oauth2\/callback\/([^/?#]+)/)?.[1] ||
|
|
411
|
+
null;
|
|
412
|
+
return id ? assertRaybaseExternalProviderId(id) : null;
|
|
413
|
+
}
|
|
414
|
+
function isProviderUserCreationContext(context) {
|
|
415
|
+
const path = authContextPath(context);
|
|
416
|
+
return path.startsWith("/callback/") || path.startsWith("/oauth2/callback/") || path === "/sign-in/social" || path === "/sign-in/oauth2";
|
|
417
|
+
}
|
|
418
|
+
function authForbidden(message) {
|
|
419
|
+
throw new APIError("FORBIDDEN", { message });
|
|
420
|
+
}
|
|
421
|
+
function raybaseProviderDatabaseHooks(options) {
|
|
422
|
+
const { env, secret, config, externalAuth } = options;
|
|
423
|
+
return {
|
|
424
|
+
user: {
|
|
425
|
+
create: {
|
|
426
|
+
before: async (user, context) => {
|
|
427
|
+
if (!isProviderUserCreationContext(context))
|
|
428
|
+
return;
|
|
429
|
+
const providerId = providerIdFromAuthContext(context);
|
|
430
|
+
if (!providerId)
|
|
431
|
+
return authForbidden("Provider is required");
|
|
432
|
+
try {
|
|
433
|
+
const creation = await resolveProviderUserCreation({
|
|
434
|
+
db: authDb(env),
|
|
435
|
+
secret,
|
|
436
|
+
config,
|
|
437
|
+
externalAuth,
|
|
438
|
+
request: requestFromAuthContext(context),
|
|
439
|
+
providerId,
|
|
440
|
+
user
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
data: {
|
|
444
|
+
operatorRole: creation.operatorRole,
|
|
445
|
+
appRoles: JSON.stringify(creation.appRoles),
|
|
446
|
+
emailVerified: true
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
return authForbidden(error instanceof Error ? error.message : "Provider setup or invite is required");
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
// Better Auth's adapter only writes the fields it knows about, so the
|
|
455
|
+
// roles asked for above never reach the row: the pending path writes
|
|
456
|
+
// them in `finalizePendingExternalAuth`, and public signup writes them
|
|
457
|
+
// here. This is also the first place the new user's id exists, so it is
|
|
458
|
+
// where the public-signup counterpart of `auth.signup.public` is
|
|
459
|
+
// recorded — the pending path has its own `owner.setup.provider` /
|
|
460
|
+
// `invite.accept.provider` event.
|
|
461
|
+
after: async (user, context) => {
|
|
462
|
+
if (!isProviderUserCreationContext(context))
|
|
463
|
+
return;
|
|
464
|
+
const providerId = providerIdFromAuthContext(context);
|
|
465
|
+
if (!providerId)
|
|
466
|
+
return;
|
|
467
|
+
if (await parsePendingExternalAuthCookie(secret, requestFromAuthContext(context)))
|
|
468
|
+
return;
|
|
469
|
+
const userId = String(user.id ?? "");
|
|
470
|
+
if (!userId)
|
|
471
|
+
return;
|
|
472
|
+
const db = authDb(env);
|
|
473
|
+
await setUserAccessRoles(db, userId, null, defaultPublicAppRoles());
|
|
474
|
+
await audit(db, userId, "auth.signup.public.provider", "user", userId, { providerId });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
account: {
|
|
479
|
+
create: {
|
|
480
|
+
after: async (account, context) => {
|
|
481
|
+
const providerId = providerIdFromAuthContext(context, account);
|
|
482
|
+
if (!providerId || providerId === "credential")
|
|
483
|
+
return;
|
|
484
|
+
const request = requestFromAuthContext(context);
|
|
485
|
+
const db = authDb(env);
|
|
486
|
+
const userId = String(account.userId ?? "");
|
|
487
|
+
if (!userId)
|
|
488
|
+
return;
|
|
489
|
+
const user = await db.first(`SELECT "id", "email", "emailVerified" FROM "_auth_users" WHERE "id" = ?`, [userId]);
|
|
490
|
+
if (!user)
|
|
491
|
+
return;
|
|
492
|
+
const pending = await finalizePendingExternalAuth(db, secret, request, providerId, user, { existingUser: true });
|
|
493
|
+
if (!pending) {
|
|
494
|
+
await audit(db, user.id, "auth.provider.link", "user", user.id, { providerId });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
session: {
|
|
500
|
+
create: {
|
|
501
|
+
before: async (session) => {
|
|
502
|
+
const userId = String(session.userId ?? "");
|
|
503
|
+
if (!userId)
|
|
504
|
+
return;
|
|
505
|
+
const row = await authDb(env).first(`SELECT "banned" FROM "_auth_users" WHERE "id" = ?`, [userId]);
|
|
506
|
+
if (row?.banned)
|
|
507
|
+
return authForbidden("Account disabled");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async function passwordLifecycleRequest(auth, request, env, registration) {
|
|
514
|
+
if (request.method !== "POST")
|
|
515
|
+
return auth.handler(request);
|
|
516
|
+
const path = new URL(request.url).pathname.replace(/^\/api\/auth/, "");
|
|
517
|
+
if (path !== "/change-password" && path !== "/reset-password")
|
|
518
|
+
return auth.handler(request);
|
|
519
|
+
let body = {};
|
|
520
|
+
try {
|
|
521
|
+
body = await request.clone().json();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return auth.handler(request);
|
|
525
|
+
}
|
|
526
|
+
const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
|
|
527
|
+
if (!newPassword)
|
|
528
|
+
return auth.handler(request);
|
|
529
|
+
let userId = "";
|
|
530
|
+
let previousPassword;
|
|
531
|
+
if (path === "/change-password") {
|
|
532
|
+
previousPassword = typeof body.currentPassword === "string" ? body.currentPassword : undefined;
|
|
533
|
+
const session = await auth.api.getSession({
|
|
534
|
+
headers: new Headers(request.headers),
|
|
535
|
+
query: { disableCookieCache: true }
|
|
536
|
+
}).catch(() => null);
|
|
537
|
+
userId = typeof session?.user?.id === "string" ? session.user.id : "";
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
const token = (typeof body.token === "string" && body.token) ||
|
|
541
|
+
new URL(request.url).searchParams.get("token") ||
|
|
542
|
+
"";
|
|
543
|
+
if (token) {
|
|
544
|
+
const verification = await authDb(env).first(`SELECT "value" FROM "_auth_verifications" WHERE "identifier" = ?`, [`reset-password:${token}`]);
|
|
545
|
+
userId = verification?.value ?? "";
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const response = await auth.handler(request);
|
|
549
|
+
if (response.ok && userId) {
|
|
550
|
+
await runRaybasePasswordSecretLifecycle({
|
|
551
|
+
db: authDb(env),
|
|
552
|
+
registration,
|
|
553
|
+
event: path === "/change-password" ? "password-change" : "password-reset",
|
|
554
|
+
userId,
|
|
555
|
+
password: newPassword,
|
|
556
|
+
...(path === "/change-password" && previousPassword !== undefined ? { previousPassword } : {}),
|
|
557
|
+
environment: env.RAYBASE_ENV,
|
|
558
|
+
signal: request.signal
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return response;
|
|
562
|
+
}
|
|
563
|
+
export function createRaybaseAuth(env, config, options = {}) {
|
|
564
|
+
const secret = authSecret(env);
|
|
565
|
+
const baseURL = authBaseUrl(env);
|
|
566
|
+
const genericOAuthProviders = secureGenericOAuth(options.externalAuth?.genericOAuth);
|
|
567
|
+
const authEmail = resolveRaybaseAuthEmailConfig(config);
|
|
568
|
+
const signInLinks = resolveRaybaseSignInLinksConfig(config);
|
|
569
|
+
const magicLinkEnabled = authEmail?.magicLink.enabled === true || signInLinks.enabled;
|
|
570
|
+
const emailCodeEnabled = authEmail?.emailCode.enabled === true;
|
|
571
|
+
const requireEmailVerification = authEmail?.emailVerification.enabled === true && options.skipEmailVerification !== true;
|
|
572
|
+
const emailAndPassword = {
|
|
573
|
+
enabled: true,
|
|
574
|
+
minPasswordLength: 10,
|
|
575
|
+
requireEmailVerification,
|
|
576
|
+
...(authEmail?.passwordReset.enabled
|
|
577
|
+
? {
|
|
578
|
+
async sendResetPassword(input, request) {
|
|
579
|
+
const email = String(input.user.email ?? "").toLowerCase();
|
|
580
|
+
const resetUrl = `${appUrlForEmail(config, request)}/reset-password?token=${encodeURIComponent(input.token)}`;
|
|
581
|
+
queueAuthEmail(sendRaybaseAuthEmail(env, config, "passwordReset", { to: email, resetUrl }, { isLocal: options.isLocal, request }), options, authEmailAuditCallback(env, "auth.email.password_reset", null, "user", userIdFromRecord(input.user)));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
: {})
|
|
585
|
+
};
|
|
586
|
+
const emailVerification = authEmail?.emailVerification.enabled === true && options.skipEmailVerification !== true
|
|
587
|
+
? {
|
|
588
|
+
sendOnSignUp: true,
|
|
589
|
+
sendOnSignIn: true,
|
|
590
|
+
autoSignInAfterVerification: true,
|
|
591
|
+
expiresIn: authEmail.emailVerification.expiresIn,
|
|
592
|
+
async sendVerificationEmail(input, request) {
|
|
593
|
+
const email = String(input.user.email ?? "").toLowerCase();
|
|
594
|
+
const verifyUrl = `${appUrlForEmail(config, request)}/verify-email?token=${encodeURIComponent(input.token)}`;
|
|
595
|
+
queueAuthEmail(sendRaybaseAuthEmail(env, config, "emailVerification", { to: email, verifyUrl }, { isLocal: options.isLocal, request }), options, authEmailAuditCallback(env, "auth.email.verification", null, "user", userIdFromRecord(input.user)));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
: undefined;
|
|
599
|
+
const plugins = [
|
|
600
|
+
raybaseSignupGuard(secret),
|
|
601
|
+
...(magicLinkEnabled ? [raybaseMagicLinkExistingUserGuard(env)] : []),
|
|
602
|
+
...(signInLinks.enabled && authEmail?.magicLink.enabled !== true ? [raybaseRuntimeMagicLinkCreationGuard()] : []),
|
|
603
|
+
...(emailCodeEnabled ? [raybaseEmailCodeSignInOnlyGuard()] : []),
|
|
604
|
+
passkey({
|
|
605
|
+
rpName: config.appName,
|
|
606
|
+
schema: {
|
|
607
|
+
passkey: {
|
|
608
|
+
modelName: betterAuthSchema.passkey
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
authenticatorSelection: {
|
|
612
|
+
residentKey: "preferred",
|
|
613
|
+
userVerification: "preferred"
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
];
|
|
617
|
+
if (magicLinkEnabled) {
|
|
618
|
+
plugins.push(magicLink({
|
|
619
|
+
disableSignUp: true,
|
|
620
|
+
expiresIn: authEmail?.magicLink.enabled === true ? authEmail.magicLink.expiresIn : signInLinks.expiresIn,
|
|
621
|
+
async sendMagicLink(input, ctx) {
|
|
622
|
+
if (authEmail?.magicLink.enabled !== true)
|
|
623
|
+
return;
|
|
624
|
+
const user = await getUserByEmail(authDb(env), input.email);
|
|
625
|
+
if (!user || user.disabled)
|
|
626
|
+
return;
|
|
627
|
+
queueAuthEmail(sendRaybaseAuthEmail(env, config, "magicLink", { to: user.email, signInUrl: rewriteEmailUrl(input.url, config, ctx?.request) }, { isLocal: options.isLocal, request: ctx?.request }), options, authEmailAuditCallback(env, "auth.email.magic_link", null, "user", user.id));
|
|
628
|
+
}
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
if (emailCodeEnabled) {
|
|
632
|
+
plugins.push(emailOTP({
|
|
633
|
+
// Core has no guest concept: a code signs in an account that already
|
|
634
|
+
// exists, exactly as the magic link does.
|
|
635
|
+
disableSignUp: true,
|
|
636
|
+
...(authEmail?.emailCode.expiresIn !== undefined ? { expiresIn: authEmail.emailCode.expiresIn } : {}),
|
|
637
|
+
async sendVerificationOTP(input, ctx) {
|
|
638
|
+
if (input.type !== "sign-in")
|
|
639
|
+
return;
|
|
640
|
+
const user = await getUserByEmail(authDb(env), input.email);
|
|
641
|
+
if (!user || user.disabled)
|
|
642
|
+
return;
|
|
643
|
+
queueAuthEmail(sendRaybaseAuthEmail(env, config, "emailCode", { to: user.email, code: input.otp }, { isLocal: options.isLocal, request: ctx?.request }), options, authEmailAuditCallback(env, "auth.email.email_code", null, "user", user.id));
|
|
644
|
+
}
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
if (genericOAuthProviders.length)
|
|
648
|
+
plugins.push(genericOAuth({ config: genericOAuthProviders }));
|
|
649
|
+
const auth = betterAuth({
|
|
650
|
+
appName: config.appName,
|
|
651
|
+
secret,
|
|
652
|
+
// The env arrives with the request, so this instance is rebuilt per request.
|
|
653
|
+
// This logger prints Better Auth's boot diagnostics once per isolate instead
|
|
654
|
+
// of once per request; everything else passes through untouched.
|
|
655
|
+
logger: raybaseBetterAuthLogger,
|
|
656
|
+
...(baseURL ? { baseURL } : {}),
|
|
657
|
+
database: raybaseBetterAuthAdapter(authDb(env)),
|
|
658
|
+
user: {
|
|
659
|
+
modelName: betterAuthSchema.user
|
|
660
|
+
},
|
|
661
|
+
session: {
|
|
662
|
+
modelName: betterAuthSchema.session
|
|
663
|
+
},
|
|
664
|
+
verification: {
|
|
665
|
+
modelName: betterAuthSchema.verification
|
|
666
|
+
},
|
|
667
|
+
emailAndPassword,
|
|
668
|
+
...(emailVerification ? { emailVerification } : {}),
|
|
669
|
+
socialProviders: secureSocialProviders(options.externalAuth?.socialProviders),
|
|
670
|
+
account: {
|
|
671
|
+
modelName: betterAuthSchema.account,
|
|
672
|
+
accountLinking: {
|
|
673
|
+
enabled: true,
|
|
674
|
+
disableImplicitLinking: false,
|
|
675
|
+
requireLocalEmailVerified: true,
|
|
676
|
+
allowDifferentEmails: false,
|
|
677
|
+
updateUserInfoOnLink: false
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
databaseHooks: raybaseProviderDatabaseHooks({ env, secret, config, externalAuth: options.externalAuth }),
|
|
681
|
+
plugins: plugins
|
|
682
|
+
});
|
|
683
|
+
if (!options.passwordSecretLifecycle)
|
|
684
|
+
return auth;
|
|
685
|
+
const wrapped = {
|
|
686
|
+
api: auth.api,
|
|
687
|
+
handler: (request) => passwordLifecycleRequest(auth, request, env, options.passwordSecretLifecycle)
|
|
688
|
+
};
|
|
689
|
+
passwordSecretLifecycleByAuth.set(wrapped, options.passwordSecretLifecycle);
|
|
690
|
+
return wrapped;
|
|
691
|
+
}
|
|
692
|
+
export function authEmailConfigured(config) {
|
|
693
|
+
return Boolean(resolveRaybaseAuthEmailConfig(config));
|
|
694
|
+
}
|
|
695
|
+
export function authEmailFeatureEnabled(config, feature) {
|
|
696
|
+
return isRaybaseAuthEmailFeatureEnabled(config, feature);
|
|
697
|
+
}
|
|
698
|
+
function raybaseSignupGuard(secret) {
|
|
699
|
+
return {
|
|
700
|
+
id: "raybase-signup-guard",
|
|
701
|
+
version: "0.1.0",
|
|
702
|
+
hooks: {
|
|
703
|
+
before: [
|
|
704
|
+
{
|
|
705
|
+
matcher: (ctx) => ctx.path === "/sign-up/email",
|
|
706
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
707
|
+
const header = ctx.headers?.get(internalSignupHeader) ?? ctx.request?.headers.get(internalSignupHeader);
|
|
708
|
+
if (header === secret)
|
|
709
|
+
return;
|
|
710
|
+
throw new APIError("FORBIDDEN", { message: "Invite required" });
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
]
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function raybaseMagicLinkExistingUserGuard(env) {
|
|
718
|
+
return {
|
|
719
|
+
id: "raybase-magic-link-existing-user-guard",
|
|
720
|
+
version: "0.1.0",
|
|
721
|
+
hooks: {
|
|
722
|
+
before: [
|
|
723
|
+
{
|
|
724
|
+
matcher: (ctx) => ctx.path === "/magic-link/verify",
|
|
725
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
726
|
+
const requestUrl = ctx.request?.url;
|
|
727
|
+
if (!requestUrl)
|
|
728
|
+
return;
|
|
729
|
+
const token = new URL(requestUrl).searchParams.get("token");
|
|
730
|
+
if (!token)
|
|
731
|
+
return;
|
|
732
|
+
const db = authDb(env);
|
|
733
|
+
const verification = await db.first(`SELECT "value" FROM "_auth_verifications" WHERE "identifier" = ?`, [token]);
|
|
734
|
+
if (!verification?.value)
|
|
735
|
+
return;
|
|
736
|
+
const value = JSON.parse(verification.value);
|
|
737
|
+
if (!value.email)
|
|
738
|
+
return;
|
|
739
|
+
const user = await getUserByEmail(db, value.email);
|
|
740
|
+
if (!user || user.disabled) {
|
|
741
|
+
throw new APIError("FORBIDDEN", { message: "Magic link sign-in requires an existing invited user" });
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
}
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
// Better Auth's emailOTP plugin also mounts code-based password reset, email
|
|
750
|
+
// change and email verification. `auth.email.emailCode` promises one thing —
|
|
751
|
+
// signing in with a code — so the rest of the plugin's surface is closed rather
|
|
752
|
+
// than left half-wired to templates that do not exist. The deprecated
|
|
753
|
+
// `/forget-password/email-otp` alias sits outside the `/email-otp/` prefix, so
|
|
754
|
+
// the prefix alone would leave one way in.
|
|
755
|
+
function raybaseEmailCodeSignInOnlyGuard() {
|
|
756
|
+
return {
|
|
757
|
+
id: "raybase-email-code-sign-in-only-guard",
|
|
758
|
+
version: "0.1.0",
|
|
759
|
+
hooks: {
|
|
760
|
+
before: [
|
|
761
|
+
{
|
|
762
|
+
matcher: (ctx) => ctx.path?.startsWith("/email-otp/") === true || ctx.path === "/forget-password/email-otp",
|
|
763
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
764
|
+
const type = ctx.body?.type;
|
|
765
|
+
if (ctx.path === "/email-otp/send-verification-otp" && type === "sign-in")
|
|
766
|
+
return;
|
|
767
|
+
throw new APIError("FORBIDDEN", { message: "Email codes are only for signing in" });
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
]
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
function raybaseRuntimeMagicLinkCreationGuard() {
|
|
775
|
+
return {
|
|
776
|
+
id: "raybase-runtime-magic-link-creation-guard",
|
|
777
|
+
version: "0.1.0",
|
|
778
|
+
hooks: {
|
|
779
|
+
before: [
|
|
780
|
+
{
|
|
781
|
+
matcher: (ctx) => ctx.path === "/sign-in/magic-link",
|
|
782
|
+
handler: createAuthMiddleware(async () => {
|
|
783
|
+
throw new APIError("FORBIDDEN", { message: "Email magic-link sign-in is not enabled" });
|
|
784
|
+
})
|
|
785
|
+
}
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function authEmailAuditCallback(env, baseAction, actorId, targetType, targetId, metadata = {}) {
|
|
791
|
+
return async (outcome) => {
|
|
792
|
+
const details = outcome.status === "failed"
|
|
793
|
+
? { error: redactedEmailError(outcome.error) }
|
|
794
|
+
: outcome.result.status === "skipped"
|
|
795
|
+
? { skippedLocal: true, request: outcome.result.requestSummary }
|
|
796
|
+
: { request: outcome.result.requestSummary, response: outcome.result.responseSummary };
|
|
797
|
+
await audit(authDb(env), actorId, `${baseAction}.${outcome.status}`, targetType, targetId ?? undefined, {
|
|
798
|
+
provider: outcome.status === "failed" ? "unknown" : String(outcome.result.requestSummary.provider ?? "cloudflare"),
|
|
799
|
+
...metadata,
|
|
800
|
+
...details
|
|
801
|
+
}).catch(() => undefined);
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function userIdFromRecord(user) {
|
|
805
|
+
return typeof user.id === "string" && user.id ? user.id : null;
|
|
806
|
+
}
|
|
807
|
+
function queueAuthEmail(task, options, onSettled) {
|
|
808
|
+
const queued = { task, onSettled };
|
|
809
|
+
if (options.emailQueue) {
|
|
810
|
+
options.emailQueue.push(queued);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
void settleQueuedAuthEmail(queued);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
export async function settleQueuedAuthEmail(queued) {
|
|
817
|
+
try {
|
|
818
|
+
const result = await queued.task;
|
|
819
|
+
await queued.onSettled?.({ status: result.status === "skipped" ? "skipped" : "sent", result });
|
|
820
|
+
}
|
|
821
|
+
catch (error) {
|
|
822
|
+
console.warn("Raybase email delivery failed.", redactedEmailError(error));
|
|
823
|
+
await queued.onSettled?.({ status: "failed", error });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
export async function getRaybaseSession(request, env, config) {
|
|
827
|
+
const auth = createRaybaseAuth(env, config);
|
|
828
|
+
const session = await auth.api.getSession({
|
|
829
|
+
headers: new Headers(request.headers),
|
|
830
|
+
query: { disableCookieCache: true }
|
|
831
|
+
});
|
|
832
|
+
if (!session?.user || !session.session)
|
|
833
|
+
return null;
|
|
834
|
+
const user = await resolveUser(authDb(env), toRaybaseUser(session.user));
|
|
835
|
+
if (user.disabled)
|
|
836
|
+
return null;
|
|
837
|
+
return { user, principal: { identity: user }, session: session.session };
|
|
838
|
+
}
|
|
839
|
+
export function toRaybaseUser(user) {
|
|
840
|
+
return withHostUserRoleCeiling({
|
|
841
|
+
id: String(user.id),
|
|
842
|
+
email: String(user.email ?? ""),
|
|
843
|
+
name: String(user.name ?? user.email ?? ""),
|
|
844
|
+
operatorRole: null,
|
|
845
|
+
appRoles: parseStoredAppRoles(user.appRoles),
|
|
846
|
+
emailVerified: user.emailVerified === true || user.emailVerified === 1,
|
|
847
|
+
disabled: Boolean(user.banned)
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
export function normalizeRoles(roles) {
|
|
851
|
+
const normalized = roles.map((role) => String(role ?? "").trim()).filter(Boolean);
|
|
852
|
+
return Array.from(new Set(["user", ...normalized]));
|
|
853
|
+
}
|
|
854
|
+
export function defaultOwnerAppRoles(config) {
|
|
855
|
+
return normalizeRoles(["owner", ...(config.roles ?? [])]);
|
|
856
|
+
}
|
|
857
|
+
export function parseStoredAppRoles(value) {
|
|
858
|
+
if (Array.isArray(value))
|
|
859
|
+
return normalizeRoles(value);
|
|
860
|
+
if (typeof value !== "string" || !value.trim())
|
|
861
|
+
return ["user"];
|
|
862
|
+
try {
|
|
863
|
+
const parsed = JSON.parse(value);
|
|
864
|
+
if (Array.isArray(parsed))
|
|
865
|
+
return normalizeRoles(parsed);
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
return normalizeRoles(value.split(","));
|
|
869
|
+
}
|
|
870
|
+
return normalizeRoles([value]);
|
|
871
|
+
}
|
|
872
|
+
async function resolveUser(db, identity) {
|
|
873
|
+
const row = await db.first(`SELECT "id", "email", "name", "emailVerified", "operatorRole", "appRoles", "banned" FROM "_auth_users" WHERE "id" = ?`, [identity.id]);
|
|
874
|
+
if (!row)
|
|
875
|
+
return identity;
|
|
876
|
+
const operatorRole = normalizeStoredAdminRole(row.operatorRole);
|
|
877
|
+
const appRoles = row.appRoles ? parseStoredAppRoles(row.appRoles) : normalizeRoles(["user"]);
|
|
878
|
+
return withHostUserRoleCeiling({
|
|
879
|
+
id: row.id,
|
|
880
|
+
email: row.email,
|
|
881
|
+
name: row.name,
|
|
882
|
+
operatorRole,
|
|
883
|
+
appRoles,
|
|
884
|
+
emailVerified: row.emailVerified === true || row.emailVerified === 1,
|
|
885
|
+
disabled: Boolean(row.banned)
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
export function userFromPrincipal(principal) {
|
|
889
|
+
return principal.identity;
|
|
890
|
+
}
|
|
891
|
+
export async function audit(db, actorId, action, targetType, targetId, metadata) {
|
|
892
|
+
await recordAudit(db, {
|
|
893
|
+
actor: actorAuditRef(actorId),
|
|
894
|
+
action,
|
|
895
|
+
target: { type: targetType, id: targetId },
|
|
896
|
+
metadata
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
export function base64url(bytes) {
|
|
900
|
+
let binary = "";
|
|
901
|
+
for (const byte of bytes)
|
|
902
|
+
binary += String.fromCharCode(byte);
|
|
903
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
904
|
+
}
|
|
905
|
+
export function fromBase64url(value) {
|
|
906
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
907
|
+
const binary = atob(padded);
|
|
908
|
+
const bytes = new Uint8Array(binary.length);
|
|
909
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
910
|
+
bytes[index] = binary.charCodeAt(index);
|
|
911
|
+
return bytes;
|
|
912
|
+
}
|
|
913
|
+
async function hmac(secret, data) {
|
|
914
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
915
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
916
|
+
return base64url(new Uint8Array(signature));
|
|
917
|
+
}
|
|
918
|
+
export async function signToken(secret, payload) {
|
|
919
|
+
const body = {
|
|
920
|
+
purpose: payload.purpose,
|
|
921
|
+
email: payload.email,
|
|
922
|
+
role: payload.role,
|
|
923
|
+
operatorRole: isRaybaseAdminRole(payload.operatorRole) ? payload.operatorRole : undefined,
|
|
924
|
+
roles: payload.roles,
|
|
925
|
+
recovery: payload.recovery === true ? true : undefined,
|
|
926
|
+
jti: randomId("token"),
|
|
927
|
+
exp: Math.floor(wallNowMs() / 1000) + payload.ttlSeconds
|
|
928
|
+
};
|
|
929
|
+
const encoded = base64url(encoder.encode(JSON.stringify(body)));
|
|
930
|
+
return `${encoded}.${await hmac(secret, encoded)}`;
|
|
931
|
+
}
|
|
932
|
+
export async function verifySignedToken(secret, token, purpose) {
|
|
933
|
+
const [encoded, signature] = token.split(".");
|
|
934
|
+
if (!encoded || !signature)
|
|
935
|
+
throw new Error("Invalid token format");
|
|
936
|
+
const expected = await hmac(secret, encoded);
|
|
937
|
+
if (signature !== expected)
|
|
938
|
+
throw new Error("Invalid token signature");
|
|
939
|
+
const payload = JSON.parse(decoder.decode(fromBase64url(encoded)));
|
|
940
|
+
if (payload.purpose !== purpose)
|
|
941
|
+
throw new Error("Wrong token purpose");
|
|
942
|
+
if (payload.exp < Math.floor(wallNowMs() / 1000))
|
|
943
|
+
throw new Error("Token expired");
|
|
944
|
+
return payload;
|
|
945
|
+
}
|
|
946
|
+
export async function ownerConfigured(db) {
|
|
947
|
+
// Host users never hold a role, whatever their row says.
|
|
948
|
+
const row = await db.first(`SELECT "id" FROM "_auth_users" WHERE "operatorRole" = 'owner' AND "id" NOT LIKE 'hostuser\\_%' ESCAPE '\\' LIMIT 1`);
|
|
949
|
+
return Boolean(row);
|
|
950
|
+
}
|
|
951
|
+
export async function consumeSignedToken(db, secret, token, purpose) {
|
|
952
|
+
const payload = await verifySignedToken(secret, token, purpose);
|
|
953
|
+
await assertSignedTokenAvailable(db, token, payload, purpose);
|
|
954
|
+
try {
|
|
955
|
+
await db.run(`INSERT INTO "_used_tokens" ("jti", "purpose", "usedAt") VALUES (?, ?, ?)`, [payload.jti, purpose, nowIso()]);
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
// The primary key is what decides a race between two redemptions of the
|
|
959
|
+
// same token; the loser gets the same answer as a late arrival.
|
|
960
|
+
if (String(error?.message ?? error).includes("UNIQUE constraint failed")) {
|
|
961
|
+
throw new Error("Token already used");
|
|
962
|
+
}
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
return payload;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Undo a `consumeSignedToken` claim. A single-use token is claimed before the
|
|
969
|
+
* work it authorizes, so two concurrent redemptions cannot both succeed; when
|
|
970
|
+
* that work then fails before anything durable exists, the claim has to come
|
|
971
|
+
* back or a mistyped password would permanently destroy the invitation.
|
|
972
|
+
*/
|
|
973
|
+
export async function releaseSignedToken(db, jti, purpose) {
|
|
974
|
+
await db.run(`DELETE FROM "_used_tokens" WHERE "jti" = ? AND "purpose" = ?`, [jti, purpose]);
|
|
975
|
+
}
|
|
976
|
+
export async function previewSignedToken(db, secret, token, purpose) {
|
|
977
|
+
const payload = await verifySignedToken(secret, token, purpose);
|
|
978
|
+
await assertSignedTokenAvailable(db, token, payload, purpose);
|
|
979
|
+
return payload;
|
|
980
|
+
}
|
|
981
|
+
async function assertSignedTokenAvailable(db, token, payload, purpose) {
|
|
982
|
+
const used = await db.first(`SELECT "jti" FROM "_used_tokens" WHERE "jti" = ?`, [payload.jti]);
|
|
983
|
+
if (used)
|
|
984
|
+
throw new Error("Token already used");
|
|
985
|
+
if (purpose === "invite") {
|
|
986
|
+
const invite = await db.first(`SELECT "id", "usedAt", "expiresAt", "status" FROM "_invites" WHERE "tokenHash" = ?`, [await sha256(token)]);
|
|
987
|
+
if (!invite)
|
|
988
|
+
throw new Error("Invite not found");
|
|
989
|
+
if (invite.usedAt)
|
|
990
|
+
throw new Error("Invite already used");
|
|
991
|
+
if (invite.status === "cancelled")
|
|
992
|
+
throw new Error("Invite cancelled");
|
|
993
|
+
if (invite.status === "accepted")
|
|
994
|
+
throw new Error("Invite already used");
|
|
995
|
+
if (new Date(invite.expiresAt).getTime() < wallNowMs()) {
|
|
996
|
+
await db.run(`UPDATE "_invites" SET "status" = 'expired' WHERE "id" = ? AND "status" = 'pending'`, [invite.id]);
|
|
997
|
+
throw new Error("Invite expired");
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
export async function sha256(value) {
|
|
1002
|
+
return base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value))));
|
|
1003
|
+
}
|
|
1004
|
+
function cookieExpires(expiresAt) {
|
|
1005
|
+
return new Date(expiresAt).toUTCString();
|
|
1006
|
+
}
|
|
1007
|
+
function cookieMaxAge(expiresAt) {
|
|
1008
|
+
return Math.max(0, Math.floor((new Date(expiresAt).getTime() - wallNowMs()) / 1000));
|
|
1009
|
+
}
|
|
1010
|
+
async function pendingCookieValue(secret, pendingId, nonce) {
|
|
1011
|
+
const body = `${pendingId}.${nonce}`;
|
|
1012
|
+
return `${body}.${await hmac(secret, body)}`;
|
|
1013
|
+
}
|
|
1014
|
+
export async function createPendingExternalAuthCookie(secret, pendingId, nonce, expiresAt) {
|
|
1015
|
+
const value = await pendingCookieValue(secret, pendingId, nonce);
|
|
1016
|
+
return `${pendingExternalAuthCookieName}=${value}; Path=/api/auth; HttpOnly; SameSite=Lax; Secure; Max-Age=${cookieMaxAge(expiresAt)}; Expires=${cookieExpires(expiresAt)}`;
|
|
1017
|
+
}
|
|
1018
|
+
export function clearPendingExternalAuthCookie() {
|
|
1019
|
+
return `${pendingExternalAuthCookieName}=; Path=/api/auth; HttpOnly; SameSite=Lax; Secure; Max-Age=0`;
|
|
1020
|
+
}
|
|
1021
|
+
function readCookie(request, name) {
|
|
1022
|
+
const header = request?.headers.get("cookie");
|
|
1023
|
+
if (!header)
|
|
1024
|
+
return null;
|
|
1025
|
+
for (const part of header.split(";")) {
|
|
1026
|
+
const [rawName, ...rawValue] = part.trim().split("=");
|
|
1027
|
+
if (rawName === name)
|
|
1028
|
+
return rawValue.join("=") || "";
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
async function parsePendingExternalAuthCookie(secret, request) {
|
|
1033
|
+
const value = readCookie(request, pendingExternalAuthCookieName);
|
|
1034
|
+
if (!value)
|
|
1035
|
+
return null;
|
|
1036
|
+
const [id, nonce, signature] = value.split(".");
|
|
1037
|
+
if (!id || !nonce || !signature)
|
|
1038
|
+
return null;
|
|
1039
|
+
const expected = await hmac(secret, `${id}.${nonce}`);
|
|
1040
|
+
if (signature !== expected)
|
|
1041
|
+
return null;
|
|
1042
|
+
return { id, nonce };
|
|
1043
|
+
}
|
|
1044
|
+
function parsePendingRoles(value) {
|
|
1045
|
+
try {
|
|
1046
|
+
const parsed = JSON.parse(value);
|
|
1047
|
+
if (Array.isArray(parsed))
|
|
1048
|
+
return normalizeRoles(parsed);
|
|
1049
|
+
}
|
|
1050
|
+
catch { }
|
|
1051
|
+
return normalizeRoles(["user"]);
|
|
1052
|
+
}
|
|
1053
|
+
export async function createPendingExternalAuth(db, secret, config, input) {
|
|
1054
|
+
const providerId = assertRaybaseExternalProviderId(input.providerId);
|
|
1055
|
+
const payload = await verifySignedToken(secret, input.token, input.purpose);
|
|
1056
|
+
await assertSignedTokenAvailable(db, input.token, payload, input.purpose);
|
|
1057
|
+
const tokenHash = await sha256(input.token);
|
|
1058
|
+
const now = wallNowMs();
|
|
1059
|
+
const expiresAt = new Date(now + (input.ttlSeconds ?? pendingExternalAuthTtlSeconds) * 1000).toISOString();
|
|
1060
|
+
const id = randomId("pending_auth");
|
|
1061
|
+
const nonce = randomId("nonce");
|
|
1062
|
+
const appRoles = input.purpose === "owner-setup" ? defaultOwnerAppRoles(config) : normalizeRoles(payload.roles ?? [payload.role ?? "user"]);
|
|
1063
|
+
const operatorRole = input.purpose === "owner-setup" ? "owner" : parsePendingOperatorRole(payload.operatorRole ?? null);
|
|
1064
|
+
await db.run(`UPDATE "_auth_pending"
|
|
1065
|
+
SET "status" = 'expired', "error" = 'replaced', "consumedAt" = ?
|
|
1066
|
+
WHERE "tokenJti" = ? AND "status" = 'started'`, [nowIso(), payload.jti]);
|
|
1067
|
+
await db.run(`INSERT INTO "_auth_pending" (
|
|
1068
|
+
"id",
|
|
1069
|
+
"purpose",
|
|
1070
|
+
"tokenJti",
|
|
1071
|
+
"tokenHash",
|
|
1072
|
+
"providerId",
|
|
1073
|
+
"expectedEmail",
|
|
1074
|
+
"operatorRole",
|
|
1075
|
+
"appRoles",
|
|
1076
|
+
"nonceHash",
|
|
1077
|
+
"status",
|
|
1078
|
+
"updatesChoice",
|
|
1079
|
+
"expiresAt"
|
|
1080
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'started', ?, ?)`, [
|
|
1081
|
+
id,
|
|
1082
|
+
input.purpose,
|
|
1083
|
+
payload.jti,
|
|
1084
|
+
tokenHash,
|
|
1085
|
+
providerId,
|
|
1086
|
+
payload.email?.toLowerCase() ?? null,
|
|
1087
|
+
operatorRole,
|
|
1088
|
+
JSON.stringify(appRoles),
|
|
1089
|
+
await sha256(nonce),
|
|
1090
|
+
input.updatesChoice ?? null,
|
|
1091
|
+
expiresAt
|
|
1092
|
+
]);
|
|
1093
|
+
const pending = await db.first(`SELECT * FROM "_auth_pending" WHERE "id" = ?`, [id]);
|
|
1094
|
+
if (!pending)
|
|
1095
|
+
throw new Error("Unable to create provider auth state");
|
|
1096
|
+
return { pending, cookie: await createPendingExternalAuthCookie(secret, id, nonce, expiresAt) };
|
|
1097
|
+
}
|
|
1098
|
+
function parsePendingOperatorRole(value) {
|
|
1099
|
+
return isRaybaseAdminRole(value) ? value : null;
|
|
1100
|
+
}
|
|
1101
|
+
export async function expirePendingExternalAuth(db, pendingId, reason = "expired") {
|
|
1102
|
+
await db.run(`UPDATE "_auth_pending"
|
|
1103
|
+
SET "status" = 'expired', "error" = ?, "consumedAt" = ?
|
|
1104
|
+
WHERE "id" = ? AND "status" = 'started'`, [reason, nowIso(), pendingId]);
|
|
1105
|
+
}
|
|
1106
|
+
async function validateInviteHashAvailable(db, tokenHash) {
|
|
1107
|
+
const invite = await db.first(`SELECT "id", "usedAt", "expiresAt", "status" FROM "_invites" WHERE "tokenHash" = ?`, [tokenHash]);
|
|
1108
|
+
if (!invite)
|
|
1109
|
+
throw new Error("Invite not found");
|
|
1110
|
+
if (invite.usedAt)
|
|
1111
|
+
throw new Error("Invite already used");
|
|
1112
|
+
if (invite.status === "cancelled")
|
|
1113
|
+
throw new Error("Invite cancelled");
|
|
1114
|
+
if (invite.status === "accepted")
|
|
1115
|
+
throw new Error("Invite already used");
|
|
1116
|
+
if (new Date(invite.expiresAt).getTime() < wallNowMs()) {
|
|
1117
|
+
await db.run(`UPDATE "_invites" SET "status" = 'expired' WHERE "id" = ? AND "status" = 'pending'`, [invite.id]);
|
|
1118
|
+
throw new Error("Invite expired");
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
export async function validatePendingExternalAuth(db, secret, request, providerId, user) {
|
|
1122
|
+
const id = assertRaybaseExternalProviderId(providerId);
|
|
1123
|
+
const cookie = await parsePendingExternalAuthCookie(secret, request);
|
|
1124
|
+
if (!cookie)
|
|
1125
|
+
throw new Error("Provider setup or invite is required");
|
|
1126
|
+
const pending = await db.first(`SELECT * FROM "_auth_pending" WHERE "id" = ? AND "status" = 'started'`, [cookie.id]);
|
|
1127
|
+
if (!pending)
|
|
1128
|
+
throw new Error("Provider setup or invite is required");
|
|
1129
|
+
if (pending.providerId !== id)
|
|
1130
|
+
throw new Error("Provider does not match pending setup");
|
|
1131
|
+
if ((await sha256(cookie.nonce)) !== pending.nonceHash)
|
|
1132
|
+
throw new Error("Provider setup state is invalid");
|
|
1133
|
+
if (new Date(pending.expiresAt).getTime() < wallNowMs()) {
|
|
1134
|
+
await expirePendingExternalAuth(db, pending.id);
|
|
1135
|
+
throw new Error("Provider setup expired");
|
|
1136
|
+
}
|
|
1137
|
+
const used = await db.first(`SELECT "jti" FROM "_used_tokens" WHERE "jti" = ?`, [pending.tokenJti]);
|
|
1138
|
+
if (used)
|
|
1139
|
+
throw new Error("Token already used");
|
|
1140
|
+
if (pending.purpose === "invite")
|
|
1141
|
+
await validateInviteHashAvailable(db, pending.tokenHash);
|
|
1142
|
+
if (user.emailVerified !== true && user.emailVerified !== 1)
|
|
1143
|
+
throw new Error("Provider did not verify the email address");
|
|
1144
|
+
const email = String(user.email ?? "").toLowerCase();
|
|
1145
|
+
if (!email)
|
|
1146
|
+
throw new Error("Provider email is required");
|
|
1147
|
+
if (pending.expectedEmail && pending.expectedEmail.toLowerCase() !== email)
|
|
1148
|
+
throw new Error("Provider email does not match this invite");
|
|
1149
|
+
return { ...pending, appRolesList: parsePendingRoles(pending.appRoles) };
|
|
1150
|
+
}
|
|
1151
|
+
export async function finalizePendingExternalAuth(db, secret, request, providerId, user, options = {}) {
|
|
1152
|
+
const cookie = await parsePendingExternalAuthCookie(secret, request);
|
|
1153
|
+
if (!cookie)
|
|
1154
|
+
return null;
|
|
1155
|
+
const pending = await validatePendingExternalAuth(db, secret, request, providerId, user);
|
|
1156
|
+
await setUserAccessRoles(db, user.id, parsePendingOperatorRole(pending.operatorRole), pending.appRolesList);
|
|
1157
|
+
await db.run(`INSERT INTO "_used_tokens" ("jti", "purpose", "usedAt") VALUES (?, ?, ?)`, [pending.tokenJti, pending.purpose, nowIso()]);
|
|
1158
|
+
if (pending.purpose === "invite") {
|
|
1159
|
+
await db.run(`UPDATE "_invites" SET "usedAt" = ?, "status" = 'accepted' WHERE "tokenHash" = ?`, [nowIso(), pending.tokenHash]);
|
|
1160
|
+
}
|
|
1161
|
+
await db.run(`UPDATE "_auth_pending" SET "status" = 'consumed', "consumedAt" = ? WHERE "id" = ?`, [nowIso(), pending.id]);
|
|
1162
|
+
await audit(db, user.id, pending.purpose === "owner-setup" ? "owner.setup.provider" : "invite.accept.provider", "user", user.id, { providerId, purpose: pending.purpose, existingUser: options.existingUser === true });
|
|
1163
|
+
return pending;
|
|
1164
|
+
}
|
|
1165
|
+
export async function createInvite(db, secret, input) {
|
|
1166
|
+
const ttlSeconds = input.ttlSeconds ?? 7 * 24 * 60 * 60;
|
|
1167
|
+
const roles = normalizeRoles(input.roles?.length ? input.roles : [input.role]);
|
|
1168
|
+
const requestedRole = String(input.role ?? "").trim();
|
|
1169
|
+
const primaryRole = requestedRole && roles.includes(requestedRole) ? requestedRole : roles[0];
|
|
1170
|
+
const token = await signToken(secret, {
|
|
1171
|
+
purpose: "invite",
|
|
1172
|
+
email: input.email,
|
|
1173
|
+
role: primaryRole,
|
|
1174
|
+
operatorRole: input.operatorRole ?? undefined,
|
|
1175
|
+
roles,
|
|
1176
|
+
ttlSeconds
|
|
1177
|
+
});
|
|
1178
|
+
const payload = await verifySignedToken(secret, token, "invite");
|
|
1179
|
+
const expiresAt = new Date(payload.exp * 1000).toISOString();
|
|
1180
|
+
const id = randomId("invite");
|
|
1181
|
+
await db.run(`INSERT INTO "_invites" ("id", "tokenHash", "email", "role", "roles", "status", "createdBy", "note", "expiresAt")
|
|
1182
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?)`, [
|
|
1183
|
+
id,
|
|
1184
|
+
await sha256(token),
|
|
1185
|
+
input.email?.trim().toLowerCase() || null,
|
|
1186
|
+
primaryRole,
|
|
1187
|
+
JSON.stringify(roles),
|
|
1188
|
+
input.createdBy ?? null,
|
|
1189
|
+
input.note?.trim() || null,
|
|
1190
|
+
expiresAt
|
|
1191
|
+
]);
|
|
1192
|
+
return { id, token, expiresAt };
|
|
1193
|
+
}
|
|
1194
|
+
function parseInviteRoles(value) {
|
|
1195
|
+
if (Array.isArray(value))
|
|
1196
|
+
return value.map(String);
|
|
1197
|
+
if (typeof value !== "string")
|
|
1198
|
+
return [];
|
|
1199
|
+
try {
|
|
1200
|
+
const parsed = JSON.parse(value);
|
|
1201
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
return [];
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
export async function listInvites(db, options = {}) {
|
|
1208
|
+
const where = options.status ? ` WHERE "status" = ?` : "";
|
|
1209
|
+
const whereParams = options.status ? [options.status] : [];
|
|
1210
|
+
const limit = Math.min(Math.max(options.limit ?? 100, 1), 500);
|
|
1211
|
+
const offset = Math.max(options.offset ?? 0, 0);
|
|
1212
|
+
const rows = await db.all(`SELECT "id", "email", "roles", "status", "note", "expiresAt", "usedAt", "createdAt"
|
|
1213
|
+
FROM "_invites"${where} ORDER BY "createdAt" DESC LIMIT ? OFFSET ?`, [...whereParams, limit, offset]);
|
|
1214
|
+
const total = await db.first(`SELECT COUNT(*) AS "count" FROM "_invites"${where}`, whereParams);
|
|
1215
|
+
return {
|
|
1216
|
+
invites: rows.map((row) => ({
|
|
1217
|
+
id: String(row.id),
|
|
1218
|
+
email: row.email === null ? null : String(row.email),
|
|
1219
|
+
appRoles: normalizeRoles(parseInviteRoles(row.roles)),
|
|
1220
|
+
status: String(row.status),
|
|
1221
|
+
note: row.note === null ? null : String(row.note),
|
|
1222
|
+
expiresAt: String(row.expiresAt),
|
|
1223
|
+
usedAt: row.usedAt === null ? null : String(row.usedAt),
|
|
1224
|
+
createdAt: String(row.createdAt)
|
|
1225
|
+
})),
|
|
1226
|
+
total: Number(total?.count ?? 0)
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
export async function markInviteUsed(db, token) {
|
|
1230
|
+
await db.run(`UPDATE "_invites" SET "usedAt" = ?, "status" = 'accepted' WHERE "tokenHash" = ?`, [nowIso(), await sha256(token)]);
|
|
1231
|
+
}
|
|
1232
|
+
export async function cancelInvite(db, inviteId) {
|
|
1233
|
+
await db.run(`UPDATE "_invites" SET "status" = 'cancelled' WHERE "id" = ? AND "status" = 'pending'`, [inviteId]);
|
|
1234
|
+
}
|
|
1235
|
+
async function createRaybaseSignInLinkVerification(db, user, expiresIn) {
|
|
1236
|
+
const tokenBytes = new Uint8Array(32);
|
|
1237
|
+
crypto.getRandomValues(tokenBytes);
|
|
1238
|
+
const token = base64url(tokenBytes);
|
|
1239
|
+
const createdAt = wallNowMs();
|
|
1240
|
+
const expiresAtMs = createdAt + expiresIn * 1_000;
|
|
1241
|
+
await db.run(`INSERT INTO "_auth_verifications" ("id", "identifier", "value", "expiresAt", "createdAt", "updatedAt")
|
|
1242
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [randomId("verification"), token, JSON.stringify({ email: user.email, name: user.name }), expiresAtMs, createdAt, createdAt]);
|
|
1243
|
+
return { token, expiresAt: new Date(expiresAtMs).toISOString() };
|
|
1244
|
+
}
|
|
1245
|
+
function normalizeRaybaseSignInLinkRedirect(redirectTo) {
|
|
1246
|
+
if (typeof redirectTo !== "string" || !redirectTo.startsWith("/") || redirectTo.startsWith("//") || redirectTo.includes("\\")) {
|
|
1247
|
+
throw new Error("ctx.auth.signInLinks.create redirectTo must be a root-relative path on the current origin");
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
const base = new URL("https://raybase-sign-in-link.invalid/");
|
|
1251
|
+
const resolved = new URL(redirectTo, base);
|
|
1252
|
+
if (resolved.origin !== base.origin || resolved.pathname.startsWith("//"))
|
|
1253
|
+
throw new Error("origin changed");
|
|
1254
|
+
return `${resolved.pathname}${resolved.search}${resolved.hash}`;
|
|
1255
|
+
}
|
|
1256
|
+
catch {
|
|
1257
|
+
throw new Error("ctx.auth.signInLinks.create redirectTo must be a valid root-relative path on the current origin");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
export function createRaybaseAuthContextRuntime(input) {
|
|
1261
|
+
// Invites and sign-in links are issued for the person an agent runs on
|
|
1262
|
+
// behalf of, never for the agent itself.
|
|
1263
|
+
const actingPrincipal = principalOnBehalfOf(input.principal);
|
|
1264
|
+
const audit = input.audit ?? (async (event) => {
|
|
1265
|
+
await recordAudit(input.db, {
|
|
1266
|
+
actor: input.principal,
|
|
1267
|
+
action: event.action,
|
|
1268
|
+
target: event.target,
|
|
1269
|
+
metadata: event.metadata,
|
|
1270
|
+
source: event.source
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
return {
|
|
1274
|
+
invites: {
|
|
1275
|
+
async create(options = {}) {
|
|
1276
|
+
if (options.expiresIn !== undefined && (!Number.isInteger(options.expiresIn) || options.expiresIn <= 0)) {
|
|
1277
|
+
throw new Error("ctx.auth.invites.create expiresIn must be a positive integer number of seconds");
|
|
1278
|
+
}
|
|
1279
|
+
const note = options.note?.trim();
|
|
1280
|
+
if (note && note.length > 1_000)
|
|
1281
|
+
throw new Error("ctx.auth.invites.create note must be at most 1000 characters");
|
|
1282
|
+
const roles = normalizeRoles(options.appRoles?.length ? options.appRoles : ["user"]);
|
|
1283
|
+
const invite = await createInvite(input.db, input.secret, {
|
|
1284
|
+
email: options.email,
|
|
1285
|
+
role: roles.find((role) => role !== "user") ?? roles[0],
|
|
1286
|
+
roles,
|
|
1287
|
+
createdBy: actingPrincipal.kind === "user" || actingPrincipal.kind === "operator"
|
|
1288
|
+
? principalId(actingPrincipal)
|
|
1289
|
+
: null,
|
|
1290
|
+
ttlSeconds: options.expiresIn,
|
|
1291
|
+
note
|
|
1292
|
+
});
|
|
1293
|
+
const path = `/invite/${invite.token}`;
|
|
1294
|
+
const url = input.request ? new URL(path, input.request.url).toString() : undefined;
|
|
1295
|
+
await audit({
|
|
1296
|
+
action: "invite.create",
|
|
1297
|
+
target: { type: "_invites", id: invite.id },
|
|
1298
|
+
metadata: {
|
|
1299
|
+
email: options.email?.trim().toLowerCase() || null,
|
|
1300
|
+
appRoles: roles,
|
|
1301
|
+
note: note || null,
|
|
1302
|
+
runtimeMinted: true
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
return { ...invite, path, ...(url ? { url } : {}) };
|
|
1306
|
+
},
|
|
1307
|
+
list: (options) => listInvites(input.db, options),
|
|
1308
|
+
async revoke(inviteId) {
|
|
1309
|
+
const id = inviteId.trim();
|
|
1310
|
+
if (!id)
|
|
1311
|
+
throw new Error("Invite id is required");
|
|
1312
|
+
const existing = await input.db.first(`SELECT "status" FROM "_invites" WHERE "id" = ?`, [id]);
|
|
1313
|
+
const revoked = existing?.status === "pending";
|
|
1314
|
+
if (revoked)
|
|
1315
|
+
await cancelInvite(input.db, id);
|
|
1316
|
+
await audit({
|
|
1317
|
+
action: "invite.cancel",
|
|
1318
|
+
target: { type: "_invites", id },
|
|
1319
|
+
metadata: { runtimeMinted: true, revoked }
|
|
1320
|
+
});
|
|
1321
|
+
return { revoked };
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
signInLinks: {
|
|
1325
|
+
async create(options = {}) {
|
|
1326
|
+
const configured = input.config.auth?.signInLinks;
|
|
1327
|
+
if (configured?.enabled !== true)
|
|
1328
|
+
throw new Error("ctx.auth.signInLinks is not enabled");
|
|
1329
|
+
if (!input.sessionAuthenticated || !input.request) {
|
|
1330
|
+
throw new Error("ctx.auth.signInLinks.create requires a cookie-session-authenticated app request");
|
|
1331
|
+
}
|
|
1332
|
+
if (actingPrincipal.kind !== "user") {
|
|
1333
|
+
throw new Error("ctx.auth.signInLinks.create requires a user principal");
|
|
1334
|
+
}
|
|
1335
|
+
const resolved = resolveRaybaseSignInLinksConfig(input.config);
|
|
1336
|
+
const expiresIn = options.expiresIn ?? resolved.expiresIn;
|
|
1337
|
+
if (!Number.isInteger(expiresIn) || expiresIn <= 0 || expiresIn > raybaseSignInLinkMaximumExpiresIn) {
|
|
1338
|
+
throw new Error(`ctx.auth.signInLinks.create expiresIn must be a positive integer no greater than ${raybaseSignInLinkMaximumExpiresIn} seconds`);
|
|
1339
|
+
}
|
|
1340
|
+
const redirectTo = normalizeRaybaseSignInLinkRedirect(options.redirectTo ?? "/");
|
|
1341
|
+
const userId = actingPrincipal.user.identityId ?? actingPrincipal.userId ?? actingPrincipal.user.id;
|
|
1342
|
+
const user = await input.db.first(`SELECT "id", "email", "name", "emailVerified", "operatorRole", "appRoles", "banned" FROM "_auth_users" WHERE "id" = ?`, [userId]);
|
|
1343
|
+
if (!user)
|
|
1344
|
+
throw new Error("ctx.auth.signInLinks.create requires a current auth user");
|
|
1345
|
+
if (user.banned === true || user.banned === 1)
|
|
1346
|
+
throw new Error("ctx.auth.signInLinks.create is unavailable for a disabled account");
|
|
1347
|
+
const email = typeof user.email === "string" ? user.email.trim().toLowerCase() : "";
|
|
1348
|
+
if (!email)
|
|
1349
|
+
throw new Error("ctx.auth.signInLinks.create requires a current auth user email");
|
|
1350
|
+
if (user.emailVerified !== true && user.emailVerified !== 1) {
|
|
1351
|
+
throw new Error("ctx.auth.signInLinks.create requires an already-verified email address");
|
|
1352
|
+
}
|
|
1353
|
+
const verification = await createRaybaseSignInLinkVerification(input.db, { email, name: user.name }, expiresIn);
|
|
1354
|
+
// Better Auth 1.6 rejects hashes in relative callback URLs even though
|
|
1355
|
+
// they are valid same-origin navigation targets. Supplying the already
|
|
1356
|
+
// validated current-origin absolute URL preserves pathname/query/hash
|
|
1357
|
+
// while keeping its verifier and origin check in charge.
|
|
1358
|
+
const callbackURL = new URL(redirectTo, input.request.url).toString();
|
|
1359
|
+
const query = new URLSearchParams({ token: verification.token, callbackURL });
|
|
1360
|
+
const path = `/api/auth/magic-link/verify?${query.toString()}`;
|
|
1361
|
+
const url = new URL(path, input.request.url).toString();
|
|
1362
|
+
await audit({
|
|
1363
|
+
action: "auth.sign_in_link.create",
|
|
1364
|
+
target: { type: "_auth_users", id: userId },
|
|
1365
|
+
metadata: { expiresIn, redirectTo, runtimeMinted: true }
|
|
1366
|
+
});
|
|
1367
|
+
return { path, url, expiresAt: verification.expiresAt };
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
export const unsupportedRaybaseAuthContextRuntime = {
|
|
1373
|
+
invites: {
|
|
1374
|
+
async create() {
|
|
1375
|
+
throw new Error("ctx.auth.invites is not available from this Raybase adapter");
|
|
1376
|
+
},
|
|
1377
|
+
async list() {
|
|
1378
|
+
throw new Error("ctx.auth.invites is not available from this Raybase adapter");
|
|
1379
|
+
},
|
|
1380
|
+
async revoke() {
|
|
1381
|
+
throw new Error("ctx.auth.invites is not available from this Raybase adapter");
|
|
1382
|
+
}
|
|
1383
|
+
},
|
|
1384
|
+
signInLinks: {
|
|
1385
|
+
async create() {
|
|
1386
|
+
throw new Error("ctx.auth.signInLinks is not available from this Raybase adapter");
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
export function csrfAllowed(request) {
|
|
1391
|
+
if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS")
|
|
1392
|
+
return true;
|
|
1393
|
+
const url = new URL(request.url);
|
|
1394
|
+
const origin = request.headers.get("Origin");
|
|
1395
|
+
if (origin)
|
|
1396
|
+
return new URL(origin).host === url.host;
|
|
1397
|
+
const referer = request.headers.get("Referer");
|
|
1398
|
+
return referer ? new URL(referer).host === url.host : false;
|
|
1399
|
+
}
|
|
1400
|
+
export function headersForInternalAuth(request, env) {
|
|
1401
|
+
const headers = new Headers(request.headers);
|
|
1402
|
+
headers.set(internalSignupHeader, authSecret(env));
|
|
1403
|
+
return headers;
|
|
1404
|
+
}
|
|
1405
|
+
export async function signUpWithRaybaseToken(auth, request, env, input) {
|
|
1406
|
+
const result = await auth.api.signUpEmail({
|
|
1407
|
+
body: {
|
|
1408
|
+
email: input.email.toLowerCase(),
|
|
1409
|
+
name: input.name,
|
|
1410
|
+
password: input.password,
|
|
1411
|
+
rememberMe: true
|
|
1412
|
+
},
|
|
1413
|
+
headers: headersForInternalAuth(request, env),
|
|
1414
|
+
returnHeaders: true
|
|
1415
|
+
});
|
|
1416
|
+
const user = toRaybaseUser(result.response.user);
|
|
1417
|
+
const db = authDb(env);
|
|
1418
|
+
await setUserAdminRole(db, user.id, input.operatorRole);
|
|
1419
|
+
await setUserAppRoles(db, user.id, input.appRoles ?? ["user"]);
|
|
1420
|
+
if (input.emailVerified !== false)
|
|
1421
|
+
await setUserEmailVerified(db, user.id, true);
|
|
1422
|
+
const appRoles = normalizeRoles(input.appRoles ?? ["user"]);
|
|
1423
|
+
await runRaybasePasswordSecretLifecycle({
|
|
1424
|
+
db,
|
|
1425
|
+
registration: passwordSecretLifecycleByAuth.get(auth),
|
|
1426
|
+
event: "signup",
|
|
1427
|
+
userId: user.id,
|
|
1428
|
+
password: input.password,
|
|
1429
|
+
environment: env.RAYBASE_ENV,
|
|
1430
|
+
signal: request.signal
|
|
1431
|
+
});
|
|
1432
|
+
return {
|
|
1433
|
+
headers: result.headers,
|
|
1434
|
+
user: { ...user, operatorRole: input.operatorRole, appRoles, emailVerified: input.emailVerified !== false }
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
export async function signUpPublicUser(auth, request, env, input) {
|
|
1438
|
+
return signUpWithRaybaseToken(auth, request, env, {
|
|
1439
|
+
email: input.email,
|
|
1440
|
+
name: input.name,
|
|
1441
|
+
password: input.password,
|
|
1442
|
+
operatorRole: null,
|
|
1443
|
+
appRoles: defaultPublicAppRoles(),
|
|
1444
|
+
emailVerified: false
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
export async function signInWithPassword(auth, request, env, config, input) {
|
|
1448
|
+
const db = authDb(env);
|
|
1449
|
+
const existing = await getUserByEmail(db, input.email);
|
|
1450
|
+
if (existing?.disabled)
|
|
1451
|
+
throw new Error("Account disabled");
|
|
1452
|
+
const result = await auth.api.signInEmail({
|
|
1453
|
+
body: {
|
|
1454
|
+
email: input.email.toLowerCase(),
|
|
1455
|
+
password: input.password,
|
|
1456
|
+
rememberMe: true
|
|
1457
|
+
},
|
|
1458
|
+
headers: new Headers(request.headers),
|
|
1459
|
+
returnHeaders: true
|
|
1460
|
+
});
|
|
1461
|
+
const identity = toRaybaseUser(result.response.user);
|
|
1462
|
+
const user = await resolveUser(db, identity);
|
|
1463
|
+
if (user.disabled)
|
|
1464
|
+
throw new Error("Account disabled");
|
|
1465
|
+
await runRaybasePasswordSecretLifecycle({
|
|
1466
|
+
db,
|
|
1467
|
+
registration: passwordSecretLifecycleByAuth.get(auth),
|
|
1468
|
+
event: "login",
|
|
1469
|
+
userId: user.id,
|
|
1470
|
+
password: input.password,
|
|
1471
|
+
environment: env.RAYBASE_ENV,
|
|
1472
|
+
signal: request.signal
|
|
1473
|
+
});
|
|
1474
|
+
return {
|
|
1475
|
+
headers: result.headers,
|
|
1476
|
+
user
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
export async function signOut(auth, request) {
|
|
1480
|
+
const result = await auth.api.signOut({
|
|
1481
|
+
headers: new Headers(request.headers),
|
|
1482
|
+
returnHeaders: true
|
|
1483
|
+
});
|
|
1484
|
+
return result.headers;
|
|
1485
|
+
}
|
|
1486
|
+
export async function setUserAdminRole(db, userId, role) {
|
|
1487
|
+
assertNotHostUser(userId, "Changing the admin role");
|
|
1488
|
+
await db.run(`UPDATE "_auth_users" SET "operatorRole" = ?, "updatedAt" = ? WHERE "id" = ?`, [role, wallNowMs(), userId]);
|
|
1489
|
+
}
|
|
1490
|
+
export async function setUserAppRoles(db, userId, appRoles) {
|
|
1491
|
+
assertNotHostUser(userId, "Changing app roles");
|
|
1492
|
+
await db.run(`UPDATE "_auth_users" SET "appRoles" = ?, "updatedAt" = ? WHERE "id" = ?`, [JSON.stringify(normalizeRoles(appRoles)), wallNowMs(), userId]);
|
|
1493
|
+
}
|
|
1494
|
+
export async function setUserEmailVerified(db, userId, verified) {
|
|
1495
|
+
assertNotHostUser(userId, "Verifying the email address");
|
|
1496
|
+
await db.run(`UPDATE "_auth_users" SET "emailVerified" = ?, "updatedAt" = ? WHERE "id" = ?`, [verified ? 1 : 0, wallNowMs(), userId]);
|
|
1497
|
+
}
|
|
1498
|
+
async function setUserAccessRoles(db, userId, operatorRole, appRoles) {
|
|
1499
|
+
assertNotHostUser(userId, "Changing roles");
|
|
1500
|
+
await db.run(`UPDATE "_auth_users" SET "operatorRole" = ?, "appRoles" = ?, "updatedAt" = ? WHERE "id" = ?`, [
|
|
1501
|
+
operatorRole,
|
|
1502
|
+
JSON.stringify(normalizeRoles(appRoles)),
|
|
1503
|
+
wallNowMs(),
|
|
1504
|
+
userId
|
|
1505
|
+
]);
|
|
1506
|
+
}
|
|
1507
|
+
export async function getUserByEmail(db, email) {
|
|
1508
|
+
const row = await db.first(`SELECT "id", "email", "name", "emailVerified", "operatorRole", "appRoles", "banned" FROM "_auth_users" WHERE lower("email") = lower(?)`, [email]);
|
|
1509
|
+
return row ? rowToRaybaseUser(row) : null;
|
|
1510
|
+
}
|
|
1511
|
+
export async function getUserById(db, id) {
|
|
1512
|
+
const row = await db.first(`SELECT "id", "email", "name", "emailVerified", "operatorRole", "appRoles", "banned" FROM "_auth_users" WHERE "id" = ?`, [id]);
|
|
1513
|
+
return row ? rowToRaybaseUser(row) : null;
|
|
1514
|
+
}
|
|
1515
|
+
function devPersonaRoles(persona) {
|
|
1516
|
+
const operatorRole = raybaseDevPersonaOperatorRole(persona);
|
|
1517
|
+
const appRoles = normalizeRoles(operatorRole ? ["user"] : ["user", persona.role]);
|
|
1518
|
+
return { operatorRole, appRoles };
|
|
1519
|
+
}
|
|
1520
|
+
export async function getOrCreateDevUser(auth, request, env, config, persona = resolveRaybaseDevPersonas(config)[0]) {
|
|
1521
|
+
const { operatorRole, appRoles } = devPersonaRoles(persona);
|
|
1522
|
+
const db = authDb(env);
|
|
1523
|
+
const existing = await getUserByEmail(db, persona.email);
|
|
1524
|
+
if (existing) {
|
|
1525
|
+
await setUserAdminRole(db, existing.id, operatorRole);
|
|
1526
|
+
await setUserAppRoles(db, existing.id, appRoles);
|
|
1527
|
+
return { ...existing, operatorRole, appRoles };
|
|
1528
|
+
}
|
|
1529
|
+
const created = await signUpWithRaybaseToken(auth, request, env, {
|
|
1530
|
+
email: persona.email,
|
|
1531
|
+
name: persona.name,
|
|
1532
|
+
password: "raybase-dev-password",
|
|
1533
|
+
operatorRole,
|
|
1534
|
+
appRoles
|
|
1535
|
+
});
|
|
1536
|
+
return created.user;
|
|
1537
|
+
}
|
|
1538
|
+
function rowToRaybaseUser(row) {
|
|
1539
|
+
return withHostUserRoleCeiling({
|
|
1540
|
+
id: row.id,
|
|
1541
|
+
email: row.email,
|
|
1542
|
+
name: row.name,
|
|
1543
|
+
operatorRole: normalizeStoredAdminRole(row.operatorRole),
|
|
1544
|
+
appRoles: row.appRoles ? parseStoredAppRoles(row.appRoles) : normalizeRoles(["user"]),
|
|
1545
|
+
emailVerified: row.emailVerified === true || row.emailVerified === 1,
|
|
1546
|
+
disabled: Boolean(row.banned)
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
export async function createDevSession(auth, request, env, config) {
|
|
1550
|
+
const persona = await resolveRequestedRaybaseDevPersona(request, config);
|
|
1551
|
+
await getOrCreateDevUser(auth, request, env, config, persona);
|
|
1552
|
+
const signin = await signInWithPassword(auth, request, env, config, {
|
|
1553
|
+
email: persona.email,
|
|
1554
|
+
password: "raybase-dev-password"
|
|
1555
|
+
});
|
|
1556
|
+
const user = await resolveUser(authDb(env), signin.user);
|
|
1557
|
+
return { headers: signin.headers, user };
|
|
1558
|
+
}
|
|
1559
|
+
export function raybaseAuthExecutor(env) {
|
|
1560
|
+
return authDb(env);
|
|
1561
|
+
}
|
|
1562
|
+
//# sourceMappingURL=auth.js.map
|