From f27606b7090238711152f98c124e84b5d1b854fa Mon Sep 17 00:00:00 2001 From: klarkxy <278370456@qq.com> Date: Wed, 12 Aug 2026 23:25:43 +0800 Subject: [PATCH] feat: establish NekoNest Cloud control and relay --- .github/workflows/cloud-pwa.yml | 114 + .github/workflows/cloud-relay.yml | 81 + .gitignore | 45 + .openai/hosting.json | 4 + README.md | 171 + app/admin/AdminActions.tsx | 384 + app/admin/page.tsx | 283 + app/api/account/deletion/route.ts | 61 + app/api/account/export/route.ts | 33 + app/api/admin/beta-access/route.ts | 43 + app/api/admin/beta/route.ts | 14 + app/api/admin/exemptions/route.ts | 18 + app/api/admin/feedback/route.ts | 37 + app/api/admin/incidents/route.ts | 61 + app/api/admin/launch-gates/route.ts | 14 + app/api/admin/prices/route.ts | 13 + app/api/admin/relay-migrations/route.ts | 40 + app/api/admin/relay-purges/route.ts | 38 + app/api/admin/retention/route.ts | 34 + app/api/beta-access/route.ts | 54 + app/api/billing/quotes/route.ts | 27 + app/api/feedback/route.ts | 31 + app/api/hosts/pairing/cancel/route.ts | 33 + app/api/hosts/pairing/route.ts | 64 + app/api/hosts/revoke/route.ts | 28 + .../relay/authorization-delta/route.ts | 25 + .../relay/authorization-snapshot/route.ts | 22 + .../internal/relay/authorize-device/route.ts | 23 + .../internal/relay/authorize-phone/route.ts | 25 + .../relay/complete-phone-handoff/route.ts | 26 + .../relay/consume-phone-handoff/route.ts | 30 + app/api/internal/relay/heartbeat/route.ts | 32 + .../relay/migrations/advance/route.ts | 36 + .../internal/relay/purges/advance/route.ts | 34 + .../internal/relay/register-device/route.ts | 45 + .../relay/resolve-device-route/route.ts | 22 + .../relay/resolve-handoff-route/route.ts | 22 + .../relay/resolve-phone-route/route.ts | 25 + .../relay/resolve-tenant-route/route.ts | 25 + app/api/internal/relay/revoke-phone/route.ts | 24 + app/api/pwa/handoff/route.ts | 23 + app/api/respond.ts | 72 + app/api/status/route.ts | 15 + app/chatgpt-auth.ts | 90 + app/cloud-auth.ts | 71 + app/components/ConnectivityBanner.tsx | 43 + app/components/RouteStates.tsx | 67 + app/components/Shells.tsx | 257 + app/daemon-release.ts | 138 + app/dashboard/OpenPwaButton.tsx | 36 + app/dashboard/billing/AccessRequestForm.tsx | 97 + app/dashboard/billing/page.tsx | 125 + app/dashboard/connection-copy.ts | 39 + app/dashboard/error.tsx | 15 + app/dashboard/feedback/FeedbackForm.tsx | 89 + app/dashboard/feedback/page.tsx | 87 + app/dashboard/hosts/HostRevokeButton.tsx | 39 + app/dashboard/hosts/PairingCancelButton.tsx | 65 + app/dashboard/hosts/new/PairingForm.tsx | 363 + app/dashboard/hosts/new/onboarding.ts | 72 + app/dashboard/hosts/new/page.tsx | 39 + app/dashboard/hosts/page.tsx | 95 + app/dashboard/loading.tsx | 10 + app/dashboard/onboarding.ts | 83 + app/dashboard/page.tsx | 138 + .../security/AccountLifecycleActions.tsx | 148 + app/dashboard/security/page.tsx | 34 + app/download/page.tsx | 106 + app/globals.css | 904 ++ app/layout.tsx | 50 + app/page.tsx | 240 + app/pricing/page.tsx | 102 + app/privacy/data-inventory.ts | 148 + app/privacy/page.tsx | 92 + app/readiness/page.tsx | 73 + app/status/error.tsx | 14 + app/status/loading.tsx | 10 + app/status/page.tsx | 97 + app/trust/page.tsx | 90 + build/sites-vite-plugin.ts | 45 + db/access-boundary.ts | 48 + db/access-requests.ts | 190 + db/beta-operations.ts | 268 + db/bootstrap.ts | 367 + db/device-claim.ts | 218 + db/device-control-plane.ts | 191 + db/domain-error.ts | 23 + db/domain.ts | 149 + db/index.ts | 13 + db/invitations.ts | 58 + db/launch-gates.ts | 87 + db/pairing.ts | 351 + db/registration-replay.ts | 62 + db/relay-authorization.ts | 226 + db/relay-control-plane.ts | 1057 ++ db/relay-control-sql.ts | 214 + db/relay-migration-state.ts | 17 + db/relay-migrations.ts | 380 + db/relay-node-identity.ts | 162 + db/relay-purge-proof.ts | 68 + db/relay-purges.ts | 443 + db/relay-routing.ts | 52 + db/repository.ts | 4005 ++++++ db/retention-runner.ts | 103 + db/retention.ts | 271 + db/schema.ts | 826 ++ db/service-status.ts | 26 + docs/beta-operations.md | 67 + docs/commercial-contract.md | 47 + docs/daemon-distribution.md | 58 + docs/daemon-relay-handoff.md | 89 + docs/data-inventory.md | 53 + docs/implementation-plan.md | 55 + docs/launch-gates.md | 48 + docs/provisioning-worker.md | 37 + docs/relay-operations.md | 65 + docs/retention-maintenance.md | 26 + docs/security-headers.md | 28 + docs/state-model.md | 65 + drizzle.config.ts | 7 + drizzle/0000_condemned_legion.sql | 240 + drizzle/0001_mushy_vance_astro.sql | 46 + drizzle/0002_wild_ravenous.sql | 1 + drizzle/0003_medical_rocket_racer.sql | 40 + drizzle/0004_loud_prodigy.sql | 8 + drizzle/0005_pale_corsair.sql | 16 + drizzle/0006_clever_shocker.sql | 16 + drizzle/0007_zippy_nomad.sql | 14 + drizzle/0008_far_justice.sql | 9 + drizzle/0009_flat_robbie_robertson.sql | 16 + drizzle/0010_windy_toxin.sql | 22 + drizzle/0011_next_thunderball.sql | 1 + drizzle/0012_shared_relay_control_plane.sql | 182 + drizzle/0013_relay_migration_fencing.sql | 34 + drizzle/0014_relay_tenant_purge.sql | 26 + drizzle/0015_phone_handoff_idempotency.sql | 5 + drizzle/0016_phone_handoff_activation.sql | 6 + drizzle/9000_provisioning_invariants.sql | 111 + drizzle/9001_provisioning_slug_backfill.sql | 24 + .../9002_ready_credential_reconciliation.sql | 21 + drizzle/meta/0000_snapshot.json | 1700 +++ drizzle/meta/0001_snapshot.json | 1996 +++ drizzle/meta/0002_snapshot.json | 2003 +++ drizzle/meta/0003_snapshot.json | 2187 +++ drizzle/meta/0004_snapshot.json | 2248 +++ drizzle/meta/0005_snapshot.json | 2362 ++++ drizzle/meta/0006_snapshot.json | 2468 ++++ drizzle/meta/0007_snapshot.json | 2568 ++++ drizzle/meta/0008_snapshot.json | 2637 ++++ drizzle/meta/0009_snapshot.json | 2749 ++++ drizzle/meta/0010_snapshot.json | 2913 ++++ drizzle/meta/0011_snapshot.json | 2920 ++++ drizzle/meta/_journal.json | 97 + eslint.config.mjs | 18 + next.config.ts | 7 + package-lock.json | 11345 ++++++++++++++++ package.json | 49 + postcss.config.mjs | 7 + public/favicon.svg | 6 + public/og.png | Bin 0 -> 1430142 bytes relay/cmd/relay/main.go | 156 + relay/go.mod | 28 + relay/go.sum | 81 + relay/internal/attachmentstore/store.go | 232 + relay/internal/auditsink/audit.go | 24 + relay/internal/authsnapshot/snapshot.go | 305 + relay/internal/authsnapshot/snapshot_test.go | 84 + relay/internal/config/config.go | 330 + relay/internal/config/config_test.go | 59 + relay/internal/controlplane/client.go | 417 + relay/internal/forwarder/forwarder.go | 323 + relay/internal/forwarder/forwarder_test.go | 41 + relay/internal/migration/manager.go | 175 + relay/internal/migration/manager_test.go | 103 + relay/internal/purge/manager.go | 120 + relay/internal/purge/manager_test.go | 64 + relay/internal/pushsink/pushsink.go | 33 + relay/internal/registry/registry.go | 852 ++ relay/internal/registry/registry_test.go | 316 + relay/internal/service/service.go | 847 ++ relay/internal/service/service_test.go | 392 + relay/internal/tenantbackup/backup.go | 593 + relay/internal/tenantbackup/backup_test.go | 99 + relay/internal/tenantfs/paths.go | 94 + relay/internal/tenantfs/paths_test.go | 58 + relay/internal/tenantpurge/purge.go | 128 + relay/internal/tenantpurge/purge_test.go | 77 + relay/internal/tenantstore/db.go | 776 ++ relay/internal/tenantstore/messages.go | 187 + relay/internal/tenantstore/permissions.go | 49 + relay/internal/tenantstore/phone.go | 337 + relay/internal/tenantstore/principals.go | 122 + relay/internal/tenantstore/prompt_commands.go | 259 + relay/internal/tenantstore/push.go | 76 + relay/internal/tenantstore/util.go | 29 + release/daemon-policy.mjs | 27 + release/daemon-version.ts | 40 + release/verify-daemon-release-core.mjs | 242 + tests/access-boundary.test.mjs | 191 + tests/access-requests.test.mjs | 266 + tests/beta-access-operations.test.mjs | 28 + tests/beta-onboarding.test.mjs | 78 + tests/beta-policy-controls.test.mjs | 28 + tests/daemon-onboarding.test.mjs | 68 + tests/daemon-release.test.mjs | 140 + tests/device-control-plane.test.mjs | 190 + tests/invitations.test.mjs | 193 + tests/pairing-progress.test.mjs | 162 + tests/privacy-inventory.test.mjs | 70 + ...public-beta-access-reconciliation.test.mjs | 115 + tests/relay-control-plane.test.mjs | 1167 ++ tests/release-verifier.test.mjs | 176 + tests/rendered-html.test.mjs | 1574 +++ tests/retention-maintenance.test.mjs | 357 + tests/security-headers.test.mjs | 82 + tools/verify-daemon-release.mjs | 44 + tsconfig.json | 36 + types/cloudflare-env.d.ts | 16 + types/raw.d.ts | 4 + vite.config.ts | 61 + worker/index.ts | 54 + worker/security-headers.ts | 81 + 222 files changed, 71456 insertions(+) create mode 100644 .github/workflows/cloud-pwa.yml create mode 100644 .github/workflows/cloud-relay.yml create mode 100644 .gitignore create mode 100644 .openai/hosting.json create mode 100644 README.md create mode 100644 app/admin/AdminActions.tsx create mode 100644 app/admin/page.tsx create mode 100644 app/api/account/deletion/route.ts create mode 100644 app/api/account/export/route.ts create mode 100644 app/api/admin/beta-access/route.ts create mode 100644 app/api/admin/beta/route.ts create mode 100644 app/api/admin/exemptions/route.ts create mode 100644 app/api/admin/feedback/route.ts create mode 100644 app/api/admin/incidents/route.ts create mode 100644 app/api/admin/launch-gates/route.ts create mode 100644 app/api/admin/prices/route.ts create mode 100644 app/api/admin/relay-migrations/route.ts create mode 100644 app/api/admin/relay-purges/route.ts create mode 100644 app/api/admin/retention/route.ts create mode 100644 app/api/beta-access/route.ts create mode 100644 app/api/billing/quotes/route.ts create mode 100644 app/api/feedback/route.ts create mode 100644 app/api/hosts/pairing/cancel/route.ts create mode 100644 app/api/hosts/pairing/route.ts create mode 100644 app/api/hosts/revoke/route.ts create mode 100644 app/api/internal/relay/authorization-delta/route.ts create mode 100644 app/api/internal/relay/authorization-snapshot/route.ts create mode 100644 app/api/internal/relay/authorize-device/route.ts create mode 100644 app/api/internal/relay/authorize-phone/route.ts create mode 100644 app/api/internal/relay/complete-phone-handoff/route.ts create mode 100644 app/api/internal/relay/consume-phone-handoff/route.ts create mode 100644 app/api/internal/relay/heartbeat/route.ts create mode 100644 app/api/internal/relay/migrations/advance/route.ts create mode 100644 app/api/internal/relay/purges/advance/route.ts create mode 100644 app/api/internal/relay/register-device/route.ts create mode 100644 app/api/internal/relay/resolve-device-route/route.ts create mode 100644 app/api/internal/relay/resolve-handoff-route/route.ts create mode 100644 app/api/internal/relay/resolve-phone-route/route.ts create mode 100644 app/api/internal/relay/resolve-tenant-route/route.ts create mode 100644 app/api/internal/relay/revoke-phone/route.ts create mode 100644 app/api/pwa/handoff/route.ts create mode 100644 app/api/respond.ts create mode 100644 app/api/status/route.ts create mode 100644 app/chatgpt-auth.ts create mode 100644 app/cloud-auth.ts create mode 100644 app/components/ConnectivityBanner.tsx create mode 100644 app/components/RouteStates.tsx create mode 100644 app/components/Shells.tsx create mode 100644 app/daemon-release.ts create mode 100644 app/dashboard/OpenPwaButton.tsx create mode 100644 app/dashboard/billing/AccessRequestForm.tsx create mode 100644 app/dashboard/billing/page.tsx create mode 100644 app/dashboard/connection-copy.ts create mode 100644 app/dashboard/error.tsx create mode 100644 app/dashboard/feedback/FeedbackForm.tsx create mode 100644 app/dashboard/feedback/page.tsx create mode 100644 app/dashboard/hosts/HostRevokeButton.tsx create mode 100644 app/dashboard/hosts/PairingCancelButton.tsx create mode 100644 app/dashboard/hosts/new/PairingForm.tsx create mode 100644 app/dashboard/hosts/new/onboarding.ts create mode 100644 app/dashboard/hosts/new/page.tsx create mode 100644 app/dashboard/hosts/page.tsx create mode 100644 app/dashboard/loading.tsx create mode 100644 app/dashboard/onboarding.ts create mode 100644 app/dashboard/page.tsx create mode 100644 app/dashboard/security/AccountLifecycleActions.tsx create mode 100644 app/dashboard/security/page.tsx create mode 100644 app/download/page.tsx create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/pricing/page.tsx create mode 100644 app/privacy/data-inventory.ts create mode 100644 app/privacy/page.tsx create mode 100644 app/readiness/page.tsx create mode 100644 app/status/error.tsx create mode 100644 app/status/loading.tsx create mode 100644 app/status/page.tsx create mode 100644 app/trust/page.tsx create mode 100644 build/sites-vite-plugin.ts create mode 100644 db/access-boundary.ts create mode 100644 db/access-requests.ts create mode 100644 db/beta-operations.ts create mode 100644 db/bootstrap.ts create mode 100644 db/device-claim.ts create mode 100644 db/device-control-plane.ts create mode 100644 db/domain-error.ts create mode 100644 db/domain.ts create mode 100644 db/index.ts create mode 100644 db/invitations.ts create mode 100644 db/launch-gates.ts create mode 100644 db/pairing.ts create mode 100644 db/registration-replay.ts create mode 100644 db/relay-authorization.ts create mode 100644 db/relay-control-plane.ts create mode 100644 db/relay-control-sql.ts create mode 100644 db/relay-migration-state.ts create mode 100644 db/relay-migrations.ts create mode 100644 db/relay-node-identity.ts create mode 100644 db/relay-purge-proof.ts create mode 100644 db/relay-purges.ts create mode 100644 db/relay-routing.ts create mode 100644 db/repository.ts create mode 100644 db/retention-runner.ts create mode 100644 db/retention.ts create mode 100644 db/schema.ts create mode 100644 db/service-status.ts create mode 100644 docs/beta-operations.md create mode 100644 docs/commercial-contract.md create mode 100644 docs/daemon-distribution.md create mode 100644 docs/daemon-relay-handoff.md create mode 100644 docs/data-inventory.md create mode 100644 docs/implementation-plan.md create mode 100644 docs/launch-gates.md create mode 100644 docs/provisioning-worker.md create mode 100644 docs/relay-operations.md create mode 100644 docs/retention-maintenance.md create mode 100644 docs/security-headers.md create mode 100644 docs/state-model.md create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_condemned_legion.sql create mode 100644 drizzle/0001_mushy_vance_astro.sql create mode 100644 drizzle/0002_wild_ravenous.sql create mode 100644 drizzle/0003_medical_rocket_racer.sql create mode 100644 drizzle/0004_loud_prodigy.sql create mode 100644 drizzle/0005_pale_corsair.sql create mode 100644 drizzle/0006_clever_shocker.sql create mode 100644 drizzle/0007_zippy_nomad.sql create mode 100644 drizzle/0008_far_justice.sql create mode 100644 drizzle/0009_flat_robbie_robertson.sql create mode 100644 drizzle/0010_windy_toxin.sql create mode 100644 drizzle/0011_next_thunderball.sql create mode 100644 drizzle/0012_shared_relay_control_plane.sql create mode 100644 drizzle/0013_relay_migration_fencing.sql create mode 100644 drizzle/0014_relay_tenant_purge.sql create mode 100644 drizzle/0015_phone_handoff_idempotency.sql create mode 100644 drizzle/0016_phone_handoff_activation.sql create mode 100644 drizzle/9000_provisioning_invariants.sql create mode 100644 drizzle/9001_provisioning_slug_backfill.sql create mode 100644 drizzle/9002_ready_credential_reconciliation.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 drizzle/meta/0005_snapshot.json create mode 100644 drizzle/meta/0006_snapshot.json create mode 100644 drizzle/meta/0007_snapshot.json create mode 100644 drizzle/meta/0008_snapshot.json create mode 100644 drizzle/meta/0009_snapshot.json create mode 100644 drizzle/meta/0010_snapshot.json create mode 100644 drizzle/meta/0011_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 eslint.config.mjs create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/favicon.svg create mode 100644 public/og.png create mode 100644 relay/cmd/relay/main.go create mode 100644 relay/go.mod create mode 100644 relay/go.sum create mode 100644 relay/internal/attachmentstore/store.go create mode 100644 relay/internal/auditsink/audit.go create mode 100644 relay/internal/authsnapshot/snapshot.go create mode 100644 relay/internal/authsnapshot/snapshot_test.go create mode 100644 relay/internal/config/config.go create mode 100644 relay/internal/config/config_test.go create mode 100644 relay/internal/controlplane/client.go create mode 100644 relay/internal/forwarder/forwarder.go create mode 100644 relay/internal/forwarder/forwarder_test.go create mode 100644 relay/internal/migration/manager.go create mode 100644 relay/internal/migration/manager_test.go create mode 100644 relay/internal/purge/manager.go create mode 100644 relay/internal/purge/manager_test.go create mode 100644 relay/internal/pushsink/pushsink.go create mode 100644 relay/internal/registry/registry.go create mode 100644 relay/internal/registry/registry_test.go create mode 100644 relay/internal/service/service.go create mode 100644 relay/internal/service/service_test.go create mode 100644 relay/internal/tenantbackup/backup.go create mode 100644 relay/internal/tenantbackup/backup_test.go create mode 100644 relay/internal/tenantfs/paths.go create mode 100644 relay/internal/tenantfs/paths_test.go create mode 100644 relay/internal/tenantpurge/purge.go create mode 100644 relay/internal/tenantpurge/purge_test.go create mode 100644 relay/internal/tenantstore/db.go create mode 100644 relay/internal/tenantstore/messages.go create mode 100644 relay/internal/tenantstore/permissions.go create mode 100644 relay/internal/tenantstore/phone.go create mode 100644 relay/internal/tenantstore/principals.go create mode 100644 relay/internal/tenantstore/prompt_commands.go create mode 100644 relay/internal/tenantstore/push.go create mode 100644 relay/internal/tenantstore/util.go create mode 100644 release/daemon-policy.mjs create mode 100644 release/daemon-version.ts create mode 100644 release/verify-daemon-release-core.mjs create mode 100644 tests/access-boundary.test.mjs create mode 100644 tests/access-requests.test.mjs create mode 100644 tests/beta-access-operations.test.mjs create mode 100644 tests/beta-onboarding.test.mjs create mode 100644 tests/beta-policy-controls.test.mjs create mode 100644 tests/daemon-onboarding.test.mjs create mode 100644 tests/daemon-release.test.mjs create mode 100644 tests/device-control-plane.test.mjs create mode 100644 tests/invitations.test.mjs create mode 100644 tests/pairing-progress.test.mjs create mode 100644 tests/privacy-inventory.test.mjs create mode 100644 tests/public-beta-access-reconciliation.test.mjs create mode 100644 tests/relay-control-plane.test.mjs create mode 100644 tests/release-verifier.test.mjs create mode 100644 tests/rendered-html.test.mjs create mode 100644 tests/retention-maintenance.test.mjs create mode 100644 tests/security-headers.test.mjs create mode 100644 tools/verify-daemon-release.mjs create mode 100644 tsconfig.json create mode 100644 types/cloudflare-env.d.ts create mode 100644 types/raw.d.ts create mode 100644 vite.config.ts create mode 100644 worker/index.ts create mode 100644 worker/security-headers.ts diff --git a/.github/workflows/cloud-pwa.yml b/.github/workflows/cloud-pwa.yml new file mode 100644 index 0000000..1848eef --- /dev/null +++ b/.github/workflows/cloud-pwa.yml @@ -0,0 +1,114 @@ +name: Build pinned Cloud PWA + +on: + workflow_dispatch: + inputs: + nekonest_commit: + description: Immutable 40-character NekoNest commit SHA + required: true + type: string + connect_origin: + description: Stable HTTPS Connect origin + required: true + type: string + +permissions: + contents: read + +concurrency: + group: cloud-pwa-${{ inputs.nekonest_commit }} + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Validate immutable inputs + shell: pwsh + env: + NEKONEST_COMMIT: ${{ inputs.nekonest_commit }} + CONNECT_ORIGIN: ${{ inputs.connect_origin }} + run: | + if ($env:NEKONEST_COMMIT -notmatch '^[0-9a-f]{40}$') { + throw 'nekonest_commit must be a full immutable SHA' + } + $origin = [Uri]$env:CONNECT_ORIGIN + if ($origin.Scheme -ne 'https' -or $origin.AbsoluteUri.TrimEnd('/') -ne $env:CONNECT_ORIGIN.TrimEnd('/')) { + throw 'connect_origin must be an exact HTTPS origin' + } + if ($origin.UserInfo -or $origin.PathAndQuery -ne '/' -or $origin.Fragment) { + throw 'connect_origin must not contain credentials, path, query, or fragment' + } + + - name: Check out exact NekoNest source + uses: actions/checkout@v7 + with: + repository: klarkxy/nekonest + ref: ${{ inputs.nekonest_commit }} + fetch-depth: 0 + path: nekonest + + - name: Verify checked-out source did not move + shell: pwsh + working-directory: nekonest + env: + EXPECTED_SHA: ${{ inputs.nekonest_commit }} + run: | + $actual = (git rev-parse HEAD).Trim() + if ($actual -ne $env:EXPECTED_SHA) { throw "expected $env:EXPECTED_SHA, got $actual" } + + - name: Set up pnpm + uses: pnpm/action-setup@v6 + with: + version: 10.29.2 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: pnpm + cache-dependency-path: nekonest/pwa/pnpm-lock.yaml + + - name: Install, test, and build managed PWA + working-directory: nekonest/pwa + env: + VITE_NEKONEST_MANAGED: 'true' + run: | + pnpm install --frozen-lockfile + pnpm test + pnpm type-check + pnpm build + + - name: Write deploy-time stable endpoint config + shell: pwsh + env: + CONNECT_ORIGIN: ${{ inputs.connect_origin }} + run: | + $config = [ordered]@{ + api_base = $env:CONNECT_ORIGIN.TrimEnd('/') + ws_base = $env:CONNECT_ORIGIN.TrimEnd('/').Replace('https://', 'wss://') + attachment_base = $env:CONNECT_ORIGIN.TrimEnd('/') + push_base = $env:CONNECT_ORIGIN.TrimEnd('/') + managed = $true + handoff_exchange_path = '/api/pwa/handoff/exchange' + } + $config | ConvertTo-Json -Compress | Set-Content -Encoding utf8NoBOM nekonest/pwa/dist/runtime-config.json + + - name: Record source provenance + shell: pwsh + env: + SOURCE_SHA: ${{ inputs.nekonest_commit }} + run: | + [ordered]@{ + repository = 'https://github.com/klarkxy/nekonest' + commit = $env:SOURCE_SHA + built_at = (Get-Date).ToUniversalTime().ToString('o') + } | ConvertTo-Json -Compress | Set-Content -Encoding utf8NoBOM nekonest/pwa/dist/source-provenance.json + + - name: Upload exact-build artifact + uses: actions/upload-artifact@v7 + with: + name: nekonest-cloud-pwa-${{ inputs.nekonest_commit }} + path: nekonest/pwa/dist + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/cloud-relay.yml b/.github/workflows/cloud-relay.yml new file mode 100644 index 0000000..5939c55 --- /dev/null +++ b/.github/workflows/cloud-relay.yml @@ -0,0 +1,81 @@ +name: Verify pinned Cloud Relay + +on: + workflow_dispatch: + inputs: + relay_core_tag: + description: Exact published Relay Core tag, for example relaycore/v0.1.0 + required: true + type: string + +permissions: + contents: read + +concurrency: + group: cloud-relay-${{ inputs.relay_core_tag }} + cancel-in-progress: false + +jobs: + verify: + runs-on: ubuntu-latest + defaults: + run: + working-directory: relay + env: + GOWORK: 'off' + CGO_ENABLED: '1' + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-go@v6 + with: + go-version-file: relay/go.mod + cache-dependency-path: relay/go.sum + + - name: Require an immutable released Relay Core dependency + shell: pwsh + env: + RELAY_CORE_TAG: ${{ inputs.relay_core_tag }} + run: | + if ($env:RELAY_CORE_TAG -notmatch '^relaycore/v[0-9]+\.[0-9]+\.[0-9]+$') { + throw 'relay_core_tag must be an exact relaycore/vX.Y.Z tag' + } + if (Select-String -Path go.mod -Pattern '^replace\s' -Quiet) { + throw 'Cloud Relay go.mod must not contain replace directives' + } + $expected = $env:RELAY_CORE_TAG.Substring('relaycore/'.Length) + $actual = (go list -m -f '{{.Version}}' github.com/klarkxy/nekonest/relaycore).Trim() + if ($actual -ne $expected) { + throw "go.mod requires $actual but workflow requested $expected" + } + go mod download + + - name: Test, vet, race, and build + run: | + go test -count=1 ./... + go vet ./... + go test -race -count=1 ./... + go build -trimpath -o ../release/nekonest-cloud-relay ./cmd/relay + + - name: Record source provenance + shell: pwsh + env: + RELAY_CORE_TAG: ${{ inputs.relay_core_tag }} + run: | + $cloudCommit = (git -C .. rev-parse HEAD).Trim() + $binaryHash = (Get-FileHash ../release/nekonest-cloud-relay -Algorithm SHA256).Hash.ToLowerInvariant() + [ordered]@{ + cloud_commit = $cloudCommit + relay_core_tag = $env:RELAY_CORE_TAG + binary_sha256 = $binaryHash + built_at = (Get-Date).ToUniversalTime().ToString('o') + } | ConvertTo-Json -Compress | Set-Content -Encoding utf8NoBOM ../release/cloud-relay-provenance.json + + - uses: actions/upload-artifact@v7 + with: + name: nekonest-cloud-relay-${{ github.run_id }} + path: | + release/nekonest-cloud-relay + release/cloud-relay-provenance.json + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a39c94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.npm-cache +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/.vinext/ +/out/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +next-env.d.ts +*.tsbuildinfo +/dist/ +/.wrangler/ +/outputs/ +/work/ +/.codegraph/ diff --git a/.openai/hosting.json b/.openai/hosting.json new file mode 100644 index 0000000..01e09ac --- /dev/null +++ b/.openai/hosting.json @@ -0,0 +1,4 @@ +{ + "d1": "DB", + "r2": null +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a8efb7 --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# NekoNest Cloud + +NekoNest Cloud is the private commercial control plane and managed Relay for +the open-source NekoNest data plane. Self-hosted NekoNest remains independent: +it does not contact this repository, read subscriptions, or display Cloud +concepts. + +The managed architecture is deliberately not “one Docker Server per customer”: + +```text +Daemon / Cloud PWA + -> stable Connect origin + -> regional Cloud Relay + -> TenantEngineRegistry + -> one isolated relaycore.Engine per tenant + -> per-tenant SQLite and attachment root + +Cloud control plane + -> account, entitlement, host slots, placement, authorization revisions + -> short-lived signed authorization snapshots for Relay nodes +``` + +One account maps to one tenant/Nest. A host slot is consumed by one non-revoked +host identity, whether it is online or not. A copied device credential remains +the same identity and the managed Relay rejects a second simultaneous daemon; +software cannot prove that a fully copied private key resides on a different +physical machine. + +## Current commercial policy + +- Open-source self-hosting remains free. +- Cloud is a closed, free test while operational and legal gates remain open. +- No quote, order, payment, balance, automatic charge, or token resale path is + enabled. +- The existing entitlement/capacity source is authoritative for host slots. + Registering the N+1 non-revoked identity returns + `device_capacity_exceeded`; revoking a host releases its slot. +- Lowering capacity below the active host count is rejected until the user + explicitly revokes hosts. Cloud never chooses a device to evict. +- Price, billing period, refund, subscription-expiry, and payment-provider + policy remain outside this implementation. + +See [`docs/commercial-contract.md`](./docs/commercial-contract.md) and +[`docs/launch-gates.md`](./docs/launch-gates.md). + +## Implemented control-plane contracts + +- Account, beta entitlement, host-slot, pairing, claim, device credential, + revocation, audit, feedback, retention, and account-exit domain state in D1. +- Atomic host claiming: an identity retry is idempotent and does not consume a + second slot; a different N+1 identity is refused. +- Stable daemon registration. `POST /api/devices/register` returns the existing + device fields plus `connection_state: ready | provisioning` and optional + `retry_after_seconds`; it never returns a tenant Relay URL. +- Regions, Relay nodes, tenant placement, placement generation, + authorization revision, opaque route handles, and single-use handoff tickets. +- Ed25519-signed canonical authorization snapshots with `kid`, issue/expiry, + tenant state, home region, target node, generation, revision, and active + credential digests. +- Relay-node-only internal APIs for live authorization, revision/delta polling, + route resolution, and heartbeat. Node calls use scoped identity; clients + cannot submit a raw tenant ID. +- A 60-second, origin-bound, single-use Dashboard-to-PWA handoff. D1 stores only + the ticket digest. Exchange creates an independent revocable `phone_id`, + `phone_token`, and non-authorizing opaque route handle. Completion first + records a non-authorizing pending principal; only the first request proving + possession of the exact token and route handle activates it and advances the + tenant authorization revision. A lost exchange response therefore cannot + leave an unused active phone identity. +- Login or handoff never grants a phone access to a host. The phone must still + complete NekoNest’s per-device E2E pairing and receive that device’s key + package. +- Stable ingress resolves the current placement for every new connection and + uses authenticated internal HTTPS/WSS forwarding when the home node is + elsewhere; clients never receive a node URL or redirect. +- Fenced backup/restore and migration primitives keep one writable generation. + Application-layer tenant purge closes the Engine, deletes live data, + attachments and all tenant backups, then clears active credentials only + after node acknowledgement. Its completion audit is written only after all + D1 postconditions hold, and the purge job's `completed` marker is the final + fenced mutation. + +The Cloud PWA is built from an exact NekoNest revision. It points at the stable +Connect origin through NekoNest runtime endpoint configuration and refuses any +managed endpoint that is not `sealed`. + +The manual [`cloud-pwa.yml`](./.github/workflows/cloud-pwa.yml) workflow accepts +only a full 40-character NekoNest commit SHA, verifies the checkout, runs the +PWA tests/type-check/build, writes the deploy-time Connect config, records the +source SHA, and emits an immutable build artifact. It does not deploy by itself. + +## Removed legacy design + +The unreleased activation poller, `/api/devices/bootstrap`, tenant +`relay_url` handoff, managed-device manifest, per-tenant Server container, and +single-VPS Node provisioner are intentionally removed. They are not a fallback +or a compatibility mode. A daemon with that old managed configuration must +re-register. + +## Local control-plane development + +Requires Node.js 22.13 or newer: + +```powershell +npm ci +npm run dev +``` + +Production control-plane configuration includes: + +- D1 binding `DB` from [`.openai/hosting.json`](./.openai/hosting.json); +- `NEKONEST_CLOUD_ADMIN_EMAILS` for the temporary closed-test admin boundary; +- `NEKONEST_CLOUD_CREDENTIAL_SECRET` with at least 32 random characters for + purpose-separated HMAC digests; +- Relay snapshot signing keys and active `kid`; +- independent Relay-node mTLS identities and an exact origin allowlist for the + Cloud PWA; +- the stable Connect and PWA origins used by registration and handoff. + +Sites/ChatGPT identity and the development demo viewer are closed-test +scaffolding, not the final public identity or account-recovery decision. + +## Daemon distribution + +The download catalog is fail closed. It accepts only an exact stable NekoNest +tag, exact platform assets, `checksums.txt`, and configured SHA-256 digests; it +never follows `latest`. The compatible daemon must implement protocol 1.3, +stable `server_url`, `ready | provisioning`, and structured service errors. +Artifact hashes do not replace publisher/code signing. + +See [`docs/daemon-distribution.md`](./docs/daemon-distribution.md). + +Relay deployment, internal forwarding, migration and purge configuration are +documented in [`docs/relay-operations.md`](./docs/relay-operations.md). +The manual [`cloud-relay.yml`](./.github/workflows/cloud-relay.yml) workflow +rejects local `replace` directives, verifies the requested `relaycore/vX.Y.Z` +tag against `go.mod`, and runs tests, vet, race checks, and a provenance build +with `GOWORK=off`. + +## Database + +Drizzle schema is in [`db/schema.ts`](./db/schema.ts), with append-only +migrations under [`drizzle/`](./drizzle/). Runtime migration tracking uses +`cloud_schema_migrations`. + +```powershell +npm run db:generate +``` + +Never rewrite a migration already applied to a D1 environment. Because this +repository has not yet served production users, a disposable environment may +be recreated; any non-disposable D1 database must be exported before migration. + +## Verification + +```powershell +npm run type-check +npm run lint +npm test + +cd relay +go test -count=1 ./... +go vet ./... +``` + +The Go Relay has its own tests, race checks, and resource-leak checks. A local +green build is not public-service acceptance. Paid/public launch stays blocked +until exact-build deployment proves stable ingress, cross-tenant isolation, +sealed message and attachment paths, 15-second revocation, five-minute snapshot +expiry, backup/restore, deletion/purge, regional migration and rollback, +monitoring/on-call, retention, identity, domain/filing, and payment policy. diff --git a/app/admin/AdminActions.tsx b/app/admin/AdminActions.tsx new file mode 100644 index 0000000..f464ed9 --- /dev/null +++ b/app/admin/AdminActions.tsx @@ -0,0 +1,384 @@ +"use client"; + +import { useState } from "react"; +import type { + AccountRecord, + AccountDeletionRequestRecord, + BetaAccessRequestRecord, + BetaRecord, + FeedbackRecord, + GrantRecord, + LaunchGateRecord, + RetentionMaintenanceResult, + ServiceIncidentRecord, +} from "@/db/repository"; + +type SubmitState = { loading: boolean; message: string; error: boolean }; +const idle: SubmitState = { loading: false, message: "", error: false }; + +async function postJson = Record>(path: string, payload: Record): Promise { + const response = await fetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...payload, idempotencyKey: crypto.randomUUID() }) }); + const body = (await response.json()) as T & { message?: string }; + if (!response.ok) throw new Error(body.message || "操作失败"); + return body; +} + +export function BetaAction({ beta, blockedP0 }: { beta: BetaRecord | null; blockedP0: number }) { + const [enabled, setEnabled] = useState(beta?.state === "active"); + const [capacity, setCapacity] = useState(beta?.capacity_slots?.toString() ?? ""); + const [reason, setReason] = useState(""); + const [state, setState] = useState(idle); + async function submit(event: React.FormEvent) { + event.preventDefault(); setState({ loading: true, message: "", error: false }); + try { await postJson("/api/admin/beta", { enabled, capacitySlots: capacity ? Number(capacity) : null, reason }); setState({ loading: false, message: "新的公测政策版本已写入,页面即将刷新。", error: false }); window.setTimeout(() => window.location.reload(), 700); } + catch (error) { setState({ loading: false, message: error instanceof Error ? error.message : "操作失败", error: true }); } + } + return
{blockedP0 > 0 &&

仍有 {blockedP0} 项 P0 未通过:可以预设免费政策,但新的公开配对继续冻结;只有明确签发的闭测邀请可继续使用。

}