anhedral 0.10.1 → 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 +77 -38
- 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/dist/bin.js +271 -154
- package/dist/capabilities.d.ts +3 -3
- 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/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/mobile.js +8 -5
- 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/registry/add.d.ts +6 -0
- package/dist/registry/add.js +29 -1
- 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/usage.d.ts +10 -0
- package/dist/registry/usage.js +22 -37
- package/dist/scaffold/auth.js +3 -19
- package/dist/scaffold/workspace.js +11 -0
- 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 +7 -1
- package/dist/standard.d.ts +2 -1
- package/dist/standard.js +11 -11
- 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/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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Provider configuration
|
|
2
|
+
|
|
3
|
+
Anhedral scaffolds source, guides authentication, and provisions and configures the selected stack before deployment. The application and accounts belong to the developer or client. Feature development, design, custom integrations, and ongoing maintenance are Anhedral's services.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Run `anhedral init <directory> <stack-flags>`. Review the generated source and install dependencies.
|
|
8
|
+
2. Fill in `anhedral.providers.json`. It contains only selected integrations, nonsecret resource names, account identifiers, regions, and desired webhook settings. Empty account/domain fields require an explicit choice. Commit this file with the application source. To use an existing resource, set its ID instead of requesting a new project by name.
|
|
9
|
+
3. Supply management credentials through the process environment and run `anhedral auth <directory>`. `--status` performs checks without opening login. Cloudflare, Vercel, and Expo support interactive CLI login; API integrations show their required credentials. A successful read does not prove every write permission.
|
|
10
|
+
4. Connect the intended hosting account, project, plan, and audience using `anhedral connect`. Review and commit the connection manifest. Expo uses the explicit `owner` and `slug` in `apps/mobile/app.json`.
|
|
11
|
+
5. Inspect `anhedral registry capabilities --json` for implemented scope and evidence limits, then inspect `anhedral ship <directory> --dry-run`. It reports missing provider settings without login, network requests, writes, provisioning, or deployment. Run `ship` after reviewing the plan and authorizing the provider operations.
|
|
12
|
+
|
|
13
|
+
The manifest uses a typed, provider-specific configuration contract. It rejects unsupported providers, foreign fields, unknown enum choices, and duplicate webhook events before network operations. Empty fields are accepted for scaffolding but must be completed when required for provisioning. The exported `PROVIDER_CONFIGURATION_SCHEMA` matches these fields and enums; no generated `$schema` URL is currently installed in projects.
|
|
14
|
+
|
|
15
|
+
Authentication results label verified access as `session`, `management`, or `runtime`; unsuccessful checks use `none`. Clerk and OpenAI management credentials take precedence over runtime credentials. A runtime probe is never reported as management access. Vercel and Expo session probes require bounded, single-identity output from the pinned CLI; diagnostic paragraphs do not count as a session. These probes do not verify the selected project, all write permissions, account enrollment, or remaining owner prerequisites.
|
|
16
|
+
|
|
17
|
+
Management keys do not belong in this manifest or application source. Runtime credentials returned by providers are configured in ignored application environment files and private `.anhedral/` state. API errors and CLI results omit credential values. Never attach this private state to an issue or commit it.
|
|
18
|
+
|
|
19
|
+
## Adapters
|
|
20
|
+
|
|
21
|
+
| Provider | Management credential | Automated configuration | Owner prerequisites and remaining application work |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| Neon | `NEON_API_KEY` | Create or select an explicitly scoped organization/project; check outstanding operations; obtain the selected database/role's pooled connection URI; configure runtime and migration environment files | An eligible account/region; reviewed schema and migrations; application data access |
|
|
24
|
+
| Clerk | `CLERK_PLATFORM_API_KEY` | Verify the selected workspace; create or select an application and environment; retrieve its matching publishable and secret keys | Platform API access; production domain/DNS/OAuth setup; application authorization. `CLERK_SECRET_KEY` alone verifies runtime access and cannot create an application |
|
|
25
|
+
| Resend | `RESEND_API_KEY` with management access | Create or select the explicit sending domain; request verification; report required DNS records; issue a domain-scoped sending key only after verification | Domain ownership and DNS publication/propagation; sending eligibility; templates and delivery behavior |
|
|
26
|
+
| Stripe | `STRIPE_SECRET_KEY` | Verify the explicit account and test/live key mode; configure a webhook with explicitly listed events when requested; securely retain its signing secret | Account activation for live payments; product/price decisions and webhook handlers. Provisioning does not invent a billing model or create financial commitments |
|
|
27
|
+
| RevenueCat | `RC_API_KEY` with v2 configuration access | Create or select a project; create/select iOS, Android, or connected Stripe apps; retrieve platform public SDK keys | Store credentials or Stripe account connection; products, offerings, entitlements, SDK installation, and purchase verification |
|
|
28
|
+
| Sentry | `SENTRY_AUTH_TOKEN` | Select organization/team/project; create a project if absent; verify the team/platform; select a single active client key or create one if absent; configure DSN | Appropriate organization/team permissions; platform SDK initialization, source maps, privacy filtering, and alerts |
|
|
29
|
+
| PostHog | `POSTHOG_PERSONAL_API_KEY` | Select US/EU region and organization; create/select a project; retrieve its ingestion key and matching ingestion host | Project permissions; product events, privacy/consent rules, and SDK initialization. Personal management keys never become ingestion keys |
|
|
30
|
+
| OpenAI | `OPENAI_ADMIN_KEY` | Create or select an active project; create a named service account; retain its one-time runtime key privately | Organization administration access, provider billing eligibility, limits, and application AI behavior. `OPENAI_API_KEY` alone cannot create projects |
|
|
31
|
+
|
|
32
|
+
The adapters implement a bounded scaffolding setup, not every administration endpoint a provider exposes. Account enrollment, required billing acceptance, DNS ownership, OAuth consent, store enrollment, and product-specific design remain explicit owner steps. Provisioning success does not certify those steps or finished product functionality.
|
|
33
|
+
|
|
34
|
+
### Example manifest
|
|
35
|
+
|
|
36
|
+
For a selected Hono + Neon + PostHog stack:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"schemaVersion": 1,
|
|
41
|
+
"providers": {
|
|
42
|
+
"neon": {
|
|
43
|
+
"name": "customer-app",
|
|
44
|
+
"organizationId": "org-example",
|
|
45
|
+
"region": "aws-us-east-1",
|
|
46
|
+
"database": "app",
|
|
47
|
+
"role": "app_owner"
|
|
48
|
+
},
|
|
49
|
+
"posthog": {
|
|
50
|
+
"name": "customer-app",
|
|
51
|
+
"organizationId": "00000000-0000-0000-0000-000000000000",
|
|
52
|
+
"region": "eu"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Replace example organization IDs with the intended account's actual identifiers. Existing resources use `projectId`; Clerk uses `applicationId` and `workspaceId`; Resend uses `domainId`; RevenueCat can select `iosAppId`, `androidAppId`, or `webAppId`. New RevenueCat native apps use `bundleId` / `packageName`; a new Stripe app requires a previously connected `stripeAccountId`. Sentry selects `organization`, `team`, `project`, and `platform`. Stripe requires `accountId` and `mode`; optional `webhookUrl` must use HTTPS and `webhookEvents` must explicitly name the handled events. A webhook endpoint and handler must already be designed before registering it.
|
|
59
|
+
|
|
60
|
+
## Delivery and retries
|
|
61
|
+
|
|
62
|
+
Cloudflare reconciles D1, private R2 buckets, KV, Queues, and private JSON Basin streams. Basin HTTP ingestion stays disabled; the Worker binding is enabled. Sinks and transformations require application-specific design. Durable Objects, schedules, Workflows, and Workers AI bindings are configured through Worker deployment. Hyperdrive configurations added by a developer still require an explicit provider workflow.
|
|
63
|
+
|
|
64
|
+
If resource provisioning changes tracked binding IDs, `ship` returns `configuration-required` and stops before deployment. Review and commit those changes, then repeat the command. It never commits code automatically. Pending Neon operations or unverified Resend DNS return `provider-configuration-required` and stop deployment. The configured integrations' remaining product setup appears in their results.
|
|
65
|
+
|
|
66
|
+
Review the generated database schema and apply the selected database migrations before testing application features or authentication. Generated tables and SDK factories are starting points; provisioning resources does not migrate the application database or implement its authorization rules.
|
|
67
|
+
|
|
68
|
+
For selected Better Auth applications, shipping first reads `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` from the primary server's ignored, untracked runtime file. Configure a generated nonplaceholder secret of at least 32 characters and the intended HTTPS public origin. On POSIX systems this file must have mode `0600`; keep equivalent private file access on other systems. Tracked or symlinked files, broad file permissions, missing values, placeholder secrets, and unsafe origins stop shipping before provider mutations. For older generated Workers, remove `BETTER_AUTH_URL` or `BETTER_AUTH_SECRET` from tracked `wrangler.jsonc` `vars`, review and commit that change, then configure both values in the private runtime file. Wrangler cannot deploy a same-named plain variable and secret. The preflight does not generate a secret, choose a domain, modify files, configure OAuth, or verify application-user authorization.
|
|
69
|
+
|
|
70
|
+
Cloudflare uploads runtime credentials with the code using Wrangler's `--secrets-file`; it does not make a separate secret update that deploys an old Worker. Temporary credential files are private and removed after the operation. Vercel configures only the selected web runtime's production environment, checks every returned result for success, and then performs the checked deployment. In mixed Vercel/Cloudflare projects, default `ship` coordinates Cloudflare services first, then the Vercel web application under one operation lock. `ship --provider cloudflare` or `ship --provider vercel` explicitly selects only that hosting target. A setup gate stops before the dependent web deployment. If Cloudflare completes and Vercel fails, the result is partial; no atomic provider or data rollback is attempted. Repeating `ship` rechecks both targets and reuses provider receipts where available; it does not skip completed phases merely because the diagnostic operation journal says they completed.
|
|
71
|
+
|
|
72
|
+
Shipping records nonsecret phases and outcome in ignored `.anhedral/operation.json`. This is a diagnostic journal, not a resumable transaction or proof of live product behavior. `status --json` and the dashboard can show the recorded operation; resource replay protection remains in the provider journals and deployment/build receipts. A new run records a new operation and revalidates current prerequisites.
|
|
73
|
+
|
|
74
|
+
Creation receipts are saved immediately in ignored `.anhedral/providers.json`. Schema 2 keeps minimal creation identities and status there; supported one-time credentials are stored separately in ignored `.anhedral/provider-credentials.json` with private file permissions. Legacy schema 1 journals migrate credentials before replacing their full-response receipts, preserving recovery evidence if receipt writing is interrupted. Subsequent runs verify existing resources and reuse saved credentials. Ignored `.anhedral/runtime.json` contains deployment runtime values and must also remain private. A confirmed 4xx rejection can be retried after access is corrected. A timeout, malformed success response, rate limit, or server error can leave an unknown creation outcome: the CLI stops rather than replaying the mutation. Inspect the provider and preserve a secure backup of the receipt before reconciling the pending operation. Desired configuration changes also stop and require reconciliation. Never remove state blindly to bypass this check.
|
|
75
|
+
|
|
76
|
+
Read-only API requests retry bounded transient failures (429, 502, 503, 504, transport interruptions and timeouts) within a 30-second overall deadline, with at most three attempts. Short provider retry delays are respected; longer delays return an actionable failure instead of retrying early. Invalid or oversized JSON is not retried. Mutating POST/PATCH requests are never replayed automatically, including requests carrying an idempotency key.
|
|
77
|
+
|
|
78
|
+
Keep secure backups of `.anhedral/` and runtime environment files when moving machines. A fresh clone does not carry one-time credentials or provisioning receipts. Configure explicitly selected existing resource IDs and recover runtime credentials through authorized provider tooling rather than treating it as the original provisioning session. The CLI does not automatically delete, rotate, or replace resources. `integrations status` reads the journal without migration or writes. `integrations recover --provider <provider> --operation <exact hash> --resource <id>` verifies a supported existing resource against the unchanged recorded configuration, then writes only its local completion receipt. It never recreates or deletes a provider resource. Recovery is unavailable for legacy pending entries without operation metadata, changed desired configuration, and one-time credential creation when the original credential is missing; inspect the provider and retain secure backups. RevenueCat application recovery requires the parent project ID recorded explicitly in the original configuration.
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
anhedral integrations status my-app --json
|
|
82
|
+
anhedral integrations recover my-app --provider neon --operation <exact-64-character-hash> --resource <verified-project-id> --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Copy the operation hash from status and supply the intended resource ID yourself. Supported recovery checks cover known Neon projects, Clerk applications/workspace/environment, Resend domains, PostHog projects/organizations, active OpenAI projects, Sentry projects/teams/platform, and RevenueCat projects or explicitly scoped apps. They perform provider GET requests and then update only the local completion receipt. Status does not migrate old journals or create directories. It shows missing descriptors and one-time-credential operations as blocked. Resend sending keys, Stripe webhook signing secrets, and OpenAI service-account keys cannot be recovered from a resource ID alone; securely retained credentials and explicit provider inspection are required. Never paste credentials into the command or conversation.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Mobile delivery
|
|
89
|
+
|
|
90
|
+
Use `anhedral ship <directory> --platform ios|android --profile <profile>`. Set the intended Expo `owner` and `slug` in static `apps/mobile/app.json`. The pinned EAS CLI creates/links the project when needed, then stops for review and a source commit. Subsequent builds independently check the linked project identity. Inherited profiles require explicit resolution before automatic delivery.
|
|
91
|
+
|
|
92
|
+
Only public mobile keys are uploaded to the selected EAS environment; server credentials are excluded. Queued builds return a build-page link and `building` state. Repeat the same command to inspect the saved build ID without creating another build. Completed builds must match the selected project and source commit before returning the download link. Failed/cancelled builds require provider inspection before removing the pending record and requesting another build.
|
|
93
|
+
|
|
94
|
+
Android preview builds produce APKs; production builds produce AABs. iOS simulator artifacts need no device signing; signed iOS builds need the developer's Apple credentials and enrollment. Anhedral never runs `eas submit`. Store uploads and approvals belong to the owner.
|
|
95
|
+
|
|
96
|
+
## Verification scope
|
|
97
|
+
|
|
98
|
+
Local contract tests cover selected-stack planning, API request/response shapes, resource reuse, one-time credentials, account mismatch, pending operations, DNS gates, unknown outcomes, private file safety, runtime partitioning, and deployment handoff. These tests do not establish live account eligibility, quota, permissions, billing, DNS, signing, or production product behavior. Verify those in explicitly authorized accounts before release.
|
|
99
|
+
|
|
100
|
+
Provider contracts: [Neon API](https://api-docs.neon.tech/reference/createproject), [Clerk Platform schema](https://github.com/clerk/openapi-specs/blob/main/platform/beta.yml), [Resend API](https://resend.com/docs/api-reference/introduction), [Stripe API](https://docs.stripe.com/api), [RevenueCat API v2](https://www.revenuecat.com/docs/api-v2), [Sentry API](https://docs.sentry.io/api/), [PostHog API](https://posthog.com/docs/api), [OpenAI administration](https://developers.openai.com/api/reference/resources/admin/), [Cloudflare secrets](https://developers.cloudflare.com/workers/configuration/secrets/), [Vercel OpenAPI](https://openapi.vercel.sh), and [pinned EAS source](https://github.com/expo/eas-cli/tree/v21.0.1/packages/eas-cli/src/commands).
|
|
101
|
+
|
|
102
|
+
## Authorized staging acceptance
|
|
103
|
+
|
|
104
|
+
Use a dedicated, explicitly selected account/project for live verification. Before the first mutation, record its owner, region, resource names, permitted operations, spending limit, cleanup responsibility, and retention period in the application's verification notes. Never use production credentials for routine tests or treat access to an account as permission to incur unspecified costs.
|
|
105
|
+
|
|
106
|
+
For each selected provider, verify the following against the exact candidate CLI:
|
|
107
|
+
|
|
108
|
+
1. Missing, expired, and insufficient credentials produce an actionable state without leaking keys or starting creation.
|
|
109
|
+
2. The selected account, organization, environment and region match the manifest before provisioning.
|
|
110
|
+
3. First creation and repeated configuration use the intended resources, with no duplicate creation.
|
|
111
|
+
4. Pending DNS, provider operations or enrollment produce an explicit blocked state before deployment.
|
|
112
|
+
5. An interrupted or unknown creation outcome preserves evidence and stops automatic replay; recovery requires inspected provider state.
|
|
113
|
+
6. Runtime credentials reach only their intended server or public client target. Check runtime access through a bounded application flow, without publishing the credential value.
|
|
114
|
+
7. The actual deployed service or build matches the selected project and checked source commit. Test its promised user flow independently of build success.
|
|
115
|
+
8. Record provider/resource IDs, source revision, nonsecret verification results, unresolved owner steps, and cleanup results. Keep credential files and complete provider responses out of public logs and issues.
|
|
116
|
+
|
|
117
|
+
A completed local contract test, an accessible API endpoint, a created resource, a successful deployment, and a verified product flow are separate evidence levels. Release notes must name the level actually established; do not promote one to another.
|
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
|
-
}
|