@omg-dev/vite-plugin 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/dist/index.mjs +1087 -0
- package/package.json +42 -0
- package/src/auth-bridge.test.ts +118 -0
- package/src/auth-bridge.ts +73 -0
- package/src/brand-badge-inject.test.ts +94 -0
- package/src/build.ts +430 -0
- package/src/codegen.ts +134 -0
- package/src/error-sink.ts +66 -0
- package/src/feedback-inject.test.ts +79 -0
- package/src/index.ts +967 -0
- package/src/prerender.ts +106 -0
- package/src/pwa.test.ts +138 -0
- package/src/pwa.ts +132 -0
- package/src/scanner-billing.ts +127 -0
- package/src/scanner-triggers.ts +15 -0
- package/src/scanner-workflows.test.ts +69 -0
- package/src/scanner-workflows.ts +125 -0
- package/src/scanner.ts +146 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@omg-dev/vite-plugin",
|
|
3
|
+
"version": "0.4.24",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"default": "./dist/index.mjs"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"vibes-build": "./src/build.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "vp pack src/index.ts --no-fail-on-warn"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@omg-dev/server": "0.4.24",
|
|
19
|
+
"@omg-dev/schema": "0.4.24",
|
|
20
|
+
"ws": "^8.18.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/ws": "^8.5.13"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"vite": "*"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/BennyKok/vibes.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://docs.omg.dev",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import { fetchAuthUpstream, relayAuthUpstream } from "./auth-bridge.ts"
|
|
3
|
+
|
|
4
|
+
// Minimal ServerResponse stand-in capturing what the bridge writes.
|
|
5
|
+
function mockRes() {
|
|
6
|
+
const headers: Record<string, string> = {}
|
|
7
|
+
const state = { statusCode: 0, body: null as Buffer | null, headers }
|
|
8
|
+
const res = {
|
|
9
|
+
get statusCode() { return state.statusCode },
|
|
10
|
+
set statusCode(v: number) { state.statusCode = v },
|
|
11
|
+
setHeader(key: string, value: string) { headers[key.toLowerCase()] = String(value) },
|
|
12
|
+
end(body?: Buffer) { state.body = body ?? Buffer.alloc(0) },
|
|
13
|
+
}
|
|
14
|
+
return { res: res as unknown as Parameters<typeof relayAuthUpstream>[0], state }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("relayAuthUpstream", () => {
|
|
18
|
+
it("strips upstream framing + cookie headers and recomputes content-length", async () => {
|
|
19
|
+
// Mirrors the production failure: Cloudflare zstd-compressed the 200
|
|
20
|
+
// token response, fetch() decompressed it, and the old relay copied the
|
|
21
|
+
// stale framing headers onto the plaintext body — CF 520'd the desync.
|
|
22
|
+
const body = JSON.stringify({ token: "x".repeat(400), expiresAt: "2026-01-01T00:00:00Z" })
|
|
23
|
+
const upstream = new Response(body, {
|
|
24
|
+
status: 200,
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json;charset=utf-8",
|
|
27
|
+
"Content-Encoding": "zstd",
|
|
28
|
+
"Transfer-Encoding": "chunked",
|
|
29
|
+
"Set-Cookie": "evil=1",
|
|
30
|
+
Connection: "keep-alive",
|
|
31
|
+
"Access-Control-Allow-Origin": "https://omg.dev",
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
// Response normally re-derives content-length; force the stale value the
|
|
35
|
+
// way fetch() surfaces it after transparent decompression.
|
|
36
|
+
upstream.headers.set("Content-Length", "460")
|
|
37
|
+
|
|
38
|
+
const { res, state } = mockRes()
|
|
39
|
+
await relayAuthUpstream(res, upstream)
|
|
40
|
+
|
|
41
|
+
expect(state.statusCode).toBe(200)
|
|
42
|
+
expect(state.body?.toString()).toBe(body)
|
|
43
|
+
expect(state.headers["content-encoding"]).toBeUndefined()
|
|
44
|
+
expect(state.headers["transfer-encoding"]).toBeUndefined()
|
|
45
|
+
expect(state.headers["set-cookie"]).toBeUndefined()
|
|
46
|
+
expect(state.headers["connection"]).toBeUndefined()
|
|
47
|
+
expect(state.headers["content-length"]).toBe(String(Buffer.byteLength(body)))
|
|
48
|
+
expect(state.headers["content-type"]).toBe("application/json;charset=utf-8")
|
|
49
|
+
expect(state.headers["access-control-allow-origin"]).toBe("https://omg.dev")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("relays error statuses verbatim", async () => {
|
|
53
|
+
const upstream = new Response(JSON.stringify({ error: "unauthorized" }), { status: 401 })
|
|
54
|
+
const { res, state } = mockRes()
|
|
55
|
+
await relayAuthUpstream(res, upstream)
|
|
56
|
+
expect(state.statusCode).toBe(401)
|
|
57
|
+
expect(state.body?.toString()).toContain("unauthorized")
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("fetchAuthUpstream", () => {
|
|
62
|
+
const originalFetch = globalThis.fetch
|
|
63
|
+
const originalProxy = process.env.OMG_AI_URL
|
|
64
|
+
const originalAuthUrl = process.env.VIBES_AUTH_URL
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
globalThis.fetch = originalFetch
|
|
68
|
+
if (originalProxy === undefined) delete process.env.OMG_AI_URL
|
|
69
|
+
else process.env.OMG_AI_URL = originalProxy
|
|
70
|
+
if (originalAuthUrl === undefined) delete process.env.VIBES_AUTH_URL
|
|
71
|
+
else process.env.VIBES_AUTH_URL = originalAuthUrl
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("uses the host-side proxy when it advertises the auth route", async () => {
|
|
75
|
+
process.env.OMG_AI_URL = "http://169.254.0.1:9090"
|
|
76
|
+
const calls: string[] = []
|
|
77
|
+
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
78
|
+
calls.push(String(input))
|
|
79
|
+
return new Response("{}", { status: 200, headers: { "X-Vibes-Auth-Proxy": "1" } })
|
|
80
|
+
}) as typeof fetch
|
|
81
|
+
|
|
82
|
+
const res = await fetchAuthUpstream("token", { method: "POST" })
|
|
83
|
+
expect(res.status).toBe(200)
|
|
84
|
+
expect(calls).toEqual(["http://169.254.0.1:9090/auth/token"])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("falls back to direct when the proxy lacks the marker (old orchestrator)", async () => {
|
|
88
|
+
process.env.OMG_AI_URL = "http://169.254.0.1:9090"
|
|
89
|
+
delete process.env.VIBES_AUTH_URL
|
|
90
|
+
const calls: string[] = []
|
|
91
|
+
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
92
|
+
calls.push(String(input))
|
|
93
|
+
if (calls.length === 1) return new Response("not the auth route", { status: 404 })
|
|
94
|
+
return new Response("{}", { status: 200 })
|
|
95
|
+
}) as typeof fetch
|
|
96
|
+
|
|
97
|
+
const res = await fetchAuthUpstream("session", { method: "GET" }, "?a=1")
|
|
98
|
+
expect(res.status).toBe(200)
|
|
99
|
+
expect(calls).toEqual([
|
|
100
|
+
"http://169.254.0.1:9090/auth/get-session?a=1",
|
|
101
|
+
"https://auth.omg.dev/api/auth/get-session?a=1",
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("goes direct when no proxy env is present", async () => {
|
|
106
|
+
delete process.env.OMG_AI_URL
|
|
107
|
+
process.env.VIBES_AUTH_URL = "https://auth.example.test/"
|
|
108
|
+
const calls: string[] = []
|
|
109
|
+
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
110
|
+
calls.push(String(input))
|
|
111
|
+
return new Response("{}", { status: 401 })
|
|
112
|
+
}) as typeof fetch
|
|
113
|
+
|
|
114
|
+
const res = await fetchAuthUpstream("token", { method: "POST" })
|
|
115
|
+
expect(res.status).toBe(401)
|
|
116
|
+
expect(calls).toEqual(["https://auth.example.test/token"])
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Preview auth bridge upstream helpers — shared by the /__vibes/auth/token
|
|
2
|
+
// and /__vibes/auth/session dev-server middlewares in index.ts.
|
|
3
|
+
|
|
4
|
+
import type { ServerResponse } from "node:http"
|
|
5
|
+
|
|
6
|
+
// Auth-state headers the preview bridges must never relay downstream:
|
|
7
|
+
// - set-cookie: the bridges are read-only for auth state (sign-in/out go
|
|
8
|
+
// direct to auth.omg.dev through the better-auth client).
|
|
9
|
+
// - content-encoding / content-length / transfer-encoding / connection /
|
|
10
|
+
// keep-alive: fetch() has already decompressed and de-chunked the body, so
|
|
11
|
+
// upstream's framing headers describe bytes we are NOT sending. Relaying
|
|
12
|
+
// them verbatim produced protocol-desynced responses (a `content-encoding:
|
|
13
|
+
// zstd` header on a plaintext body) that Cloudflare's edge killed with a
|
|
14
|
+
// 520 — which made token minting hang for every signed-in preview user
|
|
15
|
+
// while signed-out 401s (too small for compression) sailed through.
|
|
16
|
+
export const AUTH_RELAY_SKIP_HEADERS = new Set([
|
|
17
|
+
"set-cookie",
|
|
18
|
+
"content-encoding",
|
|
19
|
+
"content-length",
|
|
20
|
+
"transfer-encoding",
|
|
21
|
+
"connection",
|
|
22
|
+
"keep-alive",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/** Relay an upstream auth response with sane framing for the buffered body. */
|
|
26
|
+
export async function relayAuthUpstream(res: ServerResponse, upstream: Response): Promise<void> {
|
|
27
|
+
const body = Buffer.from(await upstream.arrayBuffer())
|
|
28
|
+
res.statusCode = upstream.status
|
|
29
|
+
upstream.headers.forEach((value, key) => {
|
|
30
|
+
if (AUTH_RELAY_SKIP_HEADERS.has(key.toLowerCase())) return
|
|
31
|
+
res.setHeader(key, value)
|
|
32
|
+
})
|
|
33
|
+
res.setHeader("Content-Length", String(body.byteLength))
|
|
34
|
+
res.end(body)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fetch the auth upstream for the preview bridges.
|
|
39
|
+
*
|
|
40
|
+
* Prefers the host-side per-sandbox proxy (OMG_AI_URL, the same listener the
|
|
41
|
+
* LLM proxy uses at 169.254.0.1:9090) when it advertises the auth route via
|
|
42
|
+
* the `x-vibes-auth-proxy` marker: in-VM Bun fetch to the Cloudflare-proxied
|
|
43
|
+
* auth host is the AAAA-first/no-IPv6-route hang class, while the host-side
|
|
44
|
+
* Go client dials dual-stack. Falls back to a direct fetch when the proxy is
|
|
45
|
+
* absent (local dev outside a sandbox) or predates the auth route.
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchAuthUpstream(
|
|
48
|
+
kind: "token" | "session",
|
|
49
|
+
init: RequestInit,
|
|
50
|
+
qs = "",
|
|
51
|
+
): Promise<Response> {
|
|
52
|
+
const proxyBase = (process.env.OMG_AI_URL ?? "").trim().replace(/\/+$/, "")
|
|
53
|
+
if (proxyBase) {
|
|
54
|
+
const proxyUrl = kind === "token"
|
|
55
|
+
? `${proxyBase}/auth/token`
|
|
56
|
+
: `${proxyBase}/auth/get-session${qs}`
|
|
57
|
+
try {
|
|
58
|
+
const viaProxy = await fetch(proxyUrl, init)
|
|
59
|
+
if (viaProxy.headers.get("x-vibes-auth-proxy")) return viaProxy
|
|
60
|
+
// No marker: an older orchestrator's catch-all LLM route answered.
|
|
61
|
+
// Discard and go direct.
|
|
62
|
+
await viaProxy.arrayBuffer().catch(() => {})
|
|
63
|
+
} catch {
|
|
64
|
+
// Host proxy unreachable — go direct.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const authUrl = (process.env.VIBES_AUTH_URL || process.env.VITE_AUTH_URL || "https://auth.omg.dev")
|
|
68
|
+
.replace(/\/+$/, "")
|
|
69
|
+
const directUrl = kind === "token"
|
|
70
|
+
? `${authUrl}/token`
|
|
71
|
+
: `${authUrl}/api/auth/get-session${qs}`
|
|
72
|
+
return fetch(directUrl, init)
|
|
73
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Verifies the build-time injection of the published-app omg badge
|
|
2
|
+
// (@omg-dev/sdk/brand/auto) into the app entry — default ON, opt-out via
|
|
3
|
+
// vibes({ brandBadge: false }), build-only, app-entry-only.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test"
|
|
6
|
+
import vibes from "./index.ts"
|
|
7
|
+
|
|
8
|
+
const ROOT = "/app"
|
|
9
|
+
const ENTRY = `${ROOT}/src/main.tsx`
|
|
10
|
+
const BRAND_IMPORT = `import "@omg-dev/sdk/brand/auto";`
|
|
11
|
+
|
|
12
|
+
function ctx() {
|
|
13
|
+
const warnings: string[] = []
|
|
14
|
+
return {
|
|
15
|
+
warnings,
|
|
16
|
+
resolve: async (src: string) => ({ id: src }),
|
|
17
|
+
warn: (msg: string) => warnings.push(msg),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runTransform(
|
|
22
|
+
plugin: ReturnType<typeof vibes>,
|
|
23
|
+
code: string,
|
|
24
|
+
id: string,
|
|
25
|
+
context: ReturnType<typeof ctx>,
|
|
26
|
+
) {
|
|
27
|
+
const fn = plugin.transform as (this: unknown, code: string, id: string) => Promise<unknown>
|
|
28
|
+
return fn.call(context, code, id) as Promise<{ code: string } | null>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildPlugin(opts?: Parameters<typeof vibes>[0]) {
|
|
32
|
+
const plugin = vibes({ root: ROOT, pwa: false, feedback: false, ...opts })
|
|
33
|
+
const configResolved = plugin.configResolved as (cfg: unknown) => void
|
|
34
|
+
configResolved({ command: "build", root: ROOT })
|
|
35
|
+
return plugin
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("brand badge injection", () => {
|
|
39
|
+
test("default ON: appends the auto-mount import to the app entry", async () => {
|
|
40
|
+
const plugin = buildPlugin()
|
|
41
|
+
const out = await runTransform(plugin, "export default 1", ENTRY, ctx())
|
|
42
|
+
expect(out?.code).toContain(BRAND_IMPORT)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("opt-out via brandBadge:false omits the import", async () => {
|
|
46
|
+
const plugin = buildPlugin({ brandBadge: false })
|
|
47
|
+
const out = await runTransform(plugin, "export default 1", ENTRY, ctx())
|
|
48
|
+
expect(out).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("injected exactly once across multiple entry transforms", async () => {
|
|
52
|
+
const plugin = buildPlugin()
|
|
53
|
+
const first = await runTransform(plugin, "export default 1", ENTRY, ctx())
|
|
54
|
+
const second = await runTransform(plugin, "export default 2", ENTRY, ctx())
|
|
55
|
+
expect(first?.code).toContain(BRAND_IMPORT)
|
|
56
|
+
expect(second).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("non-entry modules are untouched", async () => {
|
|
60
|
+
const plugin = buildPlugin()
|
|
61
|
+
const out = await runTransform(plugin, "export const x = 1", `${ROOT}/src/util.ts`, ctx())
|
|
62
|
+
expect(out).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("warns and skips when the SDK subpath cannot be resolved", async () => {
|
|
66
|
+
const plugin = buildPlugin()
|
|
67
|
+
const context = ctx()
|
|
68
|
+
context.resolve = async () => null
|
|
69
|
+
const out = await runTransform(plugin, "export default 1", ENTRY, context)
|
|
70
|
+
expect(out).toBeNull()
|
|
71
|
+
expect(context.warnings.some((w) => w.includes("brand badge"))).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("badge ON bundles feedback + remix: no standalone imports injected", async () => {
|
|
75
|
+
// brand badge default ON, with feedback + pwa also on. The badge carries
|
|
76
|
+
// feedback (button-less) and the "Make it mine" CTA itself, so the
|
|
77
|
+
// standalone feedback/auto + remix-cta imports must NOT be appended.
|
|
78
|
+
const plugin = vibes({ root: ROOT })
|
|
79
|
+
const configResolved = plugin.configResolved as (cfg: unknown) => void
|
|
80
|
+
configResolved({ command: "build", root: ROOT })
|
|
81
|
+
const out = await runTransform(plugin, "export default 1", ENTRY, ctx())
|
|
82
|
+
expect(out?.code).toContain(BRAND_IMPORT)
|
|
83
|
+
expect(out?.code).not.toContain(`import "@omg-dev/sdk/feedback/auto";`)
|
|
84
|
+
expect(out?.code).not.toContain(`import "@omg-dev/pwa/remix-cta";`)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("serve mode never injects the badge", async () => {
|
|
88
|
+
const plugin = vibes({ root: ROOT })
|
|
89
|
+
const configResolved = plugin.configResolved as (cfg: unknown) => void
|
|
90
|
+
configResolved({ command: "serve", root: ROOT })
|
|
91
|
+
const out = await runTransform(plugin, "export default 1", ENTRY, ctx())
|
|
92
|
+
expect(out).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
})
|