anhedral 0.8.6 → 0.9.0
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/README.md +141 -38
- package/assets/anhedral-cli-init-technical.svg +15 -15
- package/assets/anhedral-cli-init.svg +16 -16
- package/dist/access.d.ts +10 -0
- package/dist/access.js +93 -0
- package/dist/auth-schema-snapshot.d.ts +346 -0
- package/dist/auth-schema-snapshot.js +347 -0
- package/dist/auth-schema.d.ts +1 -0
- package/dist/auth-schema.js +57 -0
- package/dist/bin.js +345 -39
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +30 -0
- package/dist/capabilities.d.ts +24 -42
- package/dist/capabilities.js +73 -75
- package/dist/cloudflare.d.ts +14 -0
- package/dist/cloudflare.js +66 -0
- package/dist/command.js +12 -3
- package/dist/delivery.d.ts +118 -0
- package/dist/delivery.js +611 -0
- package/dist/dependencies.d.ts +24 -3
- package/dist/dependencies.js +112 -74
- package/dist/dependency-order.d.ts +2 -0
- package/dist/dependency-order.js +24 -0
- package/dist/index.d.ts +18 -3
- package/dist/index.js +12 -2
- package/dist/managed-hosting.d.ts +49 -0
- package/dist/managed-hosting.js +373 -0
- package/dist/management.d.ts +3 -0
- package/dist/management.js +47 -0
- package/dist/platforms/desktop.d.ts +2 -2
- package/dist/platforms/desktop.js +25 -15
- package/dist/platforms/extension.d.ts +2 -2
- package/dist/platforms/extension.js +104 -66
- package/dist/platforms/mobile.d.ts +2 -2
- package/dist/platforms/mobile.js +13 -7
- package/dist/pnpm-policy.d.ts +0 -2
- package/dist/pnpm-policy.js +18 -13
- package/dist/project-state.d.ts +28 -0
- package/dist/project-state.js +226 -0
- package/dist/provider-readiness.d.ts +30 -0
- package/dist/provider-readiness.js +87 -0
- package/dist/provision.d.ts +46 -0
- package/dist/provision.js +197 -0
- package/dist/readiness.d.ts +4 -2
- package/dist/readiness.js +81 -14
- package/dist/registry/add.d.ts +22 -0
- package/dist/registry/add.js +198 -0
- package/dist/registry/bootstrap.d.ts +2 -0
- package/dist/registry/bootstrap.js +14 -0
- package/dist/registry/commands.d.ts +2 -0
- package/dist/registry/commands.js +99 -0
- package/dist/registry/context.d.ts +3 -0
- package/dist/registry/context.js +21 -0
- package/dist/registry/options.d.ts +2 -0
- package/dist/registry/options.js +65 -0
- package/dist/registry/receipt.d.ts +3 -0
- package/dist/registry/receipt.js +73 -0
- package/dist/registry/remote.d.ts +25 -0
- package/dist/registry/remote.js +168 -0
- package/dist/registry/resolve.d.ts +10 -0
- package/dist/registry/resolve.js +98 -0
- package/dist/registry/types.d.ts +64 -0
- package/dist/registry/usage.d.ts +1 -0
- package/dist/registry/usage.js +38 -0
- package/dist/render.d.ts +0 -4
- package/dist/render.js +3 -16
- package/dist/repository.d.ts +24 -0
- package/dist/repository.js +96 -0
- package/dist/scaffold/api.d.ts +2 -0
- package/dist/scaffold/api.js +13 -0
- package/dist/scaffold/auth.d.ts +3 -0
- package/dist/scaffold/auth.js +95 -0
- package/dist/scaffold/bootstrap.d.ts +2 -0
- package/dist/scaffold/bootstrap.js +34 -0
- package/dist/scaffold/compose.d.ts +4 -0
- package/dist/scaffold/compose.js +34 -0
- package/dist/scaffold/database.d.ts +2 -0
- package/dist/scaffold/database.js +43 -0
- package/dist/scaffold/documentation.d.ts +3 -0
- package/dist/scaffold/documentation.js +91 -0
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +5 -0
- package/dist/scaffold/files.d.ts +25 -0
- package/dist/scaffold/files.js +108 -0
- package/dist/scaffold/integrations.d.ts +3 -0
- package/dist/scaffold/integrations.js +7 -0
- package/dist/scaffold/services.d.ts +5 -0
- package/dist/scaffold/services.js +84 -0
- package/dist/scaffold/web.d.ts +2 -0
- package/dist/scaffold/web.js +102 -0
- package/dist/scaffold/workspace.d.ts +3 -0
- package/dist/scaffold/workspace.js +150 -0
- package/dist/security-backports.js +7 -2
- package/dist/stack-registry.d.ts +80 -0
- package/dist/stack-registry.js +424 -0
- package/dist/standard.d.ts +6 -19
- package/dist/standard.js +46 -653
- package/dist/transaction.js +47 -56
- package/dist/util.js +12 -2
- package/dist/vercel.d.ts +98 -0
- package/dist/vercel.js +796 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.d.ts +1 -0
- package/dist/vinext.js +83 -0
- package/dist/workspace-graph.d.ts +7 -0
- package/dist/workspace-graph.js +73 -0
- package/docs/application-stack-standard.md +76 -91
- package/docs/delivery-workflow.md +83 -0
- package/docs/managed-hosting-architecture.md +87 -0
- package/docs/sponsorships.md +25 -0
- package/package.json +23 -29
- package/dist/evidence-validation.d.ts +0 -2
- package/dist/evidence-validation.js +0 -18
- package/dist/foundation.d.ts +0 -6
- package/dist/foundation.js +0 -49
- package/dist/progress-view.d.ts +0 -57
- package/dist/progress-view.js +0 -69
- package/dist/progress.d.ts +0 -95
- package/dist/progress.js +0 -327
- package/dist/project.d.ts +0 -5
- package/dist/standard-products.d.ts +0 -2
- package/dist/standard-products.js +0 -6
- /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
- /package/dist/{project.js → registry/types.js} +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { STACK_DEPENDENCIES } from '../dependencies.js';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { writeBetterAuthSchema } from '../auth-schema.js';
|
|
5
|
+
import { json, put, readJson, patchPackage, sharedPackage } from './files.js';
|
|
6
|
+
export function scaffoldBetterAuth({ root, options, runtimeApp, has }) {
|
|
7
|
+
writeBetterAuthSchema(root, has('neon'));
|
|
8
|
+
sharedPackage(root, 'auth', `import { betterAuth } from 'better-auth';
|
|
9
|
+
import { drizzleAdapter } from '@better-auth/drizzle-adapter';
|
|
10
|
+
import type { createDatabase } from '@workspace/db';
|
|
11
|
+
export function createAuth(db: ReturnType<typeof createDatabase>, secret: string, baseURL: string) {
|
|
12
|
+
return betterAuth({ database: drizzleAdapter(db, { provider: '${has('neon') ? 'pg' : 'sqlite'}', transaction: false }), secret, baseURL, emailAndPassword: { enabled: true } });
|
|
13
|
+
}
|
|
14
|
+
`, {
|
|
15
|
+
'better-auth': '1.7.7',
|
|
16
|
+
'@better-auth/drizzle-adapter': '1.7.7',
|
|
17
|
+
'@workspace/db': 'workspace:*',
|
|
18
|
+
});
|
|
19
|
+
patchPackage(root, `apps/${runtimeApp}/package.json`, {
|
|
20
|
+
dependencies: { '@workspace/auth': 'workspace:*' },
|
|
21
|
+
});
|
|
22
|
+
const connection = has('neon') ? 'createDatabase(env.DATABASE_URL)' : 'createDatabase(env.DB)';
|
|
23
|
+
if (has('hono')) {
|
|
24
|
+
const source = readFileSync(path.join(root, 'apps/api/src/index.ts'), 'utf8');
|
|
25
|
+
put(root, 'apps/api/src/index.ts', `import { createAuth } from '@workspace/auth';\nimport { createDatabase } from '@workspace/db';\n` +
|
|
26
|
+
source.replace('export default app;', `app.on(['GET', 'POST'], '/api/auth/*', async (c) => {\n const env = c.env;\n const database = ${connection};\n return await createAuth(database, env.BETTER_AUTH_SECRET, env.BETTER_AUTH_URL).handler(c.req.raw);\n});\nexport default app;`));
|
|
27
|
+
const config = readJson(root, 'apps/api/wrangler.jsonc');
|
|
28
|
+
json(root, 'apps/api/wrangler.jsonc', {
|
|
29
|
+
...config,
|
|
30
|
+
vars: { BETTER_AUTH_URL: 'http://localhost:8787' },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
put(root, 'apps/web/app/api/auth/[...all]/route.ts', `${has('vinext') ? "import { env } from 'cloudflare:workers';" : options.hosting === 'cloudflare' ? "import { getCloudflareContext } from '@opennextjs/cloudflare';" : ''}
|
|
35
|
+
import { createAuth } from '@workspace/auth';
|
|
36
|
+
import { createDatabase } from '@workspace/db';
|
|
37
|
+
async function handler(request: Request) {
|
|
38
|
+
${options.hosting === 'cloudflare' && !has('vinext') ? 'const { env } = await getCloudflareContext({ async: true });' : ''}
|
|
39
|
+
const database = ${options.hosting === 'vercel' ? 'createDatabase(process.env.DATABASE_URL!)' : connection};
|
|
40
|
+
return await createAuth(database, ${options.hosting === 'cloudflare' ? 'env.BETTER_AUTH_SECRET, env.BETTER_AUTH_URL' : 'process.env.BETTER_AUTH_SECRET!, process.env.BETTER_AUTH_URL!'}).handler(request);
|
|
41
|
+
}
|
|
42
|
+
export { handler as GET, handler as POST };
|
|
43
|
+
`);
|
|
44
|
+
if (options.hosting === 'cloudflare' && !has('vinext'))
|
|
45
|
+
put(root, 'apps/web/cloudflare-env.d.ts', `interface CloudflareEnv { BETTER_AUTH_SECRET: string; BETTER_AUTH_URL: string; ${has('neon') ? 'DATABASE_URL: string;' : "DB: Parameters<typeof import('drizzle-orm/d1').drizzle>[0];"} }\n`);
|
|
46
|
+
if (has('vinext')) {
|
|
47
|
+
put(root, 'apps/web/secrets.d.ts', 'declare namespace Cloudflare { interface Env { BETTER_AUTH_SECRET: string } }\n');
|
|
48
|
+
const config = readJson(root, 'apps/web/wrangler.jsonc');
|
|
49
|
+
json(root, 'apps/web/wrangler.jsonc', {
|
|
50
|
+
...config,
|
|
51
|
+
vars: { BETTER_AUTH_URL: 'http://localhost:3000' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
patchPackage(root, 'apps/web/package.json', {
|
|
55
|
+
dependencies: { 'drizzle-orm': STACK_DEPENDENCIES['drizzle-orm'] },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
put(root, `apps/${runtimeApp}/${runtimeApp === 'web' && options.hosting === 'vercel' ? '.env.example' : '.dev.vars.example'}`, `${has('neon') ? 'DATABASE_URL=\n' : ''}BETTER_AUTH_SECRET=\nBETTER_AUTH_URL=http://localhost:${runtimeApp === 'web' && (options.hosting === 'vercel' || has('vinext')) ? '3000' : '8787'}\n`);
|
|
59
|
+
if (has('hono'))
|
|
60
|
+
put(root, 'apps/api/src/secrets.d.ts', 'interface Env { BETTER_AUTH_SECRET: string }\n');
|
|
61
|
+
}
|
|
62
|
+
export function scaffoldClerk({ root, has }) {
|
|
63
|
+
if (has('next') || has('vinext')) {
|
|
64
|
+
patchPackage(root, 'apps/web/package.json', {
|
|
65
|
+
dependencies: { '@clerk/nextjs': STACK_DEPENDENCIES['@clerk/nextjs'] },
|
|
66
|
+
});
|
|
67
|
+
const layoutPath = path.join(root, 'apps/web/app/layout.tsx');
|
|
68
|
+
if (existsSync(layoutPath)) {
|
|
69
|
+
const layout = readFileSync(layoutPath, 'utf8');
|
|
70
|
+
if (!layout.includes('ClerkProvider')) {
|
|
71
|
+
const wrapped = layout.replace(/(<body\b[^>]*>)([\s\S]*?)(<\/body>)/, '$1<ClerkProvider>$2</ClerkProvider>$3');
|
|
72
|
+
if (wrapped === layout)
|
|
73
|
+
throw new Error('Cannot configure ClerkProvider: the web layout must contain a body element.');
|
|
74
|
+
put(root, 'apps/web/app/layout.tsx', 'import { ClerkProvider } from "@clerk/nextjs";\n' + wrapped);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
put(root, 'apps/web/app/sign-in/[[...sign-in]]/page.tsx', 'import { SignIn } from "@clerk/nextjs";\nexport default function SignInPage() { return <SignIn />; }\n');
|
|
78
|
+
put(root, 'apps/web/app/sign-up/[[...sign-up]]/page.tsx', 'import { SignUp } from "@clerk/nextjs";\nexport default function SignUpPage() { return <SignUp />; }\n');
|
|
79
|
+
put(root, 'apps/web/proxy.ts', `import { clerkMiddleware } from '@clerk/nextjs/server';
|
|
80
|
+
export default clerkMiddleware();
|
|
81
|
+
export const config = { matcher: ['/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico)).*)', '/(api|trpc)(.*)'] };
|
|
82
|
+
`);
|
|
83
|
+
// Middleware makes auth available; product routes must explicitly require identity and authorization.
|
|
84
|
+
}
|
|
85
|
+
if (has('hono')) {
|
|
86
|
+
patchPackage(root, 'apps/api/package.json', {
|
|
87
|
+
dependencies: { '@clerk/backend': STACK_DEPENDENCIES['@clerk/backend'] },
|
|
88
|
+
});
|
|
89
|
+
put(root, 'apps/api/src/identity.ts', `import { createClerkClient } from '@clerk/backend';
|
|
90
|
+
export async function authenticate(request: Request, secretKey: string, authorizedParties: string[]) {
|
|
91
|
+
return createClerkClient({ secretKey }).authenticateRequest(request, { authorizedParties });
|
|
92
|
+
}
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { appendGitignore } from '../util.js';
|
|
4
|
+
import { bootstrapArguments } from '../registry/bootstrap.js';
|
|
5
|
+
export function bootstrapWorkspace(root, name, run, webOnly = false) {
|
|
6
|
+
const temporary = path.join(root, '.bootstrap');
|
|
7
|
+
mkdirSync(temporary);
|
|
8
|
+
try {
|
|
9
|
+
run('pnpm', bootstrapArguments(name, temporary), root);
|
|
10
|
+
const generated = path.join(temporary, name);
|
|
11
|
+
if (!existsSync(path.join(generated, 'pnpm-workspace.yaml')) ||
|
|
12
|
+
!existsSync(path.join(generated, 'apps/web/package.json')))
|
|
13
|
+
throw new Error('Official shadcn bootstrap did not produce the expected monorepo.');
|
|
14
|
+
const ignored = ['.git', 'node_modules', '.next', '.turbo'];
|
|
15
|
+
const seed = !webOnly && existsSync(path.join(root, '.gitignore'))
|
|
16
|
+
? readFileSync(path.join(root, '.gitignore'), 'utf8')
|
|
17
|
+
: '';
|
|
18
|
+
for (const entry of webOnly ? ['apps/web', 'packages/ui'] : readdirSync(generated)) {
|
|
19
|
+
if (ignored.includes(entry))
|
|
20
|
+
continue;
|
|
21
|
+
if (webOnly && existsSync(path.join(root, entry)))
|
|
22
|
+
throw new Error(`Web foundation target already exists: ${entry}.`);
|
|
23
|
+
cpSync(path.join(generated, entry), path.join(root, entry), {
|
|
24
|
+
recursive: true,
|
|
25
|
+
filter: (source) => !ignored.includes(path.basename(source)),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (seed)
|
|
29
|
+
appendGitignore(root, seed.split(/\r?\n/));
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResolvedStackRegistry } from '../registry/resolve.js';
|
|
2
|
+
import type { StandardOptions } from '../registry/types.js';
|
|
3
|
+
/** Only used in a fresh staged workspace. Existing applications are never overlaid. */
|
|
4
|
+
export declare function composeStack(root: string, options: StandardOptions, registry: ResolvedStackRegistry): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { installStackItem } from '../stack-registry.js';
|
|
4
|
+
import { createRecipeContext } from '../registry/context.js';
|
|
5
|
+
import { inspectWorkspaceGraph } from '../workspace-graph.js';
|
|
6
|
+
import { writeStandardRoot } from './workspace.js';
|
|
7
|
+
import { json } from './files.js';
|
|
8
|
+
/** Only used in a fresh staged workspace. Existing applications are never overlaid. */
|
|
9
|
+
export async function composeStack(root, options, registry) {
|
|
10
|
+
const context = createRecipeContext(root, options);
|
|
11
|
+
if (!context.has('next') && !context.has('vinext')) {
|
|
12
|
+
for (const directory of ['apps/web', 'packages/ui'])
|
|
13
|
+
rmSync(path.join(root, directory), { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
for (const item of registry.items)
|
|
16
|
+
Object.assign(context.bindings, item.bindings);
|
|
17
|
+
for (const item of registry.items) {
|
|
18
|
+
if (item.installer === 'recipe')
|
|
19
|
+
await installStackItem(item, context);
|
|
20
|
+
}
|
|
21
|
+
writeStandardRoot(root, options, registry);
|
|
22
|
+
for (const item of registry.items) {
|
|
23
|
+
for (const target of item.targets) {
|
|
24
|
+
if (!existsSync(path.join(root, target)))
|
|
25
|
+
throw new Error(`Registry target missing: ${item.name} -> ${target}.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
json(root, 'anhedral.registry.json', {
|
|
29
|
+
...registry,
|
|
30
|
+
status: 'scaffolded',
|
|
31
|
+
workspaces: inspectWorkspaceGraph(root),
|
|
32
|
+
resourcesProvisioned: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { STACK_DEPENDENCIES } from '../dependencies.js';
|
|
2
|
+
import { put, patchPackage, sharedPackage } from './files.js';
|
|
3
|
+
export function scaffoldDatabase({ root, options, runtimeApp, has }) {
|
|
4
|
+
sharedPackage(root, 'db', has('neon')
|
|
5
|
+
? `import { drizzle } from 'drizzle-orm/neon-http';
|
|
6
|
+
import { neon } from '@neondatabase/serverless';
|
|
7
|
+
import * as schema from './schema';
|
|
8
|
+
export function createDatabase(connectionString: string) {
|
|
9
|
+
// HTTP supports independent queries and batches; choose a reviewed WebSocket adapter for interactive transactions.
|
|
10
|
+
return drizzle(neon(connectionString), { schema });
|
|
11
|
+
}
|
|
12
|
+
`
|
|
13
|
+
: `import { drizzle } from 'drizzle-orm/d1';
|
|
14
|
+
import * as schema from './schema';
|
|
15
|
+
export function createDatabase(binding: Parameters<typeof drizzle>[0]) { return drizzle(binding, { schema }); }
|
|
16
|
+
`, {
|
|
17
|
+
'drizzle-orm': STACK_DEPENDENCIES['drizzle-orm'],
|
|
18
|
+
...(has('neon') ? { '@neondatabase/serverless': '1.2.0' } : {}),
|
|
19
|
+
});
|
|
20
|
+
if (has('neon')) {
|
|
21
|
+
const native = runtimeApp === 'web' && options.hosting === 'vercel';
|
|
22
|
+
put(root, `apps/${runtimeApp}/${native ? '.env.example' : '.dev.vars.example'}`, 'DATABASE_URL=\n');
|
|
23
|
+
if (!native)
|
|
24
|
+
put(root, `apps/${runtimeApp}/${runtimeApp === 'api' ? 'src/' : ''}database-secrets.d.ts`, runtimeApp === 'api'
|
|
25
|
+
? 'interface Env { DATABASE_URL: string }\n'
|
|
26
|
+
: 'declare namespace Cloudflare { interface Env { DATABASE_URL: string } }\n');
|
|
27
|
+
}
|
|
28
|
+
put(root, 'packages/db/src/schema.ts', '// Define product tables here; generate and review SQL before migration.\nexport {};\n');
|
|
29
|
+
put(root, 'packages/db/drizzle.config.ts', has('neon')
|
|
30
|
+
? `import { defineConfig } from 'drizzle-kit';
|
|
31
|
+
export default defineConfig({ dialect: 'postgresql', schema: './src/schema.ts', out: './migrations', dbCredentials: { url: process.env.DATABASE_URL! } });
|
|
32
|
+
`
|
|
33
|
+
: `import { defineConfig } from 'drizzle-kit';
|
|
34
|
+
export default defineConfig({ dialect: 'sqlite', schema: './src/schema.ts', out: './migrations' });
|
|
35
|
+
`);
|
|
36
|
+
patchPackage(root, 'packages/db/package.json', {
|
|
37
|
+
devDependencies: { 'drizzle-kit': STACK_DEPENDENCIES['drizzle-kit'] },
|
|
38
|
+
scripts: { 'db:generate': 'drizzle-kit generate' },
|
|
39
|
+
});
|
|
40
|
+
patchPackage(root, `apps/${runtimeApp}/package.json`, {
|
|
41
|
+
dependencies: { '@workspace/db': 'workspace:*' },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { put } from './files.js';
|
|
4
|
+
export function workerBuildSuffix(options) {
|
|
5
|
+
return options.products.includes('next') && options.hosting === 'cloudflare'
|
|
6
|
+
? ' && pnpm build:worker'
|
|
7
|
+
: '';
|
|
8
|
+
}
|
|
9
|
+
export function writeSetupDocumentation(root, options) {
|
|
10
|
+
const has = (product) => options.products.includes(product);
|
|
11
|
+
const agentsFile = path.join(root, 'AGENTS.md');
|
|
12
|
+
const existingAgents = existsSync(agentsFile) && (has('next') || has('vinext'))
|
|
13
|
+
? readFileSync(agentsFile, 'utf8') + '\n'
|
|
14
|
+
: '';
|
|
15
|
+
put(root, 'AGENTS.md', existingAgents +
|
|
16
|
+
`# Anhedral delivery standard
|
|
17
|
+
|
|
18
|
+
Keep accounts, source, infrastructure, billing, and recovery client-owned. Use macOS, OpenAI/Codex, Computer Use and Control Chrome during implementation and verification. Use provider plugins, APIs/MCP and CLIs for operations; Cloudflare plugin + cf where available, Wrangler for project commands.
|
|
19
|
+
|
|
20
|
+
Vinext on Cloudflare Workers by default; native Next.js on Vercel is an explicit option. Free-to-start where suitable, within provider eligibility and limits. Respect the existing account, budget, audience and release authorization. Request only missing approval for paid activation, material spending, architecture changes or release. Do not deploy or activate providers as part of initialization.
|
|
21
|
+
|
|
22
|
+
Add apps and shared packages only when needed. All services belong in apps/*, reusable code in packages/*. Shared styling is named styling. Keep secrets and privileged integrations out of client bundles. Use Clerk, Better Auth, or no auth according to project needs. Protect product routes explicitly and implement resource authorization.
|
|
23
|
+
|
|
24
|
+
Use private R2 Worker bindings. KV is eventually consistent configuration/cache, never transactional authority. Neon uses Drizzle and direct serverless HTTP with a server-only DATABASE_URL. HTTP supports independent queries and batches; interactive transactions require a reviewed WebSocket or PostgreSQL adapter. Define job retries, concurrency, idempotency and failure handling. Review SQL migrations before applying them.
|
|
25
|
+
|
|
26
|
+
Developer tooling includes Cloudflare’s security-audit skill (https://github.com/cloudflare/security-audit-skill) for repository security reviews.
|
|
27
|
+
|
|
28
|
+
For new projects, guide GitHub sign-in and the user's Git identity. Review files and secret ignores, commit the initial source, then use anhedral repository --create for a private repository under the authenticated user. Preserve existing origins and visibility; use an explicitly selected organization when requested. Connect Vercel with --git and verify actual GitHub installation access. For Git delivery, commit and push reviewed changes, wait for production, then use anhedral adopt and anhedral verify. Adoption checks exact repository, production branch and commit without uploading again. Use anhedral ship only for an explicit CLI delivery. Recover a fresh clone with anhedral recover; rerun product verification because prior test evidence is not recovered. Use anhedral doctor --providers to inspect installed tools and recorded provider context.
|
|
29
|
+
|
|
30
|
+
Maintain docs/architecture.md with selected components, rationale, alternatives, data/access boundaries, and a diagram of actual connections. Maintain docs/verification.md with product acceptance criteria, exercised flows, evidence, and blockers. Generated setup is a starter, not implemented product behavior.
|
|
31
|
+
|
|
32
|
+
Run pnpm check. Use pnpm exec fallow during development and pnpm exec fallow audit for changes. Fix legitimate findings; use narrow documented exceptions. Verify real UI flows with Computer Use and Control Chrome, plus relevant integrations, native apps/simulators, and deployed previews. Carry an authorized build-and-ship request through deployment and live verification; ask only for authorization that is still missing.
|
|
33
|
+
`);
|
|
34
|
+
put(root, 'README.md', `# ${options.name}
|
|
35
|
+
|
|
36
|
+
Initialized from the official shadcn pnpm/Turborepo monorepo, with the Anhedral Application Stack & Delivery Standard.
|
|
37
|
+
|
|
38
|
+
Initial selection: ${options.products.join(', ')}. Current selections and additions are recorded in anhedral.setup.json and anhedral.registry.json. ${has('vinext') ? 'Vinext App Router on Cloudflare Workers.' : has('next') ? `Next.js hosting: ${options.hosting === 'cloudflare' ? 'Cloudflare Workers + OpenNext' : 'Vercel (native Next.js default)'}.` : ''}
|
|
39
|
+
|
|
40
|
+
Run pnpm install, commit pnpm-lock.yaml, then pnpm dev. Run pnpm check for lint, types, Fallow audit, tests and build. Without a Git comparison base, audit runs the full Fallow gate. Add meaningful product tests and integration checks as behavior is implemented. The tests task runs only workspaces that define tests; init does not claim product verification.
|
|
41
|
+
|
|
42
|
+
${has('next') && options.hosting === 'vercel' ? 'Native Next.js uses customer-owned Vercel. Connect with the intended scope and personal/commercial use before shipping. Default to private all-deployment protection unless public access was requested; verify protection before linking Git or uploading code. Run anhedral doctor --providers for prerequisites and recorded provider context. Configure server secrets in that project; use cache components, partial prefetching, Link navigation and Suspense deliberately. ' : ''}${options.hosting === 'cloudflare' || has('hono') ? 'Cloudflare bindings contain REPLACE_WITH_* IDs. Reconcile resources in the client account and generate Worker types before development. Workers expose local dev and explicit deploy scripts. ' : ''}${has('vinext') ? 'Build with pnpm build, then pnpm --dir apps/web preview to exercise the production Worker. Vinext provides a Next-compatible API surface; native Next.js is an explicit framework option. ' : ''}${has('next') && options.hosting === 'cloudflare' ? 'Use the web preview script to exercise the actual OpenNext Worker runtime. ' : ''}Initialization does not provision infrastructure or deploy.
|
|
43
|
+
|
|
44
|
+
Security gate: pnpm audit:deps audits all dependencies. Reviewed backports in scripts/security are accepted only after patch hashes, installed package hashes and regression checks match. Unknown advisories and modified patches remain blocking.
|
|
45
|
+
|
|
46
|
+
${has('next') && options.hosting === 'cloudflare' ? 'OpenNext is configured without a provisioned incremental-cache bucket. Choose and configure a cache adapter before using ISR or cache features that require shared persistence.\n' : ''}
|
|
47
|
+
${has('neon') ? 'Configure server-only DATABASE_URL for the intended Neon branch and role. Direct HTTP needs no Hyperdrive or connection cleanup. Review migrations and authorization; interactive transactions require a reviewed WebSocket or PostgreSQL adapter.\n' : ''}
|
|
48
|
+
${has('better-auth') ? 'Core Better Auth tables are included in the Drizzle schema. Review and apply migrations before using /api/auth. Configure BETTER_AUTH_SECRET, BETTER_AUTH_URL and trusted origins for the correct environment.\n' : ''}
|
|
49
|
+
${has('clerk') ? 'Clerk middleware exposes server identity; protect selected routes explicitly. The web starter includes ClerkProvider and sign-in/sign-up pages. Configure the selected native SDKs when implementing account flows.\n' : ''}
|
|
50
|
+
${has('r2') ? 'FILES is a private R2 binding. Implement authentication and file-level authorization before exposing file routes. Clients never receive R2 credentials.\n' : ''}
|
|
51
|
+
${has('realtime') ? 'Durable Object rooms support hibernating WebSockets; the public entrypoint remains disabled until room authorization and short-lived tickets are implemented.\n' : ''}
|
|
52
|
+
${has('queues') ? 'Queue processing retries unimplemented jobs and uses a dead-letter queue. Add product processing and transactional idempotency before acknowledging messages.\n' : ''}
|
|
53
|
+
${has('basin') ? 'Basin uses a Pipelines stream binding. Configure its stream, sink, R2/catalog and SQL in client-owned infrastructure; it is distinct from operational logs.\n' : ''}
|
|
54
|
+
Provider packages are SDK factories or integration boundaries, not finished business features. Connect only needed packages to server entrypoints. Billing needs verified webhooks and idempotent consumption; RevenueCat adds cross-store entitlement configuration. Local data needs a platform-specific SQLite/filesystem adapter. Sentry needs a platform SDK and runtime initialization. Native auth and subscriptions need platform-specific setup.
|
|
55
|
+
|
|
56
|
+
Purchase the domain in the client's GoDaddy account, then add its zone to the client's Cloudflare account and configure Cloudflare nameservers. The client retains registration, renewal, billing and recovery ownership. Verify DNS records and zone activation before connecting application domains and mail. Inbound mail uses Cloudflare Email Routing. Outbound business and application mail defaults to Cloudflare Email Sending; Resend is an alternative when selected. Configure the selected mail provider before use; init does not provision domains or mail services. GitHub Actions runs PR checks and never deploys automatically. Respect existing authorization; request only missing approval for paid activation, material spending, architecture changes or release.
|
|
57
|
+
`);
|
|
58
|
+
put(root, '.github/workflows/check.yml', `name: Check
|
|
59
|
+
on:
|
|
60
|
+
pull_request:
|
|
61
|
+
push:
|
|
62
|
+
branches: [main]
|
|
63
|
+
permissions:
|
|
64
|
+
contents: read
|
|
65
|
+
jobs:
|
|
66
|
+
check:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
timeout-minutes: 20
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
|
|
71
|
+
with:
|
|
72
|
+
fetch-depth: 0
|
|
73
|
+
persist-credentials: false
|
|
74
|
+
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271
|
|
75
|
+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
|
|
76
|
+
with:
|
|
77
|
+
node-version: '22'
|
|
78
|
+
cache: pnpm
|
|
79
|
+
- run: pnpm install --frozen-lockfile
|
|
80
|
+
- run: pnpm lint && pnpm typecheck && pnpm audit:deps
|
|
81
|
+
- name: Audit pull request
|
|
82
|
+
if: github.event_name == 'pull_request'
|
|
83
|
+
env:
|
|
84
|
+
AUDIT_BASE: \${{ github.event.pull_request.base.sha }}
|
|
85
|
+
run: pnpm exec fallow audit --base "$AUDIT_BASE"
|
|
86
|
+
- name: Audit branch
|
|
87
|
+
if: github.event_name != 'pull_request'
|
|
88
|
+
run: pnpm audit:full
|
|
89
|
+
- run: pnpm test && pnpm build${workerBuildSuffix(options)}
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type Json = Record<string, unknown>;
|
|
2
|
+
export interface PackageManifest extends Json {
|
|
3
|
+
name?: string;
|
|
4
|
+
packageManager?: string;
|
|
5
|
+
scripts?: Record<string, string>;
|
|
6
|
+
dependencies?: Record<string, string>;
|
|
7
|
+
devDependencies?: Record<string, string>;
|
|
8
|
+
pnpm?: {
|
|
9
|
+
overrides?: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface TurboConfig extends Json {
|
|
13
|
+
tasks: Record<string, {
|
|
14
|
+
dependsOn?: string[];
|
|
15
|
+
outputs?: string[];
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export declare function readJson<T extends Json = Json>(root: string, file: string): T;
|
|
20
|
+
export declare function json(root: string, file: string, value: unknown): void;
|
|
21
|
+
export declare function put(root: string, file: string, value: string): void;
|
|
22
|
+
export declare function patchPackage(root: string, file: string, patch: PackageManifest): void;
|
|
23
|
+
export declare function workerConfig(name: string): Json;
|
|
24
|
+
export declare function worker(root: string, app: string, config: Json, source: string, dependencies?: Record<string, string>): void;
|
|
25
|
+
export declare function sharedPackage(root: string, name: string, source: string, dependencies: Record<string, string>): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { STACK_DEPENDENCIES } from '../dependencies.js';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { writeFile } from '../util.js';
|
|
5
|
+
export function readJson(root, file) {
|
|
6
|
+
const value = JSON.parse(readFileSync(path.join(root, file), 'utf8'));
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
|
+
throw new Error(`Expected a JSON object: ${file}.`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function json(root, file, value) {
|
|
12
|
+
writeFile(path.join(root, file), JSON.stringify(value, null, 2) + '\n');
|
|
13
|
+
}
|
|
14
|
+
export function put(root, file, value) {
|
|
15
|
+
writeFile(path.join(root, file), value);
|
|
16
|
+
}
|
|
17
|
+
export function patchPackage(root, file, patch) {
|
|
18
|
+
const current = readJson(root, file);
|
|
19
|
+
const merged = { ...current, ...patch };
|
|
20
|
+
for (const key of ['scripts', 'dependencies', 'devDependencies']) {
|
|
21
|
+
if (patch[key])
|
|
22
|
+
merged[key] = { ...current[key], ...patch[key] };
|
|
23
|
+
}
|
|
24
|
+
json(root, file, merged);
|
|
25
|
+
}
|
|
26
|
+
export function workerConfig(name) {
|
|
27
|
+
return {
|
|
28
|
+
$schema: 'node_modules/wrangler/config-schema.json',
|
|
29
|
+
name,
|
|
30
|
+
main: 'src/index.ts',
|
|
31
|
+
compatibility_date: '2026-10-05',
|
|
32
|
+
compatibility_flags: ['nodejs_compat'],
|
|
33
|
+
observability: { enabled: true },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function worker(root, app, config, source, dependencies = {}) {
|
|
37
|
+
json(root, `apps/${app}/package.json`, {
|
|
38
|
+
name: `@workspace/${app}`,
|
|
39
|
+
private: true,
|
|
40
|
+
type: 'module',
|
|
41
|
+
scripts: {
|
|
42
|
+
dev: 'wrangler dev',
|
|
43
|
+
build: 'wrangler deploy --dry-run --outdir dist',
|
|
44
|
+
deploy: 'wrangler deploy',
|
|
45
|
+
typecheck: 'wrangler types && tsc --noEmit',
|
|
46
|
+
lint: 'eslint . --max-warnings 0',
|
|
47
|
+
},
|
|
48
|
+
dependencies,
|
|
49
|
+
devDependencies: {
|
|
50
|
+
wrangler: STACK_DEPENDENCIES['wrangler'],
|
|
51
|
+
typescript: STACK_DEPENDENCIES['typescript'],
|
|
52
|
+
'@types/node': STACK_DEPENDENCIES['@types/node'],
|
|
53
|
+
eslint: STACK_DEPENDENCIES['eslint'],
|
|
54
|
+
'@workspace/eslint-config': 'workspace:*',
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
json(root, `apps/${app}/wrangler.jsonc`, config);
|
|
58
|
+
json(root, `apps/${app}/tsconfig.json`, {
|
|
59
|
+
compilerOptions: {
|
|
60
|
+
target: 'ES2022',
|
|
61
|
+
lib: ['ES2022'],
|
|
62
|
+
module: 'ESNext',
|
|
63
|
+
moduleResolution: 'Bundler',
|
|
64
|
+
strict: true,
|
|
65
|
+
noUnusedLocals: true,
|
|
66
|
+
noUnusedParameters: true,
|
|
67
|
+
skipLibCheck: true,
|
|
68
|
+
noEmit: true,
|
|
69
|
+
types: ['node'],
|
|
70
|
+
},
|
|
71
|
+
include: ['src/**/*.ts', 'worker-configuration.d.ts'],
|
|
72
|
+
});
|
|
73
|
+
put(root, `apps/${app}/eslint.config.mjs`, 'import { config } from "@workspace/eslint-config/base";\nexport default [...config, { ignores: ["dist/**", ".wrangler/**", "worker-configuration.d.ts"] }, { files: ["**/*.ts"], rules: { "no-undef": "off", "no-unused-vars": "off" } }];\n');
|
|
74
|
+
put(root, `apps/${app}/src/index.ts`, source);
|
|
75
|
+
}
|
|
76
|
+
export function sharedPackage(root, name, source, dependencies) {
|
|
77
|
+
json(root, `packages/${name}/package.json`, {
|
|
78
|
+
name: `@workspace/${name}`,
|
|
79
|
+
private: true,
|
|
80
|
+
type: 'module',
|
|
81
|
+
exports: { '.': './src/index.ts' },
|
|
82
|
+
scripts: { typecheck: 'tsc --noEmit', lint: 'eslint . --max-warnings 0' },
|
|
83
|
+
dependencies,
|
|
84
|
+
devDependencies: {
|
|
85
|
+
typescript: STACK_DEPENDENCIES['typescript'],
|
|
86
|
+
'@types/node': STACK_DEPENDENCIES['@types/node'],
|
|
87
|
+
eslint: STACK_DEPENDENCIES['eslint'],
|
|
88
|
+
'@workspace/eslint-config': 'workspace:*',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
json(root, `packages/${name}/tsconfig.json`, {
|
|
92
|
+
compilerOptions: {
|
|
93
|
+
target: 'ES2022',
|
|
94
|
+
lib: ['ES2022', 'DOM'],
|
|
95
|
+
module: 'ESNext',
|
|
96
|
+
moduleResolution: 'Bundler',
|
|
97
|
+
strict: true,
|
|
98
|
+
noUnusedLocals: true,
|
|
99
|
+
noUnusedParameters: true,
|
|
100
|
+
skipLibCheck: true,
|
|
101
|
+
noEmit: true,
|
|
102
|
+
types: ['node'],
|
|
103
|
+
},
|
|
104
|
+
include: ['src/**/*.ts'],
|
|
105
|
+
});
|
|
106
|
+
put(root, `packages/${name}/eslint.config.mjs`, 'import { config } from "@workspace/eslint-config/base";\nexport default [...config, { ignores: ["dist/**", ".wrangler/**", "worker-configuration.d.ts"] }, { files: ["**/*.ts"], rules: { "no-undef": "off", "no-unused-vars": "off" } }];\n');
|
|
107
|
+
put(root, `packages/${name}/src/index.ts`, source);
|
|
108
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { put } from './files.js';
|
|
2
|
+
export function scaffoldPrivateFiles({ root, runtimeApp }) {
|
|
3
|
+
put(root, `apps/${runtimeApp}/private-files.md`, '# Private files\n\nFILES is a private R2 Worker binding. Authenticate the request and enforce file-level product authorization before calling get/put/delete. No public buckets, R2 keys, generic download endpoints, or client credentials are generated. Use the binding directly in Hono or import env from cloudflare:workers in Vinext; explicit OpenNext uses getCloudflareContext.\n');
|
|
4
|
+
}
|
|
5
|
+
export function scaffoldRevenueCat({ root }) {
|
|
6
|
+
put(root, 'packages/billing/revenuecat.md', '# RevenueCat + Stripe\n\nConfigure web/App Store/Play Store products and offerings in client-owned accounts. Add the platform SDK to each selected app. Verify signed provider webhooks, reconcile entitlement authority, and keep transactional idempotent credits/quotas separate from subscription status. No payment activation occurs during init.\n');
|
|
7
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RecipeContext } from '../registry/types.js';
|
|
2
|
+
export declare function scaffoldRealtime({ root, options }: RecipeContext): void;
|
|
3
|
+
export declare function scaffoldQueues({ root, options }: RecipeContext): void;
|
|
4
|
+
export declare function scaffoldCron({ root, options }: RecipeContext): void;
|
|
5
|
+
export declare function scaffoldWorkflows({ root, options }: RecipeContext): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { workerConfig, worker } from './files.js';
|
|
2
|
+
export function scaffoldRealtime({ root, options }) {
|
|
3
|
+
worker(root, 'realtime', {
|
|
4
|
+
...workerConfig(`${options.name}-realtime`),
|
|
5
|
+
durable_objects: { bindings: [{ name: 'ROOMS', class_name: 'Room' }] },
|
|
6
|
+
migrations: [{ tag: 'v1', new_sqlite_classes: ['Room'] }],
|
|
7
|
+
}, `import { DurableObject } from 'cloudflare:workers';
|
|
8
|
+
export class Room extends DurableObject<Env> {
|
|
9
|
+
async fetch(request: Request): Promise<Response> {
|
|
10
|
+
if (request.headers.get('Upgrade') !== 'websocket') return new Response('WebSocket required', { status: 426 });
|
|
11
|
+
const pair = new WebSocketPair();
|
|
12
|
+
this.ctx.acceptWebSocket(pair[1]);
|
|
13
|
+
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
14
|
+
}
|
|
15
|
+
webSocketMessage(sender: WebSocket, message: string | ArrayBuffer) {
|
|
16
|
+
if ((typeof message === 'string' ? new TextEncoder().encode(message).byteLength : message.byteLength) > 16384) { sender.close(1009, 'Message too large'); return; }
|
|
17
|
+
this.ctx.getWebSockets().filter((socket) => socket !== sender).forEach((socket) => socket.send(message));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export default {
|
|
21
|
+
async fetch(): Promise<Response> {
|
|
22
|
+
// Authenticate and authorize room membership, then forward to env.ROOMS.getByName(room).fetch(request).
|
|
23
|
+
// Use short-lived single-use tickets for browser WebSockets. Never expose a long-lived service secret.
|
|
24
|
+
return new Response('Configure room authorization before enabling connections', { status: 501 });
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
export function scaffoldQueues({ root, options }) {
|
|
30
|
+
worker(root, 'jobs', {
|
|
31
|
+
...workerConfig(`${options.name}-jobs`),
|
|
32
|
+
queues: {
|
|
33
|
+
producers: [{ binding: 'JOBS', queue: `${options.name}-jobs` }],
|
|
34
|
+
consumers: [
|
|
35
|
+
{
|
|
36
|
+
queue: `${options.name}-jobs`,
|
|
37
|
+
max_batch_size: 10,
|
|
38
|
+
max_retries: 3,
|
|
39
|
+
dead_letter_queue: `${options.name}-jobs-dlq`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
}, `export default {
|
|
44
|
+
async queue(batch: MessageBatch<unknown>): Promise<void> {
|
|
45
|
+
for (const message of batch.messages) {
|
|
46
|
+
// Add transactional idempotency and product processing before acknowledging.
|
|
47
|
+
console.error('Job handler requires product implementation', message.id);
|
|
48
|
+
message.retry({ delaySeconds: 60 });
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
export function scaffoldCron({ root, options }) {
|
|
55
|
+
worker(root, 'scheduled', { ...workerConfig(`${options.name}-scheduled`), triggers: { crons: ['0 * * * *'] } }, `export default {
|
|
56
|
+
async scheduled(controller: ScheduledController): Promise<void> {
|
|
57
|
+
// Define a product schedule, idempotency key, overlap policy, and failure alerts before release.
|
|
58
|
+
console.log('Scheduled trigger', controller.scheduledTime);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
export function scaffoldWorkflows({ root, options }) {
|
|
64
|
+
worker(root, 'workflows', {
|
|
65
|
+
...workerConfig(`${options.name}-workflows`),
|
|
66
|
+
workflows: [
|
|
67
|
+
{
|
|
68
|
+
name: `${options.name}-workflow`,
|
|
69
|
+
binding: 'WORKFLOW',
|
|
70
|
+
class_name: 'ApplicationWorkflow',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}, `import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';
|
|
74
|
+
export class ApplicationWorkflow extends WorkflowEntrypoint<Env, { operationId: string }> {
|
|
75
|
+
async run(event: WorkflowEvent<{ operationId: string }>, step: WorkflowStep) {
|
|
76
|
+
return step.do('validate', { retries: { limit: 3, delay: '10 seconds', backoff: 'exponential' } }, async () => {
|
|
77
|
+
if (!event.payload.operationId) throw new Error('operationId required');
|
|
78
|
+
return { operationId: event.payload.operationId };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export default { async fetch() { return new Response('Use an authorized server binding to create workflows', { status: 403 }); } };
|
|
83
|
+
`);
|
|
84
|
+
}
|