diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml
index 10f86fb..cc89ef3 100644
--- a/.github/workflows/release-sdk.yml
+++ b/.github/workflows/release-sdk.yml
@@ -71,11 +71,13 @@ jobs:
["adapters/react"]="react-v"
["adapters/vue"]="vue-v"
["adapters/svelte"]="svelte-v"
+ ["plugins/vite"]="vite-v"
+ ["plugins/next"]="next-v"
)
TAGGED=0
- for pkg_path in "sdk" "cli" "codemod" "adapters/react" "adapters/vue" "adapters/svelte"; do
+ for pkg_path in "sdk" "cli" "codemod" "adapters/react" "adapters/vue" "adapters/svelte" "plugins/vite" "plugins/next"; do
manifest_ver=$(jq -r --arg p "$pkg_path" '.[$p]' .release-please-manifest.json)
pkg_ver=$(jq -r '.version' "${pkg_path}/package.json")
prefix="${TAG_PREFIX[$pkg_path]}"
@@ -284,6 +286,24 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - name: Publish vite plugin
+ working-directory: plugins/vite
+ run: |
+ V=$(node -p "require('./package.json').version")
+ PUBLISHED=$(npm view @rep-protocol/vite@${V} version 2>/dev/null || true)
+ if [ "$PUBLISHED" = "$V" ]; then echo "vite@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Publish next plugin
+ working-directory: plugins/next
+ run: |
+ V=$(node -p "require('./package.json').version")
+ PUBLISHED=$(npm view @rep-protocol/next@${V} version 2>/dev/null || true)
+ if [ "$PUBLISHED" = "$V" ]; then echo "next@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
release-gateway:
name: GoReleaser
runs-on: ubuntu-latest
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index d0b360d..9f17ecd 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -8,6 +8,7 @@ on:
- 'cli/**'
- 'codemod/**'
- 'adapters/**'
+ - 'plugins/**'
- 'pnpm-workspace.yaml'
- '.github/workflows/sdk.yml'
pull_request:
@@ -31,6 +32,7 @@ jobs:
- 'cli/**'
- 'codemod/**'
- 'adapters/**'
+ - 'plugins/**'
- 'pnpm-workspace.yaml'
- '.github/workflows/sdk.yml'
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 67ef134..c3931b9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -5,5 +5,7 @@
"adapters/react": "0.1.11",
"adapters/vue": "0.1.11",
"adapters/svelte": "0.1.11",
+ "plugins/vite": "0.1.11",
+ "plugins/next": "0.1.11",
"gateway": "0.1.5"
}
diff --git a/CLAUDE.md b/CLAUDE.md
index 8bbaf12..d2c7e23 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -36,7 +36,7 @@ rep/
│ └── release-sdk.yml # Release workflow (npm + GoReleaser + Docker)
│
├── package.json # Monorepo root (pnpm 9.0.0, private)
-├── pnpm-workspace.yaml # Workspace: sdk, cli, adapters/*, codemod, examples/*
+├── pnpm-workspace.yaml # Workspace: sdk, cli, adapters/*, plugins/*, codemod, docs
├── pnpm-lock.yaml
├── release-please-config.json # Release-please config for all packages
├── .release-please-manifest.json # Per-package version tracker
@@ -124,6 +124,30 @@ rep/
│ ├── vue/ # @rep-protocol/vue — useRep() composable
│ └── svelte/ # @rep-protocol/svelte — repStore()
│
+├── plugins/
+│ ├── vite/ # @rep-protocol/vite — Vite dev server plugin
+│ │ ├── package.json # v0.1.10
+│ │ ├── src/
+│ │ │ ├── index.ts # repPlugin() — Vite plugin entry
+│ │ │ ├── payload.ts # Payload builder (mirrors Go payload.go)
+│ │ │ ├── crypto.ts # AES-256-GCM, HMAC-SHA256, SRI, HKDF (Node crypto)
+│ │ │ ├── env.ts # Read env file + classify variables by prefix
+│ │ │ ├── guardrails.ts # Shannon entropy + known format checks (mirrors cli/)
+│ │ │ └── __tests__/ # 49 tests
+│ │ └── vitest.config.ts
+│ └── next/ # @rep-protocol/next — Next.js dev integration
+│ ├── package.json # v0.1.10; exports "." (RepScript) + "./session-key" (route handler)
+│ ├── src/
+│ │ ├── index.ts # RepScript RSC — renders
- ```
-
```bash
# Terminal 1: Vite dev server
diff --git a/docs/src/content/docs/guides/development.mdx b/docs/src/content/docs/guides/development.mdx
index 1409203..9789612 100644
--- a/docs/src/content/docs/guides/development.mdx
+++ b/docs/src/content/docs/guides/development.mdx
@@ -7,7 +7,72 @@ import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
During local development you typically don't have the REP gateway running. The SDK handles this gracefully — `rep.get()` returns `undefined` when no payload is present.
-## Option A: Default values (simplest)
+## Option A: Build-tool plugin (recommended)
+
+The easiest way to get full REP support in development. The plugin injects the same `\n');
+
+ const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(testDir);
+
+ try {
+ const { RepScript } = await import('../index.js');
+ const result = RepScript({ env: '.env.local' });
+ const json = result!.props.dangerouslySetInnerHTML.__html;
+
+ // Must not contain raw < or > — all escaped to \u003c / \u003e
+ expect(json).not.toContain('<');
+ expect(json).not.toContain('>');
+ expect(json).toContain('\\u003c');
+ expect(json).toContain('\\u003e');
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ cwdSpy.mockRestore();
+ }
+ });
+
+ it('strict mode throws on guardrail warnings', async () => {
+ const origNodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'development';
+
+ const envPath = join(testDir, '.env.local');
+ writeFileSync(envPath, 'REP_PUBLIC_API_KEY=sk_live_abcdef1234567890abcdef\n');
+
+ const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(testDir);
+
+ try {
+ const { RepScript } = await import('../index.js');
+ expect(() => RepScript({ env: '.env.local', strict: true })).toThrow(/guardrail/i);
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ cwdSpy.mockRestore();
+ }
+ });
+
+ it('does not expose REP_SERVER_ vars in output', async () => {
+ const origNodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'development';
+
+ const envPath = join(testDir, '.env.local');
+ writeFileSync(envPath, 'REP_PUBLIC_API=url\nREP_SERVER_DB_PASSWORD=supersecret\n');
+
+ const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(testDir);
+
+ try {
+ const { RepScript } = await import('../index.js');
+ const result = RepScript({ env: '.env.local' });
+ const json = result!.props.dangerouslySetInnerHTML.__html;
+
+ expect(json).not.toContain('supersecret');
+ expect(json).not.toContain('DB_PASSWORD');
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ cwdSpy.mockRestore();
+ }
+ });
+
+ it('does not expose non-REP env vars in output', async () => {
+ const origNodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'development';
+ process.env.SECRET_THING = 'should-not-appear';
+
+ const envPath = join(testDir, '.env.local');
+ writeFileSync(envPath, 'REP_PUBLIC_API=url\nDATABASE_URL=postgres://secret\n');
+
+ const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(testDir);
+
+ try {
+ const { RepScript } = await import('../index.js');
+ const result = RepScript({ env: '.env.local' });
+ const json = result!.props.dangerouslySetInnerHTML.__html;
+
+ expect(json).not.toContain('should-not-appear');
+ expect(json).not.toContain('SECRET_THING');
+ expect(json).not.toContain('postgres://secret');
+ expect(json).not.toContain('DATABASE_URL');
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ delete process.env.SECRET_THING;
+ cwdSpy.mockRestore();
+ }
+ });
+});
diff --git a/plugins/next/src/__tests__/session-key.test.ts b/plugins/next/src/__tests__/session-key.test.ts
new file mode 100644
index 0000000..a4805e4
--- /dev/null
+++ b/plugins/next/src/__tests__/session-key.test.ts
@@ -0,0 +1,119 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+beforeEach(() => {
+ vi.resetModules();
+});
+
+describe('GET /rep/session-key', () => {
+ it('returns 404 in production', async () => {
+ const origNodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production';
+
+ try {
+ const { GET } = await import('../session-key.js');
+ const response = GET();
+
+ expect(response.status).toBe(404);
+ const body = await response.json();
+ expect(body.error).toContain('disabled in production');
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ }
+ });
+
+ it('returns encryption key in development', async () => {
+ const origNodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'development';
+
+ try {
+ const { GET } = await import('../session-key.js');
+ const response = GET();
+
+ expect(response.status).toBe(200);
+
+ const body = await response.json();
+ expect(body.key).toBeDefined();
+ expect(typeof body.key).toBe('string');
+ expect(body.expires_at).toBeDefined();
+
+ // Key should be valid base64 of 32 bytes
+ const keyBuf = Buffer.from(body.key, 'base64');
+ expect(keyBuf).toHaveLength(32);
+
+ // Verify headers
+ expect(response.headers.get('Cache-Control')).toBe('no-store');
+ expect(response.headers.get('Content-Type')).toBe('application/json');
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ }
+ });
+
+ it('returns the same key across multiple calls (singleton)', async () => {
+ const origNodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'development';
+
+ try {
+ const { GET } = await import('../session-key.js');
+ const body1 = await GET().json();
+ const body2 = await GET().json();
+
+ expect(body1.key).toBe(body2.key);
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ }
+ });
+
+ it('returns key that can decrypt RepScript sensitive blob', async () => {
+ const origNodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'development';
+
+ // Use dynamic imports so they share the same singleton keys
+ const { writeFileSync, mkdirSync, rmSync } = await import('node:fs');
+ const { join } = await import('node:path');
+ const { tmpdir } = await import('node:os');
+ const { createDecipheriv } = await import('node:crypto');
+
+ const testDir = join(tmpdir(), 'rep-next-sk-decrypt-' + process.pid);
+ mkdirSync(testDir, { recursive: true });
+
+ const envPath = join(testDir, '.env.local');
+ writeFileSync(envPath, 'REP_PUBLIC_API=url\nREP_SENSITIVE_SECRET=my-secret-value\n');
+
+ const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(testDir);
+
+ try {
+ // Import both from same module context so they share keys
+ const { RepScript } = await import('../index.js');
+ const { GET } = await import('../session-key.js');
+
+ // Get the payload from RepScript
+ const element = RepScript({ env: '.env.local' });
+ const json = element!.props.dangerouslySetInnerHTML.__html;
+ const parsed = JSON.parse(json);
+
+ // Get the key from session-key endpoint
+ const response = GET();
+ const { key } = await response.json();
+ const encKey = Buffer.from(key, 'base64');
+
+ // Decrypt the sensitive blob
+ const data = Buffer.from(parsed.sensitive, 'base64');
+ const nonce = data.subarray(0, 12);
+ const authTag = data.subarray(data.length - 16);
+ const ciphertext = data.subarray(12, data.length - 16);
+
+ const decipher = createDecipheriv('aes-256-gcm', encKey, nonce);
+ decipher.setAuthTag(authTag);
+ decipher.setAAD(Buffer.from(parsed._meta.integrity));
+
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
+ const decrypted = JSON.parse(plaintext.toString());
+
+ expect(decrypted.SECRET).toBe('my-secret-value');
+ } finally {
+ process.env.NODE_ENV = origNodeEnv;
+ cwdSpy.mockRestore();
+ rmSync(testDir, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/plugins/next/src/crypto.ts b/plugins/next/src/crypto.ts
new file mode 100644
index 0000000..be424e7
--- /dev/null
+++ b/plugins/next/src/crypto.ts
@@ -0,0 +1,125 @@
+import { createCipheriv, createHmac, createHash, randomBytes } from 'node:crypto';
+
+export interface Keys {
+ encryptionKey: Buffer;
+ hmacSecret: Buffer;
+}
+
+/**
+ * HKDF-SHA256 key derivation (RFC 5869), single-round.
+ * Matches gateway/internal/crypto/crypto.go:DeriveKey exactly.
+ *
+ * Extract: PRK = HMAC-SHA256(salt, ikm)
+ * Expand: T(1) = HMAC-SHA256(PRK, info || 0x01)
+ */
+export function deriveKey(ikm: Buffer, salt: Buffer, info: string, length: number): Buffer {
+ if (length > 32) {
+ throw new Error('rep: deriveKey length exceeds one HKDF-SHA256 round (max 32)');
+ }
+
+ // Extract
+ const prk = createHmac('sha256', salt).update(ikm).digest();
+
+ // Expand
+ const okm = createHmac('sha256', prk)
+ .update(info)
+ .update(Buffer.from([0x01]))
+ .digest();
+
+ return okm.subarray(0, length);
+}
+
+/**
+ * Generate ephemeral keys matching Go's GenerateKeys().
+ * Master key is HKDF-derived, then discarded.
+ */
+export function generateKeys(): Keys {
+ const masterKey = randomBytes(32);
+ const startupSalt = randomBytes(32);
+ const encryptionKey = deriveKey(masterKey, startupSalt, 'rep-blob-encryption-v1', 32);
+ const hmacSecret = randomBytes(32);
+
+ return { encryptionKey, hmacSecret };
+}
+
+/**
+ * Produce deterministic JSON with sorted keys, using Go-compatible HTML escaping.
+ * Go's json.Marshal escapes <, >, & as \u003c, \u003e, \u0026.
+ */
+export function canonicalize(m: Record): string {
+ const sorted = Object.keys(m).sort();
+ const obj: Record = {};
+ for (const k of sorted) {
+ obj[k] = m[k];
+ }
+ return goEscapeJSON(JSON.stringify(obj));
+}
+
+/**
+ * Apply Go's json.Marshal HTML escaping to a JSON string.
+ * Replaces <, >, & with their unicode escape sequences.
+ */
+export function goEscapeJSON(json: string): string {
+ return json
+ .replace(//g, '\\u003e')
+ .replace(/&/g, '\\u0026');
+}
+
+/**
+ * HMAC-SHA256 integrity token.
+ * message = canonicalize(public) + "|" + sensitiveBlob
+ * Returns "hmac-sha256:"
+ */
+export function computeIntegrity(
+ publicMap: Record,
+ sensitiveBlob: string,
+ hmacKey: Buffer,
+): string {
+ const canonical = canonicalize(publicMap);
+ const message = canonical + '|' + sensitiveBlob;
+ const sig = createHmac('sha256', hmacKey).update(message).digest('base64');
+ return 'hmac-sha256:' + sig;
+}
+
+/**
+ * AES-256-GCM encryption matching Go's EncryptSensitive.
+ * Output: base64([nonce 12B][ciphertext][authTag 16B])
+ * AAD = integrityToken string.
+ *
+ * The sensitive map is serialized with sorted keys and Go HTML escaping
+ * to match Go's json.Marshal output.
+ */
+export function encryptSensitive(
+ sensitiveMap: Record,
+ key: Buffer,
+ integrityToken: string,
+): string {
+ if (Object.keys(sensitiveMap).length === 0) {
+ return '';
+ }
+
+ // Serialize with sorted keys + Go escaping to match Go's json.Marshal
+ const plaintext = Buffer.from(goEscapeJSON(JSON.stringify(
+ Object.fromEntries(Object.keys(sensitiveMap).sort().map(k => [k, sensitiveMap[k]])),
+ )));
+
+ const nonce = randomBytes(12);
+ const cipher = createCipheriv('aes-256-gcm', key, nonce);
+ cipher.setAAD(Buffer.from(integrityToken));
+
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
+ const authTag = cipher.getAuthTag();
+
+ // Format: [nonce][ciphertext][authTag] — matches Go's gcm.Seal(nonce, nonce, plaintext, aad)
+ const blob = Buffer.concat([nonce, encrypted, authTag]);
+ return blob.toString('base64');
+}
+
+/**
+ * SHA-256 SRI hash. Returns "sha256-".
+ */
+export function computeSRI(jsonBytes: Buffer): string {
+ const hash = createHash('sha256').update(jsonBytes).digest('base64');
+ return 'sha256-' + hash;
+}
diff --git a/plugins/next/src/env.ts b/plugins/next/src/env.ts
new file mode 100644
index 0000000..0e55ccb
--- /dev/null
+++ b/plugins/next/src/env.ts
@@ -0,0 +1,87 @@
+import { readFileSync, existsSync } from 'node:fs';
+import { parse as parseDotenv } from 'dotenv';
+
+export interface ClassifiedVars {
+ public: Record;
+ sensitive: Record;
+ server: Record;
+}
+
+/**
+ * Read and parse a .env file. Returns empty object if file doesn't exist.
+ */
+export function readEnvFile(path: string): Record {
+ if (!existsSync(path)) {
+ return {};
+ }
+ const content = readFileSync(path, 'utf-8');
+ return parseDotenv(Buffer.from(content));
+}
+
+/**
+ * Classify REP_* variables by prefix into public/sensitive/server tiers.
+ * Strips the REP__ prefix from names.
+ * Rejects name collisions across tiers.
+ *
+ * Matches gateway/internal/config/classify.go:ReadAndClassify logic.
+ */
+export function classifyVariables(envValues: Record): ClassifiedVars {
+ const result: ClassifiedVars = {
+ public: {},
+ sensitive: {},
+ server: {},
+ };
+
+ // Track stripped names for collision detection: name → original key
+ const seen = new Map();
+
+ for (const [key, value] of Object.entries(envValues)) {
+ if (!key.startsWith('REP_')) continue;
+ if (key.startsWith('REP_GATEWAY_')) continue;
+
+ let name: string;
+ let tier: 'public' | 'sensitive' | 'server';
+
+ if (key.startsWith('REP_PUBLIC_')) {
+ name = key.slice('REP_PUBLIC_'.length);
+ tier = 'public';
+ } else if (key.startsWith('REP_SENSITIVE_')) {
+ name = key.slice('REP_SENSITIVE_'.length);
+ tier = 'sensitive';
+ } else if (key.startsWith('REP_SERVER_')) {
+ name = key.slice('REP_SERVER_'.length);
+ tier = 'server';
+ } else {
+ continue;
+ }
+
+ const existing = seen.get(name);
+ if (existing) {
+ throw new Error(
+ `Variable name collision: "${key}" conflicts with "${existing}" — names must be unique across tiers after prefix stripping`,
+ );
+ }
+ seen.set(name, key);
+
+ result[tier][name] = value;
+ }
+
+ return result;
+}
+
+/**
+ * Read env file + process.env, merge (process.env wins), and classify.
+ */
+export function readAndClassify(envFilePath: string): ClassifiedVars {
+ const fileVars = readEnvFile(envFilePath);
+
+ // Merge: file as base, process.env overlays
+ const merged: Record = { ...fileVars };
+ for (const [key, value] of Object.entries(process.env)) {
+ if (value !== undefined) {
+ merged[key] = value;
+ }
+ }
+
+ return classifyVariables(merged);
+}
diff --git a/plugins/next/src/guardrails.ts b/plugins/next/src/guardrails.ts
new file mode 100644
index 0000000..21d42c2
--- /dev/null
+++ b/plugins/next/src/guardrails.ts
@@ -0,0 +1,92 @@
+/**
+ * Guardrails — secret detection for REP Next.js plugin.
+ * Copied from cli/src/utils/guardrails.ts (scanValue + shannonEntropy only).
+ *
+ * Implements Shannon entropy calculation and known secret format detection
+ * per REP-RFC-0001 §3.3.
+ */
+
+export interface GuardrailWarning {
+ detectionType: 'high_entropy' | 'known_format' | 'length_anomaly';
+ message: string;
+ context?: string;
+}
+
+interface KnownSecretPrefix {
+ prefix: string;
+ service: string;
+}
+
+const knownSecretPrefixes: KnownSecretPrefix[] = [
+ { prefix: 'AKIA', service: 'AWS Access Key' },
+ { prefix: 'ASIA', service: 'AWS Temporary Access Key' },
+ { prefix: 'eyJ', service: 'JWT Token' },
+ { prefix: 'ghp_', service: 'GitHub Personal Access Token' },
+ { prefix: 'gho_', service: 'GitHub OAuth Token' },
+ { prefix: 'ghs_', service: 'GitHub Server Token' },
+ { prefix: 'ghr_', service: 'GitHub Refresh Token' },
+ { prefix: 'github_pat_', service: 'GitHub Fine-Grained PAT' },
+ { prefix: 'sk_live_', service: 'Stripe Secret Key' },
+ { prefix: 'rk_live_', service: 'Stripe Restricted Key' },
+ { prefix: 'sk-', service: 'OpenAI API Key' },
+ { prefix: 'xoxb-', service: 'Slack Bot Token' },
+ { prefix: 'xoxp-', service: 'Slack User Token' },
+ { prefix: 'xoxs-', service: 'Slack App Token' },
+ { prefix: 'SG.', service: 'SendGrid API Key' },
+ { prefix: '-----BEGIN', service: 'Private Key / Certificate' },
+ { prefix: 'AGE-SECRET-KEY-', service: 'age Encryption Key' },
+];
+
+export function shannonEntropy(s: string): number {
+ if (s.length === 0) {
+ return 0;
+ }
+
+ const freq = new Map();
+ for (const char of s) {
+ freq.set(char, (freq.get(char) || 0) + 1);
+ }
+
+ const length = s.length;
+ let entropy = 0.0;
+
+ for (const count of freq.values()) {
+ const p = count / length;
+ if (p > 0) {
+ entropy -= p * Math.log2(p);
+ }
+ }
+
+ return entropy;
+}
+
+export function scanValue(value: string): GuardrailWarning[] {
+ const warnings: GuardrailWarning[] = [];
+
+ for (const kp of knownSecretPrefixes) {
+ if (value.startsWith(kp.prefix)) {
+ warnings.push({
+ detectionType: 'known_format',
+ message: `value matches known ${kp.service} format (prefix: ${kp.prefix})`,
+ });
+ break;
+ }
+ }
+
+ const entropy = shannonEntropy(value);
+ if (entropy > 4.5 && value.length > 16) {
+ warnings.push({
+ detectionType: 'high_entropy',
+ message: `value has high entropy (${entropy.toFixed(2)} bits/char) — may be a secret`,
+ });
+ }
+
+ if (value.length > 64 && !value.includes(' ') && !value.startsWith('http')) {
+ warnings.push({
+ detectionType: 'length_anomaly',
+ message: `value is ${value.length} chars with no spaces and no URL prefix — may be an encoded secret`,
+ });
+ }
+
+ return warnings;
+}
diff --git a/plugins/next/src/index.ts b/plugins/next/src/index.ts
new file mode 100644
index 0000000..cd3bc69
--- /dev/null
+++ b/plugins/next/src/index.ts
@@ -0,0 +1,104 @@
+import React from 'react';
+import { resolve } from 'node:path';
+import { readFileSync } from 'node:fs';
+import { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { getOrCreateKeys } from './keys.js';
+import { readAndClassify } from './env.js';
+import { scanValue } from './guardrails.js';
+import { buildPayload } from './payload.js';
+
+export interface RepScriptProps {
+ /** Path to env file, relative to project root. Default: '.env.local' */
+ env?: string;
+ /** Enable strict mode — guardrail warnings become errors. Default: false */
+ strict?: boolean;
+}
+
+function getPackageVersion(): string {
+ try {
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
+ return pkg.version;
+ } catch {
+ return '0.0.0';
+ }
+}
+
+/**
+ * React Server Component that injects the REP ` breakout
+ return React.createElement('script', {
+ id: '__rep__',
+ type: 'application/json',
+ 'data-rep-version': version,
+ 'data-rep-integrity': payload.sri,
+ dangerouslySetInnerHTML: { __html: payload.json },
+ });
+}
+
+export type { Keys } from './crypto.js';
+export type { ClassifiedVars } from './env.js';
+export type { PayloadResult } from './payload.js';
+export type { GuardrailWarning } from './guardrails.js';
diff --git a/plugins/next/src/keys.ts b/plugins/next/src/keys.ts
new file mode 100644
index 0000000..2b1a920
--- /dev/null
+++ b/plugins/next/src/keys.ts
@@ -0,0 +1,26 @@
+import { generateKeys, type Keys } from './crypto.js';
+
+/**
+ * Module-scoped singleton for ephemeral cryptographic keys.
+ *
+ * In Next.js dev, the server is a long-running Node.js process, so keys
+ * persist across requests within the same server lifecycle. New keys are
+ * generated on server restart — matching the gateway's behavior.
+ *
+ * RepScript and the session-key route handler both import this module,
+ * ensuring they share the same keys (encryption key used for the blob
+ * matches the key returned by /rep/session-key).
+ */
+let _keys: Keys | null = null;
+
+export function getOrCreateKeys(): Keys {
+ if (!_keys) {
+ _keys = generateKeys();
+ }
+ return _keys;
+}
+
+/** @internal For testing only. */
+export function _resetKeys(): void {
+ _keys = null;
+}
diff --git a/plugins/next/src/payload.ts b/plugins/next/src/payload.ts
new file mode 100644
index 0000000..c464f9e
--- /dev/null
+++ b/plugins/next/src/payload.ts
@@ -0,0 +1,101 @@
+import {
+ computeIntegrity,
+ encryptSensitive,
+ computeSRI,
+ goEscapeJSON,
+ type Keys,
+} from './crypto.js';
+import type { ClassifiedVars } from './env.js';
+
+export interface PayloadResult {
+ json: string;
+ scriptTag: string;
+ sri: string;
+}
+
+export interface PayloadOptions {
+ version: string;
+ hotReload?: boolean;
+}
+
+/**
+ * Build the REP payload JSON and script tag.
+ * Mirrors gateway/pkg/payload/payload.go:Build + ScriptTag.
+ *
+ * JSON field order matches Go struct serialization:
+ * { "public": {...}, "sensitive": "...", "_meta": {...} }
+ *
+ * All string values get Go HTML escaping (< > & → \u003c \u003e \u0026).
+ *
+ * Note: `injected_at` uses JS Date.toISOString() (millisecond precision)
+ * while Go uses time.RFC3339Nano (nanosecond precision). This is harmless —
+ * the timestamp differs per-request anyway and the SRI hash is computed
+ * over the actual JSON bytes produced by each implementation.
+ */
+export function buildPayload(
+ classified: ClassifiedVars,
+ keys: Keys,
+ options: PayloadOptions,
+): PayloadResult {
+ const publicMap = classified.public;
+ const sensitiveMap = classified.sensitive;
+ const hasSensitive = Object.keys(sensitiveMap).length > 0;
+
+ // Step 1: Compute integrity over public vars only (empty sensitive blob).
+ // This matches Go's payload.go — integrity is computed BEFORE encryption,
+ // and the integrity token is used as AAD for AES-GCM.
+ const integrity = computeIntegrity(publicMap, '', keys.hmacSecret);
+
+ // Step 2: Encrypt sensitive vars if present.
+ let sensitiveBlob = '';
+ if (hasSensitive) {
+ sensitiveBlob = encryptSensitive(sensitiveMap, keys.encryptionKey, integrity);
+ }
+
+ // Step 3: Build the payload JSON manually to match Go's field order.
+ // Go's json.Marshal produces fields in struct declaration order:
+ // public, sensitive (omitempty), _meta
+ const injectedAt = new Date().toISOString();
+
+ // Build _meta object
+ const metaParts: string[] = [
+ `"version":${goEscapeJSON(JSON.stringify(options.version))}`,
+ `"injected_at":${goEscapeJSON(JSON.stringify(injectedAt))}`,
+ `"integrity":${goEscapeJSON(JSON.stringify(integrity))}`,
+ ];
+
+ if (hasSensitive) {
+ metaParts.push(`"key_endpoint":"/rep/session-key"`);
+ }
+
+ if (options.hotReload) {
+ metaParts.push(`"hot_reload":"/rep/changes"`);
+ }
+
+ metaParts.push(`"ttl":0`);
+
+ // Build public object with sorted keys + Go escaping
+ const publicSorted = Object.keys(publicMap).sort();
+ const publicParts = publicSorted.map(
+ k => `${goEscapeJSON(JSON.stringify(k))}:${goEscapeJSON(JSON.stringify(publicMap[k]))}`,
+ );
+ const publicJSON = `{${publicParts.join(',')}}`;
+
+ // Assemble top-level payload
+ const parts: string[] = [`"public":${publicJSON}`];
+
+ if (hasSensitive) {
+ parts.push(`"sensitive":${goEscapeJSON(JSON.stringify(sensitiveBlob))}`);
+ }
+
+ parts.push(`"_meta":{${metaParts.join(',')}}`);
+
+ const json = `{${parts.join(',')}}`;
+ const jsonBytes = Buffer.from(json, 'utf-8');
+ const sri = computeSRI(jsonBytes);
+
+ const scriptTag =
+ ``;
+
+ return { json, scriptTag, sri };
+}
diff --git a/plugins/next/src/session-key.ts b/plugins/next/src/session-key.ts
new file mode 100644
index 0000000..d1ec34e
--- /dev/null
+++ b/plugins/next/src/session-key.ts
@@ -0,0 +1,44 @@
+import { getOrCreateKeys } from './keys.js';
+
+/**
+ * Next.js App Router route handler for /rep/session-key.
+ *
+ * Usage:
+ * // app/api/rep/session-key/route.ts
+ * export { GET } from '@rep-protocol/next/session-key';
+ *
+ * Security:
+ * - Returns 404 in production. The gateway serves this endpoint in prod
+ * with rate limiting, single-use tokens, and CORS origin checking.
+ * This dev-only handler has none of those hardening measures.
+ * - Uses standard Response (not NextResponse) to avoid coupling to
+ * next/server internals.
+ */
+export function GET(): Response {
+ if (process.env.NODE_ENV === 'production') {
+ return new Response(
+ JSON.stringify({
+ error: 'Session key endpoint is disabled in production. Use the REP gateway.',
+ }),
+ {
+ status: 404,
+ headers: { 'Content-Type': 'application/json' },
+ },
+ );
+ }
+
+ const keys = getOrCreateKeys();
+
+ return new Response(
+ JSON.stringify({
+ key: keys.encryptionKey.toString('base64'),
+ expires_at: new Date(Date.now() + 30_000).toISOString(),
+ }),
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Cache-Control': 'no-store',
+ },
+ },
+ );
+}
diff --git a/plugins/next/tsconfig.json b/plugins/next/tsconfig.json
new file mode 100644
index 0000000..fc9103f
--- /dev/null
+++ b/plugins/next/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2020"],
+ "jsx": "react-jsx",
+ "strict": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/plugins/next/vitest.config.ts b/plugins/next/vitest.config.ts
new file mode 100644
index 0000000..5a42142
--- /dev/null
+++ b/plugins/next/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ include: ['src/**/*.test.ts'],
+ },
+});
diff --git a/plugins/vite/package.json b/plugins/vite/package.json
new file mode 100644
index 0000000..cce19f5
--- /dev/null
+++ b/plugins/vite/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "@rep-protocol/vite",
+ "version": "0.1.10",
+ "description": "Vite plugin for the Runtime Environment Protocol (REP). Injects REP environment variables during development without needing the Go gateway.",
+ "author": "Ruach Tech",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ruachtech/rep",
+ "directory": "plugins/vite"
+ },
+ "homepage": "https://github.com/ruachtech/rep#readme",
+ "keywords": [
+ "vite",
+ "vite-plugin",
+ "environment-variables",
+ "runtime-config",
+ "docker",
+ "containers",
+ "frontend",
+ "rep"
+ ],
+ "main": "dist/index.js",
+ "module": "dist/index.mjs",
+ "types": "dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsup src/index.ts --format cjs,esm --dts",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "typecheck": "tsc --noEmit"
+ },
+ "peerDependencies": {
+ "vite": ">=4.0.0"
+ },
+ "dependencies": {
+ "dotenv": "^16.4.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.11.0",
+ "tsup": "^8.0.0",
+ "typescript": "^5.4.0",
+ "vite": "^5.0.0",
+ "vitest": "^1.6.0"
+ }
+}
diff --git a/plugins/vite/src/__tests__/crypto.test.ts b/plugins/vite/src/__tests__/crypto.test.ts
new file mode 100644
index 0000000..c5c868d
--- /dev/null
+++ b/plugins/vite/src/__tests__/crypto.test.ts
@@ -0,0 +1,183 @@
+import { describe, it, expect } from 'vitest';
+import { createDecipheriv, createHmac, createHash } from 'node:crypto';
+import {
+ deriveKey,
+ generateKeys,
+ canonicalize,
+ goEscapeJSON,
+ computeIntegrity,
+ encryptSensitive,
+ computeSRI,
+} from '../crypto.js';
+
+describe('deriveKey', () => {
+ it('produces deterministic output for same inputs', () => {
+ const ikm = Buffer.alloc(32, 0xaa);
+ const salt = Buffer.alloc(32, 0xbb);
+ const a = deriveKey(ikm, salt, 'rep-blob-encryption-v1', 32);
+ const b = deriveKey(ikm, salt, 'rep-blob-encryption-v1', 32);
+ expect(a).toEqual(b);
+ });
+
+ it('produces different output for different info strings', () => {
+ const ikm = Buffer.alloc(32, 0xaa);
+ const salt = Buffer.alloc(32, 0xbb);
+ const a = deriveKey(ikm, salt, 'rep-blob-encryption-v1', 32);
+ const b = deriveKey(ikm, salt, 'different-info', 32);
+ expect(a).not.toEqual(b);
+ });
+
+ it('throws for length > 32', () => {
+ expect(() => deriveKey(Buffer.alloc(32), Buffer.alloc(32), 'test', 33)).toThrow();
+ });
+
+ it('matches manual HKDF-SHA256 computation', () => {
+ const ikm = Buffer.from('test-master-key-material-here!!!' );
+ const salt = Buffer.from('test-salt-value-here-32-bytes!!!' );
+ const info = 'rep-blob-encryption-v1';
+
+ // Manual HKDF
+ const prk = createHmac('sha256', salt).update(ikm).digest();
+ const okm = createHmac('sha256', prk)
+ .update(info)
+ .update(Buffer.from([0x01]))
+ .digest();
+
+ const result = deriveKey(ikm, salt, info, 32);
+ expect(result).toEqual(okm);
+ });
+});
+
+describe('generateKeys', () => {
+ it('returns 32-byte keys', () => {
+ const keys = generateKeys();
+ expect(keys.encryptionKey).toHaveLength(32);
+ expect(keys.hmacSecret).toHaveLength(32);
+ });
+
+ it('generates different keys on each call', () => {
+ const a = generateKeys();
+ const b = generateKeys();
+ expect(a.encryptionKey).not.toEqual(b.encryptionKey);
+ expect(a.hmacSecret).not.toEqual(b.hmacSecret);
+ });
+});
+
+describe('canonicalize', () => {
+ it('sorts keys alphabetically', () => {
+ const result = canonicalize({ Z: '1', A: '2', M: '3' });
+ expect(result).toBe('{"A":"2","M":"3","Z":"1"}');
+ });
+
+ it('produces empty object for empty map', () => {
+ expect(canonicalize({})).toBe('{}');
+ });
+
+ it('applies Go HTML escaping', () => {
+ const result = canonicalize({ KEY: '' });
+ expect(result).toContain('\\u003c');
+ expect(result).toContain('\\u003e');
+ expect(result).toContain('\\u0026');
+ expect(result).not.toContain('<');
+ expect(result).not.toContain('>');
+ expect(result).not.toContain('&');
+ });
+});
+
+describe('goEscapeJSON', () => {
+ it('escapes HTML characters like Go json.Marshal', () => {
+ expect(goEscapeJSON('"`;
+
+ return { json, scriptTag, sri };
+}
diff --git a/plugins/vite/tsconfig.json b/plugins/vite/tsconfig.json
new file mode 100644
index 0000000..d6a2b76
--- /dev/null
+++ b/plugins/vite/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2020"],
+ "strict": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/plugins/vite/vitest.config.ts b/plugins/vite/vitest.config.ts
new file mode 100644
index 0000000..5a42142
--- /dev/null
+++ b/plugins/vite/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ include: ['src/**/*.test.ts'],
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 19b17fa..d2ddc38 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -153,39 +153,58 @@ importers:
specifier: ^5.4.0
version: 5.9.3
- examples/todo-react:
+ plugins/next:
dependencies:
- '@rep-protocol/react':
- specifier: workspace:*
- version: link:../../adapters/react
- '@rep-protocol/sdk':
- specifier: workspace:*
- version: link:../../sdk
- react:
- specifier: ^18.3.0
- version: 18.3.1
- react-dom:
- specifier: ^18.3.0
- version: 18.3.1(react@18.3.1)
+ dotenv:
+ specifier: ^16.4.0
+ version: 16.6.1
devDependencies:
- '@rep-protocol/cli':
- specifier: workspace:*
- version: link:../../cli
+ '@types/node':
+ specifier: ^20.11.0
+ version: 20.19.33
'@types/react':
specifier: ^18.3.0
version: 18.3.28
- '@types/react-dom':
- specifier: ^18.3.0
- version: 18.3.7(@types/react@18.3.28)
- '@vitejs/plugin-react':
- specifier: ^4.3.0
- version: 4.7.0(vite@5.4.21(@types/node@20.19.33))
+ next:
+ specifier: ^15.0.0
+ version: 15.5.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react:
+ specifier: ^19.0.0
+ version: 19.2.4
+ react-dom:
+ specifier: ^19.0.0
+ version: 19.2.4(react@19.2.4)
+ tsup:
+ specifier: ^8.0.0
+ version: 8.5.1(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2)
typescript:
specifier: ^5.4.0
version: 5.9.3
- vite:
+ vitest:
+ specifier: ^1.6.0
+ version: 1.6.1(@types/node@20.19.33)(jsdom@28.1.0)
+
+ plugins/vite:
+ dependencies:
+ dotenv:
+ specifier: ^16.4.0
+ version: 16.6.1
+ devDependencies:
+ '@types/node':
+ specifier: ^20.11.0
+ version: 20.19.33
+ tsup:
+ specifier: ^8.0.0
+ version: 8.5.1(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2)
+ typescript:
specifier: ^5.4.0
+ version: 5.9.3
+ vite:
+ specifier: ^5.0.0
version: 5.4.21(@types/node@20.19.33)
+ vitest:
+ specifier: ^1.6.0
+ version: 1.6.1(@types/node@20.19.33)(jsdom@28.1.0)
sdk:
devDependencies:
@@ -413,18 +432,6 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-react-jsx-self@7.27.1':
- resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
- '@babel/plugin-transform-react-jsx-source@7.27.1':
- resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
- engines: {node: '>=6.9.0'}
- peerDependencies:
- '@babel/core': ^7.0.0-0
-
'@babel/plugin-transform-typescript@7.28.6':
resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==}
engines: {node: '>=6.9.0'}
@@ -1268,6 +1275,57 @@ packages:
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
+ '@next/env@15.5.13':
+ resolution: {integrity: sha512-6h7Fm29+/u1WBPcPaQl0xBov7KXB6i0c8oFlSlehD+PuZJQjzXQBuYzfkM32G5iWOlKsXXyRtcMaaqwspRBujA==}
+
+ '@next/swc-darwin-arm64@15.5.13':
+ resolution: {integrity: sha512-XrBbj2iY1mQSsJ8RoFClNpUB9uuZejP94v9pJuSAzdzwFVHeP+Vu2vzBCHwSObozgYNuTVwKhLukG1rGCgj8xA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@next/swc-darwin-x64@15.5.13':
+ resolution: {integrity: sha512-Ey3fuUeWDWtVdgiLHajk2aJ74Y8EWLeqvfwlkB5RvWsN7F1caQ6TjifsQzrAcOuNSnogGvFNYzjQlu7tu0kyWg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@next/swc-linux-arm64-gnu@15.5.13':
+ resolution: {integrity: sha512-aLtu/WxDeL3188qx3zyB3+iw8nAB9F+2Mhyz9nNZpzsREc2t8jQTuiWY4+mtOgWp1d+/Q4eXuy9m3dwh3n1IyQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-arm64-musl@15.5.13':
+ resolution: {integrity: sha512-9VZ0OsVx9PEL72W50QD15iwSCF3GD/dwj42knfF5C4aiBPXr95etGIOGhb8rU7kpnzZuPNL81CY4vIyUKa2xvg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@next/swc-linux-x64-gnu@15.5.13':
+ resolution: {integrity: sha512-3knsu9H33e99ZfiWh0Bb04ymEO7YIiopOpXKX89ZZ/ER0iyfV1YLoJFxJJQNUD7OR8O7D7eiLI/TXPryPGv3+A==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-linux-x64-musl@15.5.13':
+ resolution: {integrity: sha512-AVPb6+QZ0pPanJFc1hpx81I5tTiBF4VITw5+PMaR1CrboAUUxtxn3IsV0h48xI7fzd6/zw9D9i6khRwME5NKUw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@next/swc-win32-arm64-msvc@15.5.13':
+ resolution: {integrity: sha512-FZ/HXuTxn+e5Lp6oRZMvHaMJx22gAySveJdJE0//91Nb9rMuh2ftgKlEwBFJxhkw5kAF/yIXz3iBf0tvDXRmCA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@next/swc-win32-x64-msvc@15.5.13':
+ resolution: {integrity: sha512-B5E82pX3VXu6Ib5mDuZEqGwT8asocZe3OMMnaM+Yfs0TRlmSQCBQUUXR9BkXQeGVboOWS1pTsRkS9wzFd8PABw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
@@ -1304,9 +1362,6 @@ packages:
cpu: [x64]
os: [win32]
- '@rolldown/pluginutils@1.0.0-beta.27':
- resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
-
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
@@ -1465,6 +1520,9 @@ packages:
'@sinclair/typebox@0.27.10':
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
+ '@swc/helpers@0.5.15':
+ resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+
'@testing-library/dom@9.3.4':
resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==}
engines: {node: '>=14'}
@@ -1479,18 +1537,6 @@ packages:
'@types/aria-query@5.0.4':
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
- '@types/babel__core@7.20.5':
- resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
-
- '@types/babel__generator@7.27.0':
- resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
-
- '@types/babel__template@7.4.4':
- resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
-
- '@types/babel__traverse@7.28.0':
- resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
-
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -1547,12 +1593,6 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
- '@vitejs/plugin-react@4.7.0':
- resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
- engines: {node: ^14.18.0 || >=16.0.0}
- peerDependencies:
- vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
-
'@vitest/expect@1.6.1':
resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==}
@@ -1864,6 +1904,9 @@ packages:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
+ client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -2926,6 +2969,27 @@ packages:
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
engines: {node: '>= 10'}
+ next@15.5.13:
+ resolution: {integrity: sha512-n0AXf6vlTwGuM93Z++POtjMsRuQ9pT5v2URPciXKUQIl/EB2WjXF0YiIUxaa9AEMFaMpZlaG3KPK6i4UVnx9eQ==}
+ engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.51.1
+ babel-plugin-react-compiler: '*'
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+
nlcst-to-string@4.0.0:
resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==}
@@ -3135,6 +3199,10 @@ packages:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
+ postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -3175,20 +3243,25 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-dom@19.2.4:
+ resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
+ peerDependencies:
+ react: ^19.2.4
+
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
- react-refresh@0.17.0:
- resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
- engines: {node: '>=0.10.0'}
-
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
+ react@19.2.4:
+ resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
+ engines: {node: '>=0.10.0'}
+
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
@@ -3326,6 +3399,9 @@ packages:
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
@@ -3474,6 +3550,19 @@ packages:
style-to-object@1.0.14:
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
+ styled-jsx@5.1.6:
+ resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -4441,16 +4530,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
- dependencies:
- '@babel/core': 7.29.0
- '@babel/helper-plugin-utils': 7.28.6
-
- '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)':
- dependencies:
- '@babel/core': 7.29.0
- '@babel/helper-plugin-utils': 7.28.6
-
'@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -5051,6 +5130,32 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@next/env@15.5.13': {}
+
+ '@next/swc-darwin-arm64@15.5.13':
+ optional: true
+
+ '@next/swc-darwin-x64@15.5.13':
+ optional: true
+
+ '@next/swc-linux-arm64-gnu@15.5.13':
+ optional: true
+
+ '@next/swc-linux-arm64-musl@15.5.13':
+ optional: true
+
+ '@next/swc-linux-x64-gnu@15.5.13':
+ optional: true
+
+ '@next/swc-linux-x64-musl@15.5.13':
+ optional: true
+
+ '@next/swc-win32-arm64-msvc@15.5.13':
+ optional: true
+
+ '@next/swc-win32-x64-msvc@15.5.13':
+ optional: true
+
'@oslojs/encoding@1.1.0': {}
'@pagefind/darwin-arm64@1.4.0':
@@ -5073,8 +5178,6 @@ snapshots:
'@pagefind/windows-x64@1.4.0':
optional: true
- '@rolldown/pluginutils@1.0.0-beta.27': {}
-
'@rollup/pluginutils@5.3.0(rollup@4.58.0)':
dependencies:
'@types/estree': 1.0.8
@@ -5193,6 +5296,10 @@ snapshots:
'@sinclair/typebox@0.27.10': {}
+ '@swc/helpers@0.5.15':
+ dependencies:
+ tslib: 2.8.1
+
'@testing-library/dom@9.3.4':
dependencies:
'@babel/code-frame': 7.29.0
@@ -5216,27 +5323,6 @@ snapshots:
'@types/aria-query@5.0.4': {}
- '@types/babel__core@7.20.5':
- dependencies:
- '@babel/parser': 7.29.0
- '@babel/types': 7.29.0
- '@types/babel__generator': 7.27.0
- '@types/babel__template': 7.4.4
- '@types/babel__traverse': 7.28.0
-
- '@types/babel__generator@7.27.0':
- dependencies:
- '@babel/types': 7.29.0
-
- '@types/babel__template@7.4.4':
- dependencies:
- '@babel/parser': 7.29.0
- '@babel/types': 7.29.0
-
- '@types/babel__traverse@7.28.0':
- dependencies:
- '@babel/types': 7.29.0
-
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -5292,18 +5378,6 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
- '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.33))':
- dependencies:
- '@babel/core': 7.29.0
- '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
- '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0)
- '@rolldown/pluginutils': 1.0.0-beta.27
- '@types/babel__core': 7.20.5
- react-refresh: 0.17.0
- vite: 5.4.21(@types/node@20.19.33)
- transitivePeerDependencies:
- - supports-color
-
'@vitest/expect@1.6.1':
dependencies:
'@vitest/spy': 1.6.1
@@ -5754,6 +5828,8 @@ snapshots:
cli-boxes@3.0.0: {}
+ client-only@0.0.1: {}
+
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@@ -7315,6 +7391,29 @@ snapshots:
neotraverse@0.6.18: {}
+ next@15.5.13(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ '@next/env': 15.5.13
+ '@swc/helpers': 0.5.15
+ caniuse-lite: 1.0.30001770
+ postcss: 8.4.31
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ styled-jsx: 5.1.6(react@19.2.4)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 15.5.13
+ '@next/swc-darwin-x64': 15.5.13
+ '@next/swc-linux-arm64-gnu': 15.5.13
+ '@next/swc-linux-arm64-musl': 15.5.13
+ '@next/swc-linux-x64-gnu': 15.5.13
+ '@next/swc-linux-x64-musl': 15.5.13
+ '@next/swc-win32-arm64-msvc': 15.5.13
+ '@next/swc-win32-x64-msvc': 15.5.13
+ sharp: 0.34.5
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
nlcst-to-string@4.0.0:
dependencies:
'@types/nlcst': 2.0.3
@@ -7514,6 +7613,12 @@ snapshots:
cssesc: 3.0.0
util-deprecate: 1.0.2
+ postcss@8.4.31:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -7553,16 +7658,21 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-dom@19.2.4(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ scheduler: 0.27.0
+
react-is@17.0.2: {}
react-is@18.3.1: {}
- react-refresh@0.17.0: {}
-
react@18.3.1:
dependencies:
loose-envify: 1.4.0
+ react@19.2.4: {}
+
readdirp@4.1.2: {}
readdirp@5.0.0: {}
@@ -7808,6 +7918,8 @@ snapshots:
dependencies:
loose-envify: 1.4.0
+ scheduler@0.27.0: {}
+
semver@5.7.2: {}
semver@6.3.1: {}
@@ -8021,6 +8133,11 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
+ styled-jsx@5.1.6(react@19.2.4):
+ dependencies:
+ client-only: 0.0.1
+ react: 19.2.4
+
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 80f4029..964a830 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,5 +2,6 @@ packages:
- 'sdk'
- 'cli'
- 'adapters/*'
+ - 'plugins/*'
- 'codemod'
- 'docs'
diff --git a/release-please-config.json b/release-please-config.json
index a7174e2..6deb066 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -16,7 +16,9 @@
"codemod",
"react",
"vue",
- "svelte"
+ "svelte",
+ "vite",
+ "next"
]
}
],
@@ -39,6 +41,12 @@
"adapters/svelte": {
"component": "svelte"
},
+ "plugins/vite": {
+ "component": "vite"
+ },
+ "plugins/next": {
+ "component": "next"
+ },
"gateway": {
"release-type": "simple",
"component": "gateway",