@omg-dev/sdk 0.4.24

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/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@omg-dev/sdk",
3
+ "version": "0.4.24",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./src/index.ts",
8
+ "default": "./dist/index.mjs"
9
+ },
10
+ "./feedback/auto": {
11
+ "types": "./src/feedback/auto.tsx",
12
+ "default": "./dist/feedback/auto.mjs"
13
+ },
14
+ "./brand/auto": {
15
+ "types": "./src/brand/auto.tsx",
16
+ "default": "./dist/brand/auto.mjs"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "better-auth": "^1.2.0",
21
+ "@better-auth/passkey": "^1.2.0",
22
+ "@microsoft/fetch-event-source": "^2.0.1",
23
+ "modern-screenshot": "^4.0.0"
24
+ },
25
+ "peerDependencies": {
26
+ "react": ">=18",
27
+ "react-dom": ">=18"
28
+ },
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/BennyKok/vibes.git"
33
+ },
34
+ "homepage": "https://docs.omg.dev",
35
+ "files": [
36
+ "dist",
37
+ "src"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ }
43
+ }
@@ -0,0 +1,50 @@
1
+ // @omg-dev/sdk/auth — Auto-prompt overlay.
2
+ //
3
+ // Listens for the bridge's auth-required signal (fired by any SDK request that
4
+ // comes back 401 / WS `auth_required`) and surfaces <VibesLogin /> in a
5
+ // lightweight modal overlay. Mounted by <VibesAuthProvider> unless the app
6
+ // opts out with `autoPrompt={false}`. Lives in its own file (not react.tsx) to
7
+ // avoid a react.tsx ↔ login.tsx import cycle.
8
+
9
+ import { useEffect, useState } from "react"
10
+ import { subscribeAuthRequired } from "./bridge"
11
+ import { useUser } from "./react"
12
+ import { VibesLogin } from "./login"
13
+
14
+ export function VibesAuthAutoPrompt() {
15
+ const user = useUser()
16
+ const [open, setOpen] = useState(false)
17
+
18
+ // Open on any auth-required signal.
19
+ useEffect(() => subscribeAuthRequired(() => setOpen(true)), [])
20
+
21
+ // Close automatically once the user is signed in.
22
+ useEffect(() => {
23
+ if (user) setOpen(false)
24
+ }, [user])
25
+
26
+ if (!open || user) return null
27
+
28
+ return (
29
+ <div
30
+ role="dialog"
31
+ aria-modal="true"
32
+ onClick={(e) => {
33
+ // Click on the backdrop (not the card) dismisses.
34
+ if (e.target === e.currentTarget) setOpen(false)
35
+ }}
36
+ style={{
37
+ position: "fixed",
38
+ inset: 0,
39
+ zIndex: 2147483000,
40
+ display: "flex",
41
+ alignItems: "center",
42
+ justifyContent: "center",
43
+ background: "rgba(0,0,0,0.5)",
44
+ backdropFilter: "blur(2px)",
45
+ }}
46
+ >
47
+ <VibesLogin subtitle="Sign in to continue." />
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,63 @@
1
+ // @omg-dev/sdk/auth — Module-scoped bridge so useCollection can read the
2
+ // current auth token without importing React from the main entrypoint.
3
+ // VibesAuthProvider sets this on every token change.
4
+
5
+ import type { VibesUser } from "./client"
6
+
7
+ interface AuthSnapshot {
8
+ user: VibesUser | null
9
+ token: string | null
10
+ /**
11
+ * True once the initial session check AND the first token-fetch attempt have
12
+ * settled. While false, a scoped-collection `auth_required` is treated as a
13
+ * transient "still loading" state — not a real signed-out signal — so apps
14
+ * don't flash a scary error or pop the login dialog during startup.
15
+ */
16
+ authReady: boolean
17
+ }
18
+
19
+ let current: AuthSnapshot = { user: null, token: null, authReady: false }
20
+
21
+ type AuthChangeListener = (snap: AuthSnapshot) => void
22
+ const authChangeListeners = new Set<AuthChangeListener>()
23
+
24
+ export function setAuthContext(snap: AuthSnapshot) {
25
+ current = snap
26
+ for (const cb of authChangeListeners) cb(snap)
27
+ }
28
+
29
+ export function getAuthContext(): AuthSnapshot {
30
+ return current
31
+ }
32
+
33
+ /**
34
+ * Subscribe to auth snapshot changes (user / token / authReady). Returns an
35
+ * unsubscribe fn. Data hooks use this to re-establish a subscription when the
36
+ * JWT finishes loading (null→token) or on sign-out (token→null) — without it
37
+ * the WS connects once, often before getToken() resolves, and never retries
38
+ * with a bearer.
39
+ */
40
+ export function subscribeAuthChange(cb: AuthChangeListener): () => void {
41
+ authChangeListeners.add(cb)
42
+ return () => authChangeListeners.delete(cb)
43
+ }
44
+
45
+ // ── Auth-required signal ───────────────────────────────────────────────────────
46
+ // Fired whenever a request comes back with the server's "auth required" signal
47
+ // (REST 401 or WS `auth_required`). VibesAuthProvider subscribes to this to
48
+ // auto-open the login UI. Lives in the bridge (not React) so the non-React
49
+ // fetch/WS helpers in the main entrypoint can notify without importing React.
50
+
51
+ type AuthRequiredListener = () => void
52
+ const authRequiredListeners = new Set<AuthRequiredListener>()
53
+
54
+ /** Subscribe to auth-required signals. Returns an unsubscribe fn. */
55
+ export function subscribeAuthRequired(cb: AuthRequiredListener): () => void {
56
+ authRequiredListeners.add(cb)
57
+ return () => authRequiredListeners.delete(cb)
58
+ }
59
+
60
+ /** Notify all subscribers that a request hit the server's auth-required signal. */
61
+ export function notifyAuthRequired(): void {
62
+ for (const cb of authRequiredListeners) cb()
63
+ }
@@ -0,0 +1,222 @@
1
+ // @omg-dev/sdk/auth — Auth client and token manager
2
+
3
+ import { createAuthClient } from "better-auth/react"
4
+ import { passkeyClient } from "@better-auth/passkey/client"
5
+ import { magicLinkClient, emailOTPClient } from "better-auth/client/plugins"
6
+
7
+ export interface VibesUser {
8
+ id: string
9
+ email: string
10
+ name?: string
11
+ }
12
+
13
+ export interface VibesAuthConfig {
14
+ /** App ID registered with the auth service */
15
+ appId: string
16
+ /** Auth service URL. Defaults to https://auth.omg.dev */
17
+ authUrl?: string
18
+ /** JWT mint endpoint. Defaults to `${authUrl}/token`. */
19
+ tokenUrl?: string
20
+ }
21
+
22
+ export interface VibesAuthClient {
23
+ /** The underlying Better Auth client */
24
+ authClient: ReturnType<typeof createAuthClient>
25
+ /** App ID for JWT token requests */
26
+ appId: string
27
+ /** Auth service base URL */
28
+ authUrl: string
29
+ /** JWT mint endpoint */
30
+ tokenUrl: string
31
+ /** Fetch a JWT for this app. Uses the session cookie automatically. */
32
+ getToken: () => Promise<string | null>
33
+ /** Clear the cached token */
34
+ clearToken: () => void
35
+ }
36
+
37
+ const DEFAULT_AUTH_URL = "https://auth.omg.dev"
38
+
39
+ // Same-origin Worker bridge prefix for cross-site app hosts. On *.omgs.app the
40
+ // shared `.omg.dev` cookie can't reach auth.omg.dev, so the WHOLE better-auth
41
+ // surface (sign-in, email-OTP send/verify, get-session, sign-out, passkey) and
42
+ // the slug-JWT mint are routed through `${origin}/__vibes/auth/*`, which the
43
+ // Worker forwards server-side and whose Set-Cookie it rewrites host-only. Must
44
+ // match AUTH_BRIDGE_PREFIX in apps/infra/worker/src/auth-bridge.ts.
45
+ const AUTH_BRIDGE_PREFIX = "/__vibes/auth"
46
+
47
+ /**
48
+ * Derive the appId from the current host. The slug is the first DNS label.
49
+ * Canonical deployed apps live at `<slug>.apps.omg.dev`; cross-site app hosts
50
+ * (no shared `.omg.dev` cookie) live at `<slug>.omgs.app`. Returns null when
51
+ * the host doesn't match (local dev, custom domains).
52
+ */
53
+ export function deriveAppId(host?: string): string | null {
54
+ const h = host ?? (typeof window !== "undefined" ? window.location.host : "")
55
+ if (host === undefined) {
56
+ const injected = readInjectedAppId()
57
+ if (injected) return injected
58
+ }
59
+ const m = /^([a-z0-9-]+)\.apps\.omg\.dev$/.exec(h) ?? /^([a-z0-9-]+)\.omgs\.app$/.exec(h)
60
+ return m ? m[1] : null
61
+ }
62
+
63
+ /**
64
+ * A cross-site app host is one served outside the shared `.omg.dev` cookie
65
+ * scope, so the slug JWT must be minted same-origin (the Worker reads the
66
+ * first-party `omg_access` cookie and exchanges it). Today that's `*.omgs.app`.
67
+ * Custom domains are out of scope here — they fall through to the default
68
+ * `${authUrl}/token` for now.
69
+ */
70
+ function isCrossSiteAppHost(host: string): boolean {
71
+ return host.endsWith(".omgs.app")
72
+ }
73
+
74
+ export function deriveTokenUrl(authUrl = DEFAULT_AUTH_URL): string {
75
+ // Preview injection always wins.
76
+ const injected = readInjectedTokenUrl()
77
+ if (injected) return injected
78
+ // On cross-site app hosts (e.g. *.omgs.app) the shared .omg.dev cookie isn't
79
+ // available cross-origin, so mint the token via the same-origin Worker path.
80
+ if (typeof window !== "undefined" && isCrossSiteAppHost(window.location.host)) {
81
+ return new URL("/__omg/auth/token", window.location.origin).toString()
82
+ }
83
+ // apps.omg.dev / dashboard: unchanged — cross-origin auth.omg.dev with the
84
+ // shared .omg.dev cookie.
85
+ return `${authUrl}/token`
86
+ }
87
+
88
+ function readInjectedAppId(): string | null {
89
+ if (typeof window === "undefined") return null
90
+ const value = (window as unknown as { __VIBES_APP_ID?: unknown }).__VIBES_APP_ID
91
+ return typeof value === "string" && /^[a-z0-9-]+$/.test(value) ? value : null
92
+ }
93
+
94
+ function readInjectedTokenUrl(): string | null {
95
+ if (typeof window === "undefined") return null
96
+ const value = (window as unknown as { __VIBES_AUTH_TOKEN_URL?: unknown }).__VIBES_AUTH_TOKEN_URL
97
+ if (typeof value !== "string" || !value.trim()) return null
98
+ try {
99
+ return new URL(value, window.location.origin).toString()
100
+ } catch {
101
+ return null
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Same-origin session-read endpoint, injected by the vite-plugin in preview
107
+ * (`{sandboxId}-5173.preview.omg.dev`). Present only there; null for deployed
108
+ * apps and local dev.
109
+ */
110
+ function readInjectedSessionUrl(): string | null {
111
+ if (typeof window === "undefined") return null
112
+ const value = (window as unknown as { __VIBES_AUTH_SESSION_URL?: unknown }).__VIBES_AUTH_SESSION_URL
113
+ if (typeof value !== "string" || !value.trim()) return null
114
+ try {
115
+ return new URL(value, window.location.origin).toString()
116
+ } catch {
117
+ return null
118
+ }
119
+ }
120
+
121
+ export function createVibesAuth(config: VibesAuthConfig): VibesAuthClient {
122
+ const authUrl = config.authUrl ?? DEFAULT_AUTH_URL
123
+
124
+ // On a cross-site app host (*.omgs.app) the browser can't send the shared
125
+ // `.omg.dev` cookie to auth.omg.dev, so point the better-auth client AND the
126
+ // token mint at the same-origin Worker bridge. The bridge forwards to
127
+ // auth.omg.dev server-side and rewrites the session Set-Cookie host-only, so
128
+ // sign-in / OTP-verify / get-session all land first-party to omgs.app. On
129
+ // apps.omg.dev (same registrable domain) and local dev this is a no-op.
130
+ const crossSite =
131
+ typeof window !== "undefined" && isCrossSiteAppHost(window.location.host)
132
+ const bridgeBase = crossSite ? `${window.location.origin}${AUTH_BRIDGE_PREFIX}` : null
133
+
134
+ // better-auth client appends `/api/auth/<endpoint>` to baseURL, so a bridge
135
+ // base of `${origin}/__vibes/auth` yields `${origin}/__vibes/auth/api/auth/…`.
136
+ const baseURL = bridgeBase ?? authUrl
137
+ const tokenUrl = config.tokenUrl ?? (bridgeBase ? `${bridgeBase}/token` : deriveTokenUrl(authUrl))
138
+
139
+ // In preview, the better-auth client's only cross-origin call is the
140
+ // get-session read to auth.omg.dev. That credentialed cross-origin GET is the
141
+ // one auth request that third-party-cookie blocking / Safari ITP can silently
142
+ // drop — making a signed-in dashboard user look signed-out and popping a
143
+ // needless login. Token minting already dodges this via the same-origin
144
+ // `/__vibes/auth/token` bridge (cookie forwarded server-side); do the same for
145
+ // the session read so preview auto-signs-in from the shared .omg.dev session.
146
+ // Everything else (sign-in, sign-out, OTP verify) is left untouched.
147
+ const sessionUrl = readInjectedSessionUrl()
148
+ const customFetchImpl: typeof fetch = (input, init) => {
149
+ const url =
150
+ typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
151
+ if (sessionUrl && url.includes("/api/auth/get-session")) {
152
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : ""
153
+ return fetch(sessionUrl + qs, { ...init, credentials: "include" })
154
+ }
155
+ return fetch(input, init)
156
+ }
157
+
158
+ const authClient = createAuthClient({
159
+ baseURL,
160
+ ...(sessionUrl ? { fetchOptions: { customFetchImpl } } : {}),
161
+ plugins: [
162
+ magicLinkClient(),
163
+ // email-otp registers /sign-in/email-otp on the $sessionSignal atom —
164
+ // useSession refetches right after a successful code verify, so guards
165
+ // and the auto-prompt dialog close without any redirect.
166
+ emailOTPClient(),
167
+ passkeyClient(),
168
+ ],
169
+ })
170
+
171
+ let cachedToken: string | null = null
172
+ let tokenExp: number = 0
173
+
174
+ async function getToken(): Promise<string | null> {
175
+ // Return cached token if still valid (with 60s buffer)
176
+ if (cachedToken && Date.now() < tokenExp - 60_000) {
177
+ return cachedToken
178
+ }
179
+
180
+ try {
181
+ const res = await fetch(tokenUrl, {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json" },
184
+ credentials: "include", // send .omg.dev cookie
185
+ body: JSON.stringify({ appId: config.appId }),
186
+ })
187
+
188
+ if (!res.ok) {
189
+ cachedToken = null
190
+ tokenExp = 0
191
+ return null
192
+ }
193
+
194
+ const data = await res.json() as { token: string; expiresAt: string }
195
+ cachedToken = data.token
196
+
197
+ // Parse expiry from response or JWT
198
+ if (data.expiresAt) {
199
+ tokenExp = new Date(data.expiresAt).getTime()
200
+ } else {
201
+ // Fallback: decode JWT exp claim
202
+ try {
203
+ const payload = JSON.parse(atob(data.token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")))
204
+ tokenExp = (payload.exp ?? 0) * 1000
205
+ } catch {
206
+ tokenExp = Date.now() + 3600_000 // default 1hr
207
+ }
208
+ }
209
+
210
+ return cachedToken
211
+ } catch {
212
+ return null
213
+ }
214
+ }
215
+
216
+ function clearToken() {
217
+ cachedToken = null
218
+ tokenExp = 0
219
+ }
220
+
221
+ return { authClient, appId: config.appId, authUrl, tokenUrl, getToken, clearToken }
222
+ }
@@ -0,0 +1,23 @@
1
+ // @omg-dev/sdk/auth — Fetch wrapper with JWT Bearer token
2
+
3
+ import type { VibesAuthClient } from "./client"
4
+
5
+ /**
6
+ * Creates a fetch wrapper that automatically attaches the JWT Bearer token.
7
+ * If no token is available (user not signed in), falls through without auth header.
8
+ */
9
+ export function vibesFetch(client: VibesAuthClient) {
10
+ return async function (
11
+ input: RequestInfo | URL,
12
+ init?: RequestInit
13
+ ): Promise<Response> {
14
+ const token = await client.getToken()
15
+
16
+ const headers = new Headers(init?.headers)
17
+ if (token) {
18
+ headers.set("Authorization", `Bearer ${token}`)
19
+ }
20
+
21
+ return fetch(input, { ...init, headers })
22
+ }
23
+ }
@@ -0,0 +1,24 @@
1
+ // @omg-dev/sdk/auth — Gate children behind a logged-in user.
2
+
3
+ import type { ReactNode } from "react"
4
+ import { useUser, useVibesAuth } from "./react"
5
+ import { VibesLogin } from "./login"
6
+
7
+ export function VibesAuthGuard({
8
+ children,
9
+ fallback,
10
+ loadingFallback,
11
+ }: {
12
+ children: ReactNode
13
+ /** Replace the default <VibesLogin/> when the user is signed-out. */
14
+ fallback?: ReactNode
15
+ /** Optional placeholder while the session is still resolving. */
16
+ loadingFallback?: ReactNode
17
+ }) {
18
+ const { loading } = useVibesAuth()
19
+ const user = useUser()
20
+
21
+ if (loading) return <>{loadingFallback ?? null}</>
22
+ if (!user) return <>{fallback ?? <VibesLogin />}</>
23
+ return <>{children}</>
24
+ }
@@ -0,0 +1,23 @@
1
+ // @omg-dev/sdk/auth — Client-side auth for apps on omg.dev
2
+ //
3
+ // Auto-login via cross-subdomain cookie on .omg.dev,
4
+ // JWT token management via auth.omg.dev/token,
5
+ // and React hooks for auth state.
6
+
7
+ export { createVibesAuth, deriveAppId, deriveTokenUrl } from "./client"
8
+ export {
9
+ VibesAuthProvider,
10
+ useVibesAuth,
11
+ useVibesToken,
12
+ useAuth,
13
+ useUser,
14
+ } from "./react"
15
+ export { vibesFetch } from "./fetch"
16
+ export { VibesLogin } from "./login"
17
+ export { mailAppForEmail } from "./mail-apps"
18
+ export type { MailApp } from "./mail-apps"
19
+ export { VibesAuthGuard } from "./guard"
20
+ export { VibesAuthAutoPrompt } from "./auto-prompt"
21
+ export { getAuthContext, subscribeAuthRequired, notifyAuthRequired } from "./bridge"
22
+ export type { VibesAuthConfig, VibesAuthClient, VibesUser } from "./client"
23
+ export type { VibesAuthContextValue } from "./react"