anhedral 0.10.0 → 0.11.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 +78 -39
- package/assets/{anhedral-cli-init-technical.svg → anhedral-stack-technical.svg} +6 -6
- package/assets/{anhedral-cli-init.svg → anhedral-stack.svg} +7 -7
- package/assets/security/audit-deps.mjs +6 -1
- package/dist/bin.js +271 -154
- package/dist/bounded-file.d.ts +1 -0
- package/dist/bounded-file.js +1 -0
- package/dist/capabilities.d.ts +4 -4
- package/dist/capabilities.js +1 -1
- package/dist/capability-contract.d.ts +47 -0
- package/dist/capability-contract.js +136 -0
- package/dist/cli-arguments.d.ts +16 -0
- package/dist/cli-arguments.js +115 -0
- package/dist/cli-output.d.ts +8 -0
- package/dist/cli-output.js +211 -0
- package/dist/cloudflare.d.ts +7 -2
- package/dist/cloudflare.js +166 -36
- package/dist/command-schema.d.ts +21 -0
- package/dist/command-schema.js +374 -0
- package/dist/delivery.d.ts +2 -2
- package/dist/delivery.js +21 -4
- package/dist/dependencies.d.ts +32 -0
- package/dist/dependencies.js +47 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +15 -1
- package/dist/inspector-model.d.ts +115 -0
- package/dist/inspector-model.js +1 -0
- package/dist/lifecycle-model.d.ts +15 -0
- package/dist/lifecycle-model.js +20 -0
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.js +92 -0
- package/dist/mobile-delivery.d.ts +100 -0
- package/dist/mobile-delivery.js +319 -0
- package/dist/platforms/desktop.js +4 -5
- package/dist/platforms/extension.js +9 -73
- package/dist/platforms/mobile.js +14 -52
- package/dist/project-inspector.d.ts +4 -0
- package/dist/project-inspector.js +646 -0
- package/dist/project-state.d.ts +17 -0
- package/dist/project-state.js +239 -11
- package/dist/provider-auth.d.ts +41 -0
- package/dist/provider-auth.js +210 -0
- package/dist/providers/adapters.d.ts +17 -0
- package/dist/providers/adapters.js +269 -0
- package/dist/providers/api.d.ts +28 -0
- package/dist/providers/api.js +252 -0
- package/dist/providers/config.d.ts +15 -0
- package/dist/providers/config.js +184 -0
- package/dist/providers/definitions.d.ts +318 -0
- package/dist/providers/definitions.js +127 -0
- package/dist/providers/provision.d.ts +24 -0
- package/dist/providers/provision.js +76 -0
- package/dist/providers/receipts.d.ts +20 -0
- package/dist/providers/receipts.js +66 -0
- package/dist/providers/recovery-verification.d.ts +7 -0
- package/dist/providers/recovery-verification.js +112 -0
- package/dist/providers/recovery.d.ts +30 -0
- package/dist/providers/recovery.js +34 -0
- package/dist/providers/runtime.d.ts +10 -0
- package/dist/providers/runtime.js +231 -0
- package/dist/providers/schema.d.ts +28 -0
- package/dist/providers/schema.js +49 -0
- package/dist/providers/state.d.ts +56 -0
- package/dist/providers/state.js +329 -0
- package/dist/provision.d.ts +1 -1
- package/dist/provision.js +61 -3
- package/dist/readiness.js +11 -3
- package/dist/registry/add.d.ts +6 -0
- package/dist/registry/add.js +35 -4
- package/dist/registry/commands.d.ts +3 -2
- package/dist/registry/commands.js +53 -14
- package/dist/registry/options.js +3 -2
- package/dist/registry/preview.d.ts +11 -0
- package/dist/registry/preview.js +91 -0
- package/dist/registry/receipt.js +1 -0
- package/dist/registry/resolve.js +19 -13
- package/dist/registry/types.d.ts +1 -0
- package/dist/registry/usage.d.ts +10 -0
- package/dist/registry/usage.js +22 -37
- package/dist/render.d.ts +2 -0
- package/dist/render.js +10 -0
- package/dist/scaffold/api.js +20 -3
- package/dist/scaffold/auth.js +68 -28
- package/dist/scaffold/bootstrap.d.ts +2 -0
- package/dist/scaffold/bootstrap.js +9 -0
- package/dist/scaffold/database.js +12 -6
- package/dist/scaffold/documentation.js +11 -8
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +18 -1
- package/dist/scaffold/files.d.ts +1 -0
- package/dist/scaffold/files.js +15 -4
- package/dist/scaffold/services.js +37 -13
- package/dist/scaffold/stack-documentation.d.ts +3 -0
- package/dist/scaffold/stack-documentation.js +52 -0
- package/dist/scaffold/styling.d.ts +3 -0
- package/dist/scaffold/styling.js +84 -0
- package/dist/scaffold/web.js +31 -11
- package/dist/scaffold/workspace.js +49 -22
- package/dist/ship.d.ts +320 -0
- package/dist/ship.js +211 -0
- package/dist/stack-registry.d.ts +2 -2
- package/dist/stack-registry.js +44 -11
- package/dist/standard.d.ts +2 -1
- package/dist/standard.js +13 -12
- package/dist/vercel.d.ts +3 -3
- package/dist/vercel.js +27 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.js +9 -18
- package/dist/workspace-graph.js +3 -2
- package/docs/application-stack-standard.md +51 -80
- package/docs/cli-design.md +67 -0
- package/docs/delivery-workflow.md +29 -15
- package/docs/plugin-ui.md +61 -0
- package/docs/provider-configuration.md +117 -0
- package/package.json +16 -12
- package/dist/managed-hosting.d.ts +0 -49
- package/dist/managed-hosting.js +0 -373
- package/docs/managed-hosting-architecture.md +0 -87
- package/docs/sponsorships.md +0 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anhedral",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Build and deliver applications with Cloudflare-first Vinext, selected services, optional Next.js on Vercel, and verified release evidence.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"assets/anhedral-
|
|
20
|
-
"assets/anhedral-
|
|
19
|
+
"assets/anhedral-stack.svg",
|
|
20
|
+
"assets/anhedral-stack-technical.svg",
|
|
21
21
|
"assets/anhedral-mark.svg",
|
|
22
22
|
"LICENSE",
|
|
23
23
|
"README.md",
|
|
@@ -25,17 +25,18 @@
|
|
|
25
25
|
"assets/favicon.ico",
|
|
26
26
|
"docs/application-stack-standard.md",
|
|
27
27
|
"docs/delivery-workflow.md",
|
|
28
|
-
"docs/
|
|
29
|
-
"docs/
|
|
30
|
-
"assets/security"
|
|
28
|
+
"docs/provider-configuration.md",
|
|
29
|
+
"docs/cli-design.md",
|
|
30
|
+
"assets/security",
|
|
31
|
+
"docs/plugin-ui.md"
|
|
31
32
|
],
|
|
32
|
-
"homepage": "https://github.com/anhedral/anhedral-
|
|
33
|
+
"homepage": "https://github.com/anhedral/anhedral-cli#readme",
|
|
33
34
|
"repository": {
|
|
34
35
|
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/anhedral/anhedral-
|
|
36
|
+
"url": "git+https://github.com/anhedral/anhedral-cli.git"
|
|
36
37
|
},
|
|
37
38
|
"bugs": {
|
|
38
|
-
"url": "https://github.com/anhedral/anhedral-
|
|
39
|
+
"url": "https://github.com/anhedral/anhedral-cli/issues"
|
|
39
40
|
},
|
|
40
41
|
"author": "Anhedral, Inc.",
|
|
41
42
|
"publishConfig": {
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"test:release-tooling": "node tests/test-release-tooling.test.mjs",
|
|
67
68
|
"test:all": "pnpm build && pnpm lint && pnpm check:security && pnpm audit:osv && pnpm test:unit && node tests/test-packed-cli.js && node tests/test-packlist.js",
|
|
68
69
|
"release:check": "pnpm test:all",
|
|
69
|
-
"release:pack": "pnpm build && node scripts/create-release-artifact.mjs && node scripts/check-release-secrets.mjs .artifacts/release/metadata.json",
|
|
70
|
+
"release:pack": "pnpm build && pnpm plugin:build && node scripts/create-release-artifact.mjs && node scripts/check-release-secrets.mjs .artifacts/release/metadata.json",
|
|
70
71
|
"release:artifact:verify": "node scripts/verify-release-artifact.mjs .artifacts/release/metadata.json && node scripts/check-release-secrets.mjs .artifacts/release/metadata.json",
|
|
71
72
|
"release:artifact:smoke": "node tests/test-packed-cli.js .artifacts/release/metadata.json",
|
|
72
73
|
"docs:stack-map": "node scripts/render-master-stack-map.mjs",
|
|
@@ -78,8 +79,11 @@
|
|
|
78
79
|
"test:live:delivery": "pnpm build && node tests/test-live-delivery.mjs",
|
|
79
80
|
"test:live:vercel": "pnpm build && node tests/test-live-vercel.mjs",
|
|
80
81
|
"test:live:vercel:git": "pnpm build && node tests/test-live-vercel-git.mjs",
|
|
81
|
-
"format": "prettier --write src scripts tests",
|
|
82
|
-
"format:check": "prettier --check src scripts tests"
|
|
82
|
+
"format": "prettier --write src scripts tests plugin-app",
|
|
83
|
+
"format:check": "prettier --check src scripts tests plugin-app",
|
|
84
|
+
"plugin:build": "pnpm --dir plugin-app build",
|
|
85
|
+
"plugin:check": "pnpm --dir plugin-app check",
|
|
86
|
+
"plugin:preview": "pnpm --dir plugin-app start"
|
|
83
87
|
},
|
|
84
88
|
"keywords": [
|
|
85
89
|
"cli",
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export interface ManagedProject {
|
|
2
|
-
schemaVersion: 1;
|
|
3
|
-
id: string;
|
|
4
|
-
name: string;
|
|
5
|
-
provider: 'anhedral';
|
|
6
|
-
ownerId: string | null;
|
|
7
|
-
projectId: string | null;
|
|
8
|
-
audience: 'private' | 'public';
|
|
9
|
-
hostingOrigin?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface ManagedVersion {
|
|
12
|
-
schemaVersion: 1;
|
|
13
|
-
projectId: string;
|
|
14
|
-
commit: string;
|
|
15
|
-
sourceSha256: string;
|
|
16
|
-
artifactSha256: string;
|
|
17
|
-
sourceBytes: number;
|
|
18
|
-
artifactBytes: number;
|
|
19
|
-
stageId: string;
|
|
20
|
-
state: 'staged' | 'saved';
|
|
21
|
-
versionId?: string;
|
|
22
|
-
}
|
|
23
|
-
export type ManagedFetch = typeof fetch;
|
|
24
|
-
export declare function registerManagedApplication(root: string, audience?: 'private' | 'public'): ManagedProject;
|
|
25
|
-
export declare function stageManagedVersion(root: string, artifactDirectory: string): Promise<ManagedVersion>;
|
|
26
|
-
export declare function saveManagedVersion(root: string, stageId: string, send?: ManagedFetch): Promise<ManagedVersion>;
|
|
27
|
-
export declare function deployManagedVersion(root: string, stageId: string, send?: ManagedFetch): Promise<{
|
|
28
|
-
productVerification: string;
|
|
29
|
-
url?: unknown;
|
|
30
|
-
projectId: string;
|
|
31
|
-
ownerId: string;
|
|
32
|
-
audience: "public" | "private";
|
|
33
|
-
deploymentId: string;
|
|
34
|
-
versionId: string;
|
|
35
|
-
commit: string;
|
|
36
|
-
artifactSha256: string;
|
|
37
|
-
state: unknown;
|
|
38
|
-
}>;
|
|
39
|
-
export declare function managedApplicationStatus(root: string, send?: ManagedFetch): Promise<{
|
|
40
|
-
project: ManagedProject;
|
|
41
|
-
state: string;
|
|
42
|
-
productVerification: string;
|
|
43
|
-
evidence?: undefined;
|
|
44
|
-
} | {
|
|
45
|
-
project: ManagedProject;
|
|
46
|
-
evidence: string;
|
|
47
|
-
state: unknown;
|
|
48
|
-
productVerification: string;
|
|
49
|
-
}>;
|
package/dist/managed-hosting.js
DELETED
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { atomicJson, withApplicationLock } from './project-state.js';
|
|
6
|
-
const manifest = 'anhedral.hosting.json';
|
|
7
|
-
const identifier = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
|
|
8
|
-
const maxBytes = 64 * 1024 * 1024;
|
|
9
|
-
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
-
function origin() {
|
|
11
|
-
const raw = process.env.ANHEDRAL_HOSTING_ORIGIN;
|
|
12
|
-
if (!raw)
|
|
13
|
-
throw new Error('Managed hosting backend is not configured. Set ANHEDRAL_HOSTING_ORIGIN to an operated Anhedral service; no hosting service is implied by the CLI.');
|
|
14
|
-
const url = new URL(raw);
|
|
15
|
-
if (url.protocol !== 'https:' ||
|
|
16
|
-
url.username ||
|
|
17
|
-
url.password ||
|
|
18
|
-
url.port ||
|
|
19
|
-
url.search ||
|
|
20
|
-
url.hash ||
|
|
21
|
-
url.pathname !== '/' ||
|
|
22
|
-
['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
23
|
-
throw new Error('Managed hosting requires an HTTPS origin without credentials, path, query or port.');
|
|
24
|
-
return url.origin;
|
|
25
|
-
}
|
|
26
|
-
function readProject(root) {
|
|
27
|
-
const project = JSON.parse(readFileSync(path.join(root, manifest), 'utf8'));
|
|
28
|
-
if (project.schemaVersion !== 1 ||
|
|
29
|
-
project.provider !== 'anhedral' ||
|
|
30
|
-
!identifier.test(project.id) ||
|
|
31
|
-
typeof project.name !== 'string' ||
|
|
32
|
-
!project.name.trim() ||
|
|
33
|
-
!['private', 'public'].includes(project.audience) ||
|
|
34
|
-
!(project.ownerId === null || identifier.test(project.ownerId)) ||
|
|
35
|
-
!(project.projectId === null || identifier.test(project.projectId)))
|
|
36
|
-
throw new Error('Invalid managed hosting manifest.');
|
|
37
|
-
return project;
|
|
38
|
-
}
|
|
39
|
-
export function registerManagedApplication(root, audience = 'private') {
|
|
40
|
-
root = realpathSync(root);
|
|
41
|
-
if (existsSync(path.join(root, manifest)))
|
|
42
|
-
return readProject(root);
|
|
43
|
-
if (!['private', 'public'].includes(audience))
|
|
44
|
-
throw new Error('Select private or public hosting.');
|
|
45
|
-
const name = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name;
|
|
46
|
-
if (typeof name !== 'string' || !name.trim())
|
|
47
|
-
throw new Error('Application requires a package name.');
|
|
48
|
-
const project = {
|
|
49
|
-
schemaVersion: 1,
|
|
50
|
-
id: randomUUID(),
|
|
51
|
-
name,
|
|
52
|
-
provider: 'anhedral',
|
|
53
|
-
ownerId: null,
|
|
54
|
-
projectId: null,
|
|
55
|
-
audience,
|
|
56
|
-
};
|
|
57
|
-
const ignore = path.join(root, '.gitignore');
|
|
58
|
-
const previous = existsSync(ignore) ? readFileSync(ignore, 'utf8') : '';
|
|
59
|
-
if (!previous.split(/\r?\n/).includes('.anhedral/'))
|
|
60
|
-
writeFileSync(ignore, previous + (previous && !previous.endsWith('\n') ? '\n' : '') + '.anhedral/\n');
|
|
61
|
-
atomicJson(path.join(root, manifest), project);
|
|
62
|
-
return project;
|
|
63
|
-
}
|
|
64
|
-
function git(root, args) {
|
|
65
|
-
return execFileSync('git', args, {
|
|
66
|
-
cwd: root,
|
|
67
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
68
|
-
maxBuffer: maxBytes,
|
|
69
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
function source(root) {
|
|
73
|
-
if (realpathSync(git(root, ['rev-parse', '--show-toplevel']).toString().trim()) !== root ||
|
|
74
|
-
git(root, ['status', '--porcelain']).toString().trim())
|
|
75
|
-
throw new Error('Managed versions require a clean application repository.');
|
|
76
|
-
const commit = git(root, ['rev-parse', 'HEAD']).toString().trim();
|
|
77
|
-
if (!/^[a-f0-9]{40,64}$/.test(commit))
|
|
78
|
-
throw new Error('Invalid source commit.');
|
|
79
|
-
return commit;
|
|
80
|
-
}
|
|
81
|
-
function files(root, directory) {
|
|
82
|
-
const entries = [];
|
|
83
|
-
let size = 0;
|
|
84
|
-
const visit = (current) => {
|
|
85
|
-
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
86
|
-
const file = path.join(current, entry.name);
|
|
87
|
-
const stat = lstatSync(file);
|
|
88
|
-
if (stat.isSymbolicLink() || (!stat.isFile() && !stat.isDirectory()))
|
|
89
|
-
throw new Error('Build artifacts must contain only regular files and directories.');
|
|
90
|
-
if (entry.name === '.git' || entry.name.startsWith('.env'))
|
|
91
|
-
throw new Error('Build artifacts must not contain source credentials, Git state or dependencies.');
|
|
92
|
-
if (stat.isDirectory())
|
|
93
|
-
visit(file);
|
|
94
|
-
else {
|
|
95
|
-
size += stat.size;
|
|
96
|
-
if (size > maxBytes || entries.length >= 10000)
|
|
97
|
-
throw new Error('Managed artifact exceeds staging limits.');
|
|
98
|
-
entries.push(`${path.relative(root, file)}:${digest(readFileSync(file))}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
visit(directory);
|
|
103
|
-
if (!entries.length)
|
|
104
|
-
throw new Error('Build artifact directory is empty.');
|
|
105
|
-
return digest(Buffer.from(entries.sort().join('\n')));
|
|
106
|
-
}
|
|
107
|
-
export async function stageManagedVersion(root, artifactDirectory) {
|
|
108
|
-
root = realpathSync(root);
|
|
109
|
-
return withApplicationLock(root, async () => {
|
|
110
|
-
const project = readProject(root);
|
|
111
|
-
const commit = source(root);
|
|
112
|
-
const artifact = realpathSync(path.resolve(root, artifactDirectory));
|
|
113
|
-
if (!artifact.startsWith(root + path.sep) ||
|
|
114
|
-
!lstatSync(artifact).isDirectory() ||
|
|
115
|
-
artifact.startsWith(path.join(root, '.anhedral') + path.sep))
|
|
116
|
-
throw new Error('Select an application build directory within the repository.');
|
|
117
|
-
const fingerprint = files(root, artifact);
|
|
118
|
-
for (const file of git(root, ['ls-files', '-z']).toString().split('\0').filter(Boolean)) {
|
|
119
|
-
if (path.basename(file).startsWith('.env') && path.basename(file) !== '.env.example')
|
|
120
|
-
throw new Error('Tracked environment credentials cannot be uploaded as source.');
|
|
121
|
-
if (lstatSync(path.join(root, file)).isSymbolicLink() ||
|
|
122
|
-
!lstatSync(path.join(root, file)).isFile())
|
|
123
|
-
throw new Error('Managed source archives do not support symlinks or submodules.');
|
|
124
|
-
}
|
|
125
|
-
if (!git(root, ['show', `HEAD:${manifest}`]).equals(readFileSync(path.join(root, manifest))))
|
|
126
|
-
throw new Error('Commit the exact managed manifest before staging.');
|
|
127
|
-
const archive = git(root, ['archive', '--format=tar', commit]);
|
|
128
|
-
const staging = path.join(root, '.anhedral', 'managed');
|
|
129
|
-
mkdirSync(staging, { recursive: true, mode: 0o700 });
|
|
130
|
-
if (realpathSync(staging) !== staging)
|
|
131
|
-
throw new Error('Managed staging must not be a symbolic link.');
|
|
132
|
-
const stageId = randomUUID();
|
|
133
|
-
const directory = path.join(staging, stageId);
|
|
134
|
-
mkdirSync(directory, { mode: 0o700 });
|
|
135
|
-
try {
|
|
136
|
-
writeFileSync(path.join(directory, 'source.tar'), archive, { mode: 0o600 });
|
|
137
|
-
execFileSync('tar', ['-cf', path.join(directory, 'artifact.tar'), '-C', artifact, '.'], {
|
|
138
|
-
stdio: 'ignore',
|
|
139
|
-
});
|
|
140
|
-
const build = readFileSync(path.join(directory, 'artifact.tar'));
|
|
141
|
-
if (build.length > maxBytes || archive.length > maxBytes)
|
|
142
|
-
throw new Error('Managed archives exceed staging limits.');
|
|
143
|
-
if (files(root, artifact) !== fingerprint || source(root) !== commit)
|
|
144
|
-
throw new Error('Source or build artifact changed during staging.');
|
|
145
|
-
const version = {
|
|
146
|
-
schemaVersion: 1,
|
|
147
|
-
projectId: project.id,
|
|
148
|
-
commit,
|
|
149
|
-
sourceSha256: digest(archive),
|
|
150
|
-
artifactSha256: digest(build),
|
|
151
|
-
sourceBytes: archive.length,
|
|
152
|
-
artifactBytes: build.length,
|
|
153
|
-
stageId,
|
|
154
|
-
state: 'staged',
|
|
155
|
-
};
|
|
156
|
-
atomicJson(path.join(directory, 'version.json'), version);
|
|
157
|
-
return version;
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
rmSync(directory, { recursive: true, force: true });
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
async function request(send, method, route, key, body) {
|
|
166
|
-
const token = process.env.ANHEDRAL_HOSTING_TOKEN;
|
|
167
|
-
if (!token || /[\r\n]/.test(token))
|
|
168
|
-
throw new Error('Sign in to the configured managed hosting service; ANHEDRAL_HOSTING_TOKEN is required and never stored.');
|
|
169
|
-
const endpoint = `${origin()}/v1${route}`;
|
|
170
|
-
let response;
|
|
171
|
-
try {
|
|
172
|
-
response = await send(endpoint, {
|
|
173
|
-
method,
|
|
174
|
-
redirect: 'error',
|
|
175
|
-
signal: AbortSignal.timeout(30000),
|
|
176
|
-
headers: {
|
|
177
|
-
Authorization: `Bearer ${token}`,
|
|
178
|
-
...(key ? { 'Idempotency-Key': key } : {}),
|
|
179
|
-
'Content-Type': Buffer.isBuffer(body) ? 'application/x-tar' : 'application/json',
|
|
180
|
-
},
|
|
181
|
-
body: body === undefined
|
|
182
|
-
? undefined
|
|
183
|
-
: Buffer.isBuffer(body)
|
|
184
|
-
? new Uint8Array(body)
|
|
185
|
-
: JSON.stringify(body),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
throw new Error('Managed hosting request failed; inspect connectivity and authentication.');
|
|
190
|
-
}
|
|
191
|
-
let value;
|
|
192
|
-
try {
|
|
193
|
-
value = (await response.json());
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
throw new Error('Managed hosting returned unsupported output.');
|
|
197
|
-
}
|
|
198
|
-
if (!response.ok || !value || typeof value !== 'object' || Array.isArray(value))
|
|
199
|
-
throw new Error(`Managed hosting operation failed (HTTP ${response.status}); no deployment success was recorded.`);
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
202
|
-
async function context(root, send) {
|
|
203
|
-
const project = readProject(root);
|
|
204
|
-
if (project.hostingOrigin && project.hostingOrigin !== origin())
|
|
205
|
-
throw new Error('Managed hosting origin differs from the recorded service.');
|
|
206
|
-
const session = await request(send, 'GET', '/session');
|
|
207
|
-
if (typeof session.ownerId !== 'string' ||
|
|
208
|
-
!identifier.test(session.ownerId) ||
|
|
209
|
-
(project.ownerId && session.ownerId !== project.ownerId))
|
|
210
|
-
throw new Error('Managed hosting session differs from the application owner.');
|
|
211
|
-
return { project, ownerId: session.ownerId };
|
|
212
|
-
}
|
|
213
|
-
function identity(value, projectId, ownerId, audience) {
|
|
214
|
-
if (value.projectId !== projectId || value.ownerId !== ownerId || value.audience !== audience)
|
|
215
|
-
throw new Error('Managed hosting receipt differs from project ownership or audience.');
|
|
216
|
-
}
|
|
217
|
-
function staged(root, stageId, project) {
|
|
218
|
-
if (!identifier.test(stageId))
|
|
219
|
-
throw new Error('Invalid stage identity.');
|
|
220
|
-
const directory = path.join(root, '.anhedral', 'managed', stageId);
|
|
221
|
-
if (realpathSync(directory) !== directory)
|
|
222
|
-
throw new Error('Managed staging directory must not be a symbolic link.');
|
|
223
|
-
if (!lstatSync(path.join(directory, 'version.json')).isFile() ||
|
|
224
|
-
lstatSync(path.join(directory, 'version.json')).isSymbolicLink())
|
|
225
|
-
throw new Error('Invalid staged metadata file.');
|
|
226
|
-
const version = JSON.parse(readFileSync(path.join(directory, 'version.json'), 'utf8'));
|
|
227
|
-
if (version.schemaVersion !== 1 ||
|
|
228
|
-
version.projectId !== project.id ||
|
|
229
|
-
version.stageId !== stageId ||
|
|
230
|
-
!/^[a-f0-9]{40,64}$/.test(version.commit))
|
|
231
|
-
throw new Error('Managed stage belongs to another project.');
|
|
232
|
-
for (const [file, expected, bytes] of [
|
|
233
|
-
['source.tar', version.sourceSha256, version.sourceBytes],
|
|
234
|
-
['artifact.tar', version.artifactSha256, version.artifactBytes],
|
|
235
|
-
]) {
|
|
236
|
-
if (!lstatSync(path.join(directory, file)).isFile() ||
|
|
237
|
-
lstatSync(path.join(directory, file)).isSymbolicLink())
|
|
238
|
-
throw new Error('Invalid staged archive file.');
|
|
239
|
-
const archive = readFileSync(path.join(directory, file));
|
|
240
|
-
if (archive.length !== bytes ||
|
|
241
|
-
archive.length > maxBytes ||
|
|
242
|
-
digest(archive) !== expected ||
|
|
243
|
-
lstatSync(path.join(directory, file)).isSymbolicLink())
|
|
244
|
-
throw new Error('Managed staged archive changed.');
|
|
245
|
-
}
|
|
246
|
-
return { version, directory };
|
|
247
|
-
}
|
|
248
|
-
export async function saveManagedVersion(root, stageId, send = fetch) {
|
|
249
|
-
root = realpathSync(root);
|
|
250
|
-
return withApplicationLock(root, async () => {
|
|
251
|
-
const { project, ownerId } = await context(root, send);
|
|
252
|
-
const { version, directory } = staged(root, stageId, project);
|
|
253
|
-
const key = digest(Buffer.from(`${project.id}:${version.commit}:${version.sourceSha256}:${version.artifactSha256}`));
|
|
254
|
-
const created = await request(send, 'POST', '/projects', project.id, {
|
|
255
|
-
clientProjectId: project.id,
|
|
256
|
-
name: project.name,
|
|
257
|
-
audience: project.audience,
|
|
258
|
-
});
|
|
259
|
-
if (typeof created.projectId !== 'string' ||
|
|
260
|
-
!identifier.test(created.projectId) ||
|
|
261
|
-
(project.projectId && created.projectId !== project.projectId))
|
|
262
|
-
throw new Error('Managed project identity changed.');
|
|
263
|
-
identity(created, created.projectId, ownerId, project.audience);
|
|
264
|
-
if (created.clientProjectId !== project.id)
|
|
265
|
-
throw new Error('Backend project differs from the registered client identity.');
|
|
266
|
-
const connected = {
|
|
267
|
-
...project,
|
|
268
|
-
projectId: created.projectId,
|
|
269
|
-
ownerId,
|
|
270
|
-
hostingOrigin: origin(),
|
|
271
|
-
};
|
|
272
|
-
atomicJson(path.join(root, manifest), connected);
|
|
273
|
-
const base = `/projects/${created.projectId}`;
|
|
274
|
-
const receipt = await request(send, 'POST', `${base}/versions`, key, {
|
|
275
|
-
commit: version.commit,
|
|
276
|
-
sourceSha256: version.sourceSha256,
|
|
277
|
-
artifactSha256: version.artifactSha256,
|
|
278
|
-
sourceBytes: version.sourceBytes,
|
|
279
|
-
artifactBytes: version.artifactBytes,
|
|
280
|
-
});
|
|
281
|
-
identity(receipt, created.projectId, ownerId, project.audience);
|
|
282
|
-
if (typeof receipt.versionId !== 'string' || !identifier.test(receipt.versionId))
|
|
283
|
-
throw new Error('Managed version identity is missing.');
|
|
284
|
-
for (const kind of ['source', 'artifact']) {
|
|
285
|
-
const bytes = readFileSync(path.join(directory, `${kind}.tar`));
|
|
286
|
-
const uploaded = await request(send, 'PUT', `${base}/versions/${receipt.versionId}/${kind}`, `${key}:${kind}`, bytes);
|
|
287
|
-
identity(uploaded, created.projectId, ownerId, project.audience);
|
|
288
|
-
if (uploaded.versionId !== receipt.versionId ||
|
|
289
|
-
uploaded.sha256 !== digest(bytes) ||
|
|
290
|
-
uploaded.bytes !== bytes.length)
|
|
291
|
-
throw new Error('Managed upload receipt differs from the staged archive.');
|
|
292
|
-
}
|
|
293
|
-
const saved = await request(send, 'GET', `${base}/versions/${receipt.versionId}`);
|
|
294
|
-
identity(saved, created.projectId, ownerId, project.audience);
|
|
295
|
-
if (saved.versionId !== receipt.versionId ||
|
|
296
|
-
saved.commit !== version.commit ||
|
|
297
|
-
saved.sourceSha256 !== version.sourceSha256 ||
|
|
298
|
-
saved.artifactSha256 !== version.artifactSha256 ||
|
|
299
|
-
saved.state !== 'saved')
|
|
300
|
-
throw new Error('Backend has not confirmed an exact saved source/build version.');
|
|
301
|
-
const result = { ...version, state: 'saved', versionId: receipt.versionId };
|
|
302
|
-
atomicJson(path.join(directory, 'version.json'), result);
|
|
303
|
-
return result;
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
export async function deployManagedVersion(root, stageId, send = fetch) {
|
|
307
|
-
root = realpathSync(root);
|
|
308
|
-
return withApplicationLock(root, async () => {
|
|
309
|
-
const { project, ownerId } = await context(root, send);
|
|
310
|
-
const { version } = staged(root, stageId, project);
|
|
311
|
-
if (!project.projectId ||
|
|
312
|
-
version.state !== 'saved' ||
|
|
313
|
-
!version.versionId ||
|
|
314
|
-
!identifier.test(version.versionId))
|
|
315
|
-
throw new Error('Save the exact managed version before requesting deployment.');
|
|
316
|
-
const base = `/projects/${project.projectId}`;
|
|
317
|
-
identity(await request(send, 'GET', base), project.projectId, ownerId, project.audience);
|
|
318
|
-
const receipt = await request(send, 'POST', `${base}/deployments`, `${version.versionId}:${project.audience}`, {
|
|
319
|
-
versionId: version.versionId,
|
|
320
|
-
audience: project.audience,
|
|
321
|
-
commit: version.commit,
|
|
322
|
-
artifactSha256: version.artifactSha256,
|
|
323
|
-
});
|
|
324
|
-
if (typeof receipt.deploymentId !== 'string' || !identifier.test(receipt.deploymentId))
|
|
325
|
-
throw new Error('Backend did not return a deployment identity.');
|
|
326
|
-
const result = await request(send, 'GET', `${base}/deployments/${receipt.deploymentId}`);
|
|
327
|
-
identity(result, project.projectId, ownerId, project.audience);
|
|
328
|
-
if (result.deploymentId !== receipt.deploymentId ||
|
|
329
|
-
result.versionId !== version.versionId ||
|
|
330
|
-
result.commit !== version.commit ||
|
|
331
|
-
result.artifactSha256 !== version.artifactSha256 ||
|
|
332
|
-
!['queued', 'building', 'deployed', 'failed'].includes(String(result.state)))
|
|
333
|
-
throw new Error('Managed deployment evidence differs from the saved version.');
|
|
334
|
-
if (result.state === 'deployed') {
|
|
335
|
-
if (typeof result.url !== 'string')
|
|
336
|
-
throw new Error('A deployed result requires a confirmed URL.');
|
|
337
|
-
const url = new URL(result.url);
|
|
338
|
-
if (url.protocol !== 'https:' || url.username || url.password || url.hash || url.search)
|
|
339
|
-
throw new Error('Invalid managed deployment URL.');
|
|
340
|
-
}
|
|
341
|
-
else
|
|
342
|
-
delete result.url;
|
|
343
|
-
const confirmed = {
|
|
344
|
-
projectId: project.projectId,
|
|
345
|
-
ownerId,
|
|
346
|
-
audience: project.audience,
|
|
347
|
-
deploymentId: result.deploymentId,
|
|
348
|
-
versionId: version.versionId,
|
|
349
|
-
commit: version.commit,
|
|
350
|
-
artifactSha256: version.artifactSha256,
|
|
351
|
-
state: result.state,
|
|
352
|
-
...(result.state === 'deployed' ? { url: result.url } : {}),
|
|
353
|
-
};
|
|
354
|
-
atomicJson(path.join(root, '.anhedral', 'managed-deployment.json'), confirmed);
|
|
355
|
-
return { ...confirmed, productVerification: 'required' };
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
export async function managedApplicationStatus(root, send = fetch) {
|
|
359
|
-
root = realpathSync(root);
|
|
360
|
-
const { project, ownerId } = await context(root, send);
|
|
361
|
-
if (!project.projectId)
|
|
362
|
-
return { project, state: 'not-connected', productVerification: 'unverified' };
|
|
363
|
-
const result = await request(send, 'GET', `/projects/${project.projectId}`);
|
|
364
|
-
identity(result, project.projectId, ownerId, project.audience);
|
|
365
|
-
return {
|
|
366
|
-
project,
|
|
367
|
-
evidence: 'provider-refresh',
|
|
368
|
-
state: ['registered', 'queued', 'building', 'deployed', 'failed'].includes(String(result.state))
|
|
369
|
-
? result.state
|
|
370
|
-
: 'unknown',
|
|
371
|
-
productVerification: 'unverified',
|
|
372
|
-
};
|
|
373
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Anhedral Cloud: setup and deployment contract
|
|
2
|
-
|
|
3
|
-
The CLI has `cloud register|stage|save|deploy|status` and a locally tested API client. There is no operated hosting backend, platform login, issued app hostname or managed provisioning service. Everything marked **planned** below is a design requirement, not an available command. Customer-account delivery remains independent; never silently substitute it for requested managed hosting.
|
|
4
|
-
|
|
5
|
-
## Product boundaries
|
|
6
|
-
|
|
7
|
-
Anhedral is the B2B managed software services business. Anhedral Cloud is its hosting/operations service and client workspace. Anhedral Init (`anhedral` on npm) is the open-source application foundation and CLI; customer-account deployments must work without an Anhedral subscription. Managed delivery is an optional provider adapter, not a dependency hidden in generated application code.
|
|
8
|
-
|
|
9
|
-
Keep `anhedral-landing` as the public website and existing portal frontend initially; retain working Next.js/Vercel hosting while the Hono/Workers control plane is built. Both portal and CLI should consume that same API. One durable model should connect organizations, memberships/roles, engagements, applications, environments, resources, releases and usage. A services project can contain multiple applications; product users are distinct from portal members. The current portal's email-primary membership permits one team per person; expand membership deliberately before multi-organization operator/client access. Private file links need storage-side authorization, not just a private portal page.
|
|
10
|
-
|
|
11
|
-
Use the portal for client progress, approvals, application access, operating status, invoices and exports; expose technical controls according to role. Keep hosted customer code on a separate origin/domain from staff/client sessions. Sell engineering and managed operations with explicit responsibilities and bounded infrastructure usage. Source/data export, recovery and the customer-account deployment path should remain usable when managed hosting ends.
|
|
12
|
-
|
|
13
|
-
Sequence: align released CLI/docs; connect portal organizations to the control-plane registry; ship one independently recoverable private application; prove updates, isolation, backups and cost controls; then add operational/billing UI and selected distribution features. Begin as a managed service for invited clients; expand self-service only after those paths are repeatable.
|
|
14
|
-
|
|
15
|
-
## Accounts and paid activation
|
|
16
|
-
|
|
17
|
-
Managed users should need Anhedral sign-in only for ordinary hosted apps. Anhedral operates the infrastructure and bills its customers; customer-account hosting keeps providers and bills in the customer's account. Product login remains separate from platform/hosting access.
|
|
18
|
-
|
|
19
|
-
| Owner | Account/service | When and gate |
|
|
20
|
-
| --- | --- | --- |
|
|
21
|
-
| Anhedral | [Cloudflare Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/pricing/), DNS zone and scoped deployment credentials | Required managed runtime. Published baseline is $25/month plus overages; purchase currently uses the dashboard. Verify account, subscription and permissions before provisioning. |
|
|
22
|
-
| Anhedral | [Neon serverless driver](https://neon.com/docs/serverless/serverless-driver); optional [Hyperdrive](https://developers.cloudflare.com/hyperdrive/platform/pricing/) | Default direct HTTP Postgres; provision isolated app credentials/data boundaries. Add Hyperdrive only for measured needs, using the non-pooled Neon endpoint. D1 is the SQLite option. Free quotas are not production capacity guarantees. Hyperdrive is included in Workers plans; Neon has separate billing. |
|
|
23
|
-
| Anhedral | [R2](https://developers.cloudflare.com/r2/pricing/), selected Cloudflare services | Source/build archives and app files; compute/storage/operations/AI/media have separate meters. Enable only selected services. |
|
|
24
|
-
| Anhedral | Better Auth, verified sender and optional OAuth provider registrations | Platform identity, recovery and login mail. No customer Clerk account required; selected app identity providers can differ. |
|
|
25
|
-
| Anhedral | Stripe merchant account | Anhedral subscriptions and usage billing. Customer app payments require the app owner's merchant account or deliberate Stripe Connect onboarding; never mix ledgers. |
|
|
26
|
-
| App owner, when selected | RevenueCat, Apple/Google store products; external AI, mail or other providers | Own commercial identity and app integrations. Verification, terms, billing and OAuth consent may require a human; the CLI reports the exact remaining step. |
|
|
27
|
-
| App owner, when distributed | [Apple](https://developer.apple.com/help/account/basics/about-your-developer-account), [Play](https://support.google.com/googleplay/android-developer/answer/6112435), [Chrome](https://developer.chrome.com/docs/webstore/register) publisher accounts | Paid enrollment/registration and identity checks. Native signing/builds and review remain platform-specific; cloud backend deployment does not publish clients. |
|
|
28
|
-
| Optional | GitHub; custom domain/registrar | GitHub export/connection, not a prerequisite for managed source recovery. Platform subdomains use Anhedral DNS; [customer domains](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/plans/) require ownership validation and hostname limits. |
|
|
29
|
-
|
|
30
|
-
Prices/eligibility must be rechecked at activation. Current numeric baseline is a dated October 8, 2026 check, not an app quote. Paid activation requires authorized spending; an agent can prepare setup and resume after enrollment without recreating resources.
|
|
31
|
-
|
|
32
|
-
## Platform architecture
|
|
33
|
-
|
|
34
|
-
Use separate control-plane and dispatch Workers. Better Auth identifies platform users; Neon holds owners, apps, permissions, resource identities, versions, deployments and billing ledgers. R2 stores immutable source/build archives. Queues/Workflows execute resumable build/provision/deploy jobs. The dispatch Worker maps an authorized hostname to an isolated user Worker; it must not trust a caller-selected worker name.
|
|
35
|
-
|
|
36
|
-
[Workers for Platforms bindings](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/bindings/) support app resources including D1/R2/KV, Durable Objects, Queues, Workflows and Hyperdrive. Attach only that app's resources; use separate credentials and migration/runtime roles for Postgres. Never give user code platform deployment, billing or shared-admin credentials. Build untrusted code in an isolated runner; export only reviewed executable artifacts. Resolve resources on the server from a declared capability manifest, not arbitrary account IDs from uploaded code.
|
|
37
|
-
|
|
38
|
-
Private previews and production enforce platform audience at dispatch, including assets, APIs and WebSocket upgrades. In-app authorization is still the application's responsibility. Test the chosen framework in the actual dispatch runtime: [platform limits](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/limits/) include disabled `caches.default` and restricted `request.cf` in untrusted namespaces. Do not enable trusted mode for customer code to bypass compatibility failures. User Workers currently lack gradual deployments; use immutable versioned Worker names plus an authorized routing switch for previews/rollback, and verify WebSocket/stateful-version compatibility.
|
|
39
|
-
|
|
40
|
-
## Agent workflow
|
|
41
|
-
|
|
42
|
-
**Planned interface:** `cloud login` → `cloud plan --json` → implement/check/commit → `cloud ship --json --wait` → `cloud verify`. Login should use a browser/device authorization flow and OS-protected renewable credentials. CI uses revocable project-scoped credentials. JSON output should distinguish setup required, payment required, queued, failed, deployed and verified, with resumable operation IDs and non-secret next actions.
|
|
43
|
-
|
|
44
|
-
`plan` must identify owner/environment, declared resources, provider access, compatibility, estimated cost and authorized limits without mutations. `ship` should check source/build provenance, resolve resources, stage/save, apply reviewed compatible migrations, deploy, poll to completion and return the independently confirmed URL. Neither command exists yet. `new --hosting anhedral` is also not supported; generation currently selects Cloudflare or Vercel.
|
|
45
|
-
|
|
46
|
-
The current low-level sequence is executable only with an operated compatible backend and configured `ANHEDRAL_HOSTING_ORIGIN` / `ANHEDRAL_HOSTING_TOKEN`:
|
|
47
|
-
|
|
48
|
-
```sh
|
|
49
|
-
anhedral cloud register my-app --audience private --json
|
|
50
|
-
# Review source/secrets and commit the managed manifest in the app's own Git repo.
|
|
51
|
-
git -C my-app add <reviewed-paths>
|
|
52
|
-
git -C my-app commit -m "Prepare application version"
|
|
53
|
-
pnpm --dir my-app run check
|
|
54
|
-
# Build a complete self-contained Worker bundle/assets into an ignored app directory.
|
|
55
|
-
anhedral cloud stage my-app --artifact /absolute/path/my-app/build --json
|
|
56
|
-
anhedral cloud save my-app --version <returned-stage-id> --json
|
|
57
|
-
anhedral cloud deploy my-app --version <returned-stage-id> --json
|
|
58
|
-
anhedral cloud status my-app --json
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Current `deploy` requests deployment and checks once; it may return queued/building. It does not wait for completion, run product acceptance or implement managed rollback/export. `save` writes connected identity into the tracked manifest; review/commit that change before staging a subsequent version. A staged artifact is not proven to originate from its source merely because both were archived: the planned build receipt must bind source commit, lockfile, toolchain and artifact digest.
|
|
62
|
-
|
|
63
|
-
## Source and API contract
|
|
64
|
-
|
|
65
|
-
Local Git remains the working source of truth. Save the exact clean commit plus complete build tree; retain immutable version IDs, SHA-256 hashes and byte lengths. Current staging permits regular files only, at most 64 MiB per archive and 10,000 build files; it rejects symlinks/submodules and credential files. Restore/clone/export must preserve history and allow exit; optional [Artifacts](https://developers.cloudflare.com/artifacts/platform/pricing/) can add Git-compatible managed history but is Paid-plan-only and has separately published metering.
|
|
66
|
-
|
|
67
|
-
Current API routes are under `/v1`. Authenticate every route, derive ownership from the session, preserve audience and validate project/version identity. Pin service origin and owner after connection; forbid redirects and credential-bearing origins.
|
|
68
|
-
|
|
69
|
-
| Route | Contract |
|
|
70
|
-
| --- | --- |
|
|
71
|
-
| `GET /session` | Authenticated owner identity. |
|
|
72
|
-
| `POST /projects`, `GET /projects/:id` | Idempotent registration/current owned project state. |
|
|
73
|
-
| `POST /projects/:id/versions` | Immutable commit, hashes/lengths and version identity. |
|
|
74
|
-
| `PUT /projects/:id/versions/:version/source` or `/artifact` | Independently verify uploaded hashes/lengths. |
|
|
75
|
-
| `GET /projects/:id/versions/:version` | Saved only when both verified archives exist. |
|
|
76
|
-
| `POST /projects/:id/deployments` | Idempotent deployment of exact saved version/audience. |
|
|
77
|
-
| `GET /projects/:id/deployments/:deployment` | Independently confirmed state/source/artifact; HTTPS URL only when deployed. |
|
|
78
|
-
|
|
79
|
-
Validate archives again server-side before extraction, including traversal, links, duplicate paths and decompression/size limits. Preserve incomplete upload/job evidence for safe retries. Saving source, deploying code and passing product tests remain separate states.
|
|
80
|
-
|
|
81
|
-
## Limits and completion gates
|
|
82
|
-
|
|
83
|
-
Enforce [dispatch CPU/subrequest limits](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/custom-limits/) plus application-owned request, storage, SQL, job, AI/media and outbound budgets. Provider alerts are not hard spend caps. Reserve costly work transactionally before execution; reconcile completion/failure idempotently. Rate-limit platform API jobs with bounded retry/backoff for provider throttling. Limit previews/builds, concurrent jobs and retention; define grace/suspension/export policies. Do not delete customer data merely because billing fails. Optional paid/beta/enterprise services need explicit eligibility checks and adapters; free analytics does not make the rest free.
|
|
84
|
-
|
|
85
|
-
Build in this order: platform identity and account/subscription preflight; durable registry/archive API; isolated build/resource provisioning; dispatch/private routes; resumable deployment polling; logs/secrets/recovery/export; billing/quotas; signed client distribution where requested. First acceptance fixture: two owners, independent private apps, persistence, file upload, one bounded job and repeated update. Prove cross-tenant rejection, anonymous denial, interrupted-save/deploy retry, source restoration, quota rejection and code rollback/data compatibility before onboarding.
|
|
86
|
-
|
|
87
|
-
Maintain this single contract as capabilities ship: replace planned claims with verified command/API behavior and update account/limit links. Keep dated test evidence in existing release/test artifacts, not new analysis documents. Deployment and publication remain unproven until the operated service and installed agent workflow pass acceptance.
|
package/docs/sponsorships.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Support Anhedral Init
|
|
2
|
-
|
|
3
|
-
## Help more people build applications they own
|
|
4
|
-
|
|
5
|
-
Anhedral Init is free and open source. We build tools that help developers turn an idea into an application with source code, infrastructure, and data in their own accounts.
|
|
6
|
-
|
|
7
|
-
If Anhedral Init helps you build, saves you time, or makes your work easier, consider supporting its continued development. Your support helps us improve the tools, maintain integrations, fix bugs, and write clearer documentation.
|
|
8
|
-
|
|
9
|
-
## GitHub Sponsors — coming soon
|
|
10
|
-
|
|
11
|
-
Support can be a thank-you for something useful today or an ongoing contribution to the project's future. Every contribution helps us spend more time improving Anhedral Init for the people who use it.
|
|
12
|
-
|
|
13
|
-
We plan to offer donations and ongoing sponsorships through GitHub Sponsors. Our sponsorship profile is not available yet; we'll add the link here when it launches.
|
|
14
|
-
|
|
15
|
-
In the meantime, [contact us at business@anhedral.com](mailto:business@anhedral.com?subject=Support%20Anhedral%20Init) if you'd like to support the project.
|
|
16
|
-
|
|
17
|
-
## Company sponsorships
|
|
18
|
-
|
|
19
|
-
Does your company use Anhedral Init or want to support its development? We'd love to hear from you. Get in touch to discuss a sponsorship and how we can recognize your support.
|
|
20
|
-
|
|
21
|
-
**[Become a sponsor](mailto:business@anhedral.com?subject=Anhedral%20Init%20sponsorship)**
|
|
22
|
-
|
|
23
|
-
Thank you for helping us keep Anhedral Init free, open source, and improving.
|
|
24
|
-
|
|
25
|
-
[Back to Anhedral Init](../README.md)
|