feat: establish NekoNest Cloud control and relay

This commit is contained in:
2026-08-12 23:25:43 +08:00
commit f27606b709
222 changed files with 71456 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
export type FreeBetaBoundaryOperation =
| "create_pairing"
| "claim_pairing"
| "cancel_pairing"
| "connect_device"
| "revoke_host";
export const FREE_BETA_ACCESS_BOUNDARY: ReadonlyArray<{
operation: FreeBetaBoundaryOperation;
requiresCurrentEntitlement: boolean;
preservesExistingHost: boolean;
}> = [
{ operation: "create_pairing", requiresCurrentEntitlement: true, preservesExistingHost: true },
{ operation: "claim_pairing", requiresCurrentEntitlement: true, preservesExistingHost: true },
{ operation: "cancel_pairing", requiresCurrentEntitlement: false, preservesExistingHost: true },
{ operation: "connect_device", requiresCurrentEntitlement: false, preservesExistingHost: true },
{ operation: "revoke_host", requiresCurrentEntitlement: false, preservesExistingHost: false },
] as const;
/**
* Existing claimed devices remain authenticated after a free policy or invite
* ends. Entitlement gates new pairing and claim operations instead of silently
* revoking an already issued device credential.
*/
export const AUTHENTICATE_ACTIVE_DEVICE_SQL = `
UPDATE device_credentials
SET last_used_at = ?1
WHERE host_id = ?2 AND token_hash = ?3 AND status = 'active'
AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?1)
AND EXISTS (
SELECT 1 FROM hosts
WHERE id = ?2 AND lifecycle = 'active' AND slot_state = 'active'
)
`;
/** Security exits intentionally have no entitlement predicate. */
export const REVOKE_ACTIVE_DEVICE_CREDENTIALS_SQL = `
UPDATE device_credentials
SET status = 'revoked', revoked_at = ?1
WHERE host_id = ?2 AND status = 'active'
`;
export const DEACTIVATE_OWNED_HOST_SQL = `
UPDATE hosts
SET lifecycle = 'deactivated', slot_state = 'released',
connection_state = 'offline', deactivated_at = ?1
WHERE id = ?2 AND account_id = ?3 AND lifecycle = 'active'
`;
+190
View File
@@ -0,0 +1,190 @@
import { PUBLIC_BETA_GATE_READY_SQL } from "./launch-gates.ts";
export type BetaAccessRequestStatus =
| "requested"
| "approved"
| "declined"
| "cancelled";
/**
* Reserve one account-level pending request by first reserving its replay key.
* Parameters: scope, key, hash, response JSON, expiry, now, account id.
*/
export const CREATE_ACCESS_REQUEST_IDEMPOTENCY_SQL = `
INSERT INTO idempotency_records
(scope, key, request_hash, response_json, status_code, expires_at, created_at)
SELECT ?1, ?2, ?3, ?4, 201, ?5, ?6
WHERE NOT EXISTS (
SELECT 1 FROM beta_access_requests
WHERE account_id = ?7 AND status = 'requested'
)
AND NOT EXISTS (
SELECT 1 FROM entitlement_grants
WHERE account_id = ?7 AND state = 'active' AND starts_at <= ?6
AND (ends_at IS NULL OR ends_at > ?6) AND revoked_at IS NULL
)
AND NOT EXISTS (
SELECT 1 FROM beta_programs
WHERE state = 'active' AND starts_at <= ?6
AND (ends_at IS NULL OR ends_at > ?6)
AND ${PUBLIC_BETA_GATE_READY_SQL}
)
`;
/** Parameters: id, account, OS, slots, use case, now, scope, key, hash. */
export const CREATE_ACCESS_REQUEST_SQL = `
INSERT INTO beta_access_requests
(id, account_id, status, preferred_os, requested_slots, use_case,
requested_at, created_at, updated_at)
SELECT ?1, ?2, 'requested', ?3, ?4, ?5, ?6, ?6, ?6
WHERE EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?7 AND key = ?8 AND request_hash = ?9
)
`;
/**
* Reserve cancellation only while the same account still owns a pending row.
* Parameters: scope, key, hash, response JSON, expiry, now, request id, account.
*/
export const CREATE_ACCESS_CANCELLATION_IDEMPOTENCY_SQL = `
INSERT INTO idempotency_records
(scope, key, request_hash, response_json, status_code, expires_at, created_at)
SELECT ?1, ?2, ?3, ?4, 200, ?5, ?6
FROM beta_access_requests
WHERE id = ?7 AND account_id = ?8 AND status = 'requested'
`;
/** Parameters: now, request id, account, scope, key, hash. */
export const CANCEL_ACCESS_REQUEST_SQL = `
UPDATE beta_access_requests
SET status = 'cancelled', cancelled_at = ?1, updated_at = ?1
WHERE id = ?2 AND account_id = ?3 AND status = 'requested'
AND EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?4 AND key = ?5 AND request_hash = ?6
)
`;
/**
* Reserve an administrator decision only while the request is pending.
* Parameters: scope, key, hash, response JSON, status code, expiry, now, request.
*/
export const CREATE_ACCESS_RESOLUTION_IDEMPOTENCY_SQL = `
INSERT INTO idempotency_records
(scope, key, request_hash, response_json, status_code, expires_at, created_at)
SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7
FROM beta_access_requests
WHERE id = ?8 AND status = 'requested'
`;
/**
* Create the non-monetary invitation for an approved request.
* Parameters: grant id, request id/source ref, capacity, now, end, reason,
* actor, request id, scope, key, hash.
*/
export const CREATE_APPROVED_INVITATION_SQL = `
INSERT INTO entitlement_grants
(id, account_id, host_id, source, source_ref, capacity_slots,
starts_at, ends_at, state, reason, created_by, created_at)
SELECT ?1, account_id, NULL, 'admin_exemption', ?2, ?3,
?4, ?5, 'active', ?6, ?7, ?4
FROM beta_access_requests
WHERE id = ?8 AND status = 'requested'
AND EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?9 AND key = ?10 AND request_hash = ?11
)
`;
/** Parameters: response, actor, grant id, now, request id, scope, key, hash. */
export const APPROVE_ACCESS_REQUEST_SQL = `
UPDATE beta_access_requests
SET status = 'approved', admin_response = ?1, resolved_by = ?2,
invitation_grant_id = ?3, resolved_at = ?4, updated_at = ?4
WHERE id = ?5 AND status = 'requested'
AND EXISTS (SELECT 1 FROM entitlement_grants WHERE id = ?3)
AND EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?6 AND key = ?7 AND request_hash = ?8
)
`;
/** Parameters: response, actor, now, request id, scope, key, hash. */
export const DECLINE_ACCESS_REQUEST_SQL = `
UPDATE beta_access_requests
SET status = 'declined', admin_response = ?1, resolved_by = ?2,
resolved_at = ?3, updated_at = ?3
WHERE id = ?4 AND status = 'requested'
AND EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?5 AND key = ?6 AND request_hash = ?7
)
`;
/**
* Manual invitations are only for proactive invitations. When an account has
* a pending request, administrators must resolve that request so user-visible
* state and the grant cannot diverge.
* Parameters: scope, key, hash, response JSON, expiry, now, account id.
*/
export const CREATE_MANUAL_INVITATION_IDEMPOTENCY_SQL = `
INSERT INTO idempotency_records
(scope, key, request_hash, response_json, status_code, expires_at, created_at)
SELECT ?1, ?2, ?3, ?4, 201, ?5, ?6
WHERE NOT EXISTS (
SELECT 1 FROM beta_access_requests
WHERE account_id = ?7 AND status = 'requested'
)
`;
/** Parameters: grant fields followed by scope, key and hash. */
export const CREATE_MANUAL_INVITATION_SQL = `
INSERT INTO entitlement_grants
(id, account_id, host_id, source, source_ref, capacity_slots,
starts_at, ends_at, state, reason, created_by, created_at)
SELECT ?1, ?2, NULL, 'admin_exemption', ?3, ?4,
?5, ?6, 'active', ?7, ?8, ?9
WHERE EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?10 AND key = ?11 AND request_hash = ?12
)
`;
export const PUBLIC_BETA_ACCESS_RESPONSE =
"公开免费测试已经开放,当前不再需要单独闭测邀请。";
/**
* Close stale pending requests only when an active public beta and every P0
* evidence gate are simultaneously true inside the same D1 batch.
* Parameters: user-facing response, actor, now.
*/
export const FULFILL_ACCESS_REQUESTS_BY_PUBLIC_BETA_SQL = `
UPDATE beta_access_requests
SET status = 'approved', admin_response = ?1, resolved_by = ?2,
invitation_grant_id = NULL, resolved_at = ?3, updated_at = ?3
WHERE status = 'requested'
AND EXISTS (
SELECT 1 FROM beta_programs
WHERE state = 'active' AND starts_at <= ?3
AND (ends_at IS NULL OR ends_at > ?3)
AND ${PUBLIC_BETA_GATE_READY_SQL}
)
`;
/** Parameters: actor, response, correlation id, now. */
export const AUDIT_PUBLIC_BETA_ACCESS_FULFILLMENT_SQL = `
INSERT OR IGNORE INTO audit_events
(id, actor_id, action, target_type, target_id, reason,
before_json, after_json, correlation_id, created_at)
SELECT 'audit_public_beta_' || substr(id, 8), ?1,
'beta_access.fulfilled_by_public_beta', 'beta_access_request', id,
'公开免费测试开放,待审申请无需单独邀请',
'{"status":"requested"}',
json_object('status', 'approved', 'adminResponse', ?2,
'invitationGrantId', NULL),
?3, ?4
FROM beta_access_requests
WHERE status = 'approved' AND resolved_by = ?1 AND resolved_at = ?4
AND invitation_grant_id IS NULL AND admin_response = ?2
`;
+268
View File
@@ -0,0 +1,268 @@
export const BETA_OPERATIONS_WINDOW_DAYS = 30;
export const BETA_ACCOUNTS_SQL = `
SELECT
COUNT(*) AS total,
SUM(CASE WHEN created_at >= ? THEN 1 ELSE 0 END) AS new_accounts,
SUM(CASE WHEN EXISTS (
SELECT 1 FROM hosts
WHERE hosts.account_id = accounts.id AND hosts.lifecycle = 'active'
) THEN 1 ELSE 0 END) AS with_active_host
FROM accounts`;
export const BETA_PAIRINGS_SQL = `
SELECT
COUNT(*) AS created,
SUM(CASE WHEN status = 'claimed' THEN 1 ELSE 0 END) AS claimed,
SUM(CASE WHEN status = 'waiting' AND expires_at > ? THEN 1 ELSE 0 END) AS waiting,
SUM(CASE WHEN status = 'waiting' AND expires_at <= ? THEN 1 ELSE 0 END) AS expired,
SUM(CASE
WHEN status = 'waiting' AND (expires_at <= ? OR locked_at IS NOT NULL)
THEN 1 ELSE 0
END) AS attention_required,
SUM(CASE WHEN locked_at IS NOT NULL THEN 1 ELSE 0 END) AS locked,
AVG(CASE
WHEN status = 'claimed' AND claimed_at IS NOT NULL
THEN (julianday(claimed_at) - julianday(created_at)) * 86400
END) AS average_claim_seconds
FROM pairing_requests
WHERE created_at >= ?`;
export const BETA_CLAIM_ATTEMPTS_SQL = `
SELECT
SUM(CASE WHEN outcome = 'rejected' THEN 1 ELSE 0 END) AS rejected,
SUM(CASE WHEN outcome = 'rate_limited' THEN 1 ELSE 0 END) AS rate_limited
FROM pairing_claim_attempts
WHERE created_at >= ?`;
export const BETA_PROVISIONING_SQL = `
SELECT
COUNT(*) AS created,
SUM(CASE WHEN state = 'active' THEN 1 ELSE 0 END) AS succeeded,
SUM(CASE WHEN state = 'failed' THEN 1 ELSE 0 END) AS failed,
SUM(CASE WHEN state IN ('provisioning', 'quiescing', 'copying', 'switching', 'draining') THEN 1 ELSE 0 END) AS in_progress,
AVG(CASE
WHEN state = 'active'
THEN (julianday(updated_at) - julianday(created_at)) * 86400
END) AS average_completion_seconds
FROM tenant_placements
WHERE created_at >= ?`;
export const BETA_SUPPORT_SQL = `
SELECT
COUNT(*) AS created,
SUM(CASE WHEN category = 'connection_issue' THEN 1 ELSE 0 END) AS connection_issues,
SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) AS open,
SUM(CASE WHEN status = 'resolved' THEN 1 ELSE 0 END) AS resolved,
AVG(CASE
WHEN status = 'resolved' AND resolved_at IS NOT NULL
THEN (julianday(resolved_at) - julianday(created_at)) * 86400
END) AS average_resolution_seconds
FROM beta_feedback
WHERE created_at >= ?`;
export const BETA_ACCESS_REQUESTS_SQL = `
SELECT
COUNT(*) AS submitted,
SUM(requested_slots) AS requested_slot_demand,
AVG(requested_slots) AS average_requested_slots,
SUM(CASE WHEN preferred_os = 'windows' THEN 1 ELSE 0 END) AS windows,
SUM(CASE WHEN preferred_os = 'linux' THEN 1 ELSE 0 END) AS linux,
SUM(CASE WHEN preferred_os = 'both' THEN 1 ELSE 0 END) AS both,
SUM(CASE WHEN status = 'requested' THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) AS approved,
SUM(CASE WHEN status = 'declined' THEN 1 ELSE 0 END) AS declined,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
SUM(CASE
WHEN status = 'approved' AND resolved_at IS NOT NULL AND EXISTS (
SELECT 1 FROM hosts
WHERE hosts.account_id = beta_access_requests.account_id
AND hosts.claimed_at >= beta_access_requests.resolved_at
) THEN 1 ELSE 0
END) AS approved_with_post_approval_claim,
AVG(CASE
WHEN status IN ('approved', 'declined') AND resolved_at IS NOT NULL
THEN (julianday(resolved_at) - julianday(requested_at)) * 86400
END) AS average_review_seconds
FROM beta_access_requests
WHERE requested_at >= ?`;
type AggregateRow = Record<string, number | null>;
export type BetaOperationsSnapshot = {
generatedAt: string;
windowDays: number;
accounts: {
total: number;
newAccounts: number;
withActiveHost: number;
};
pairings: {
created: number;
claimed: number;
waiting: number;
expired: number;
attentionRequired: number;
locked: number;
rejectedAttempts: number;
rateLimitedAttempts: number;
claimRatePercent: number | null;
averageClaimSeconds: number | null;
};
provisioning: {
created: number;
succeeded: number;
failed: number;
inProgress: number;
successRatePercent: number | null;
averageCompletionSeconds: number | null;
};
support: {
created: number;
connectionIssues: number;
open: number;
resolved: number;
resolutionRatePercent: number | null;
averageResolutionSeconds: number | null;
};
accessRequests: {
submitted: number;
requestedSlotDemand: number;
averageRequestedSlots: number | null;
windows: number;
linux: number;
both: number;
pending: number;
approved: number;
declined: number;
cancelled: number;
decided: number;
approvalRatePercent: number | null;
averageReviewSeconds: number | null;
approvedWithPostApprovalClaim: number;
postApprovalClaimRatePercent: number | null;
};
unavailable: readonly [
"relay_reconnect_rate",
"relay_latency",
"runtime_resource_cost",
"support_effort",
];
};
function count(row: AggregateRow, key: string): number {
const value = Number(row[key] ?? 0);
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
}
function duration(row: AggregateRow, key: string): number | null {
const value = Number(row[key]);
return Number.isFinite(value) && value >= 0 ? Math.round(value) : null;
}
function average(row: AggregateRow, key: string): number | null {
const value = Number(row[key]);
return Number.isFinite(value) && value >= 0
? Math.round(value * 10) / 10
: null;
}
function percent(numerator: number, denominator: number): number | null {
if (denominator <= 0) return null;
return Math.round((numerator / denominator) * 1000) / 10;
}
export function deriveBetaOperationsSnapshot(input: {
generatedAt: string;
accounts: AggregateRow;
pairings: AggregateRow;
claimAttempts: AggregateRow;
provisioning: AggregateRow;
support: AggregateRow;
accessRequests: AggregateRow;
}): BetaOperationsSnapshot {
const pairingCreated = count(input.pairings, "created");
const pairingClaimed = count(input.pairings, "claimed");
const provisioningCreated = count(input.provisioning, "created");
const provisioningSucceeded = count(input.provisioning, "succeeded");
const supportCreated = count(input.support, "created");
const supportResolved = count(input.support, "resolved");
const accessSubmitted = count(input.accessRequests, "submitted");
const accessApproved = count(input.accessRequests, "approved");
const accessDeclined = count(input.accessRequests, "declined");
const accessDecided = accessApproved + accessDeclined;
const approvedWithPostApprovalClaim = count(
input.accessRequests,
"approved_with_post_approval_claim",
);
return {
generatedAt: input.generatedAt,
windowDays: BETA_OPERATIONS_WINDOW_DAYS,
accounts: {
total: count(input.accounts, "total"),
newAccounts: count(input.accounts, "new_accounts"),
withActiveHost: count(input.accounts, "with_active_host"),
},
pairings: {
created: pairingCreated,
claimed: pairingClaimed,
waiting: count(input.pairings, "waiting"),
expired: count(input.pairings, "expired"),
attentionRequired: count(input.pairings, "attention_required"),
locked: count(input.pairings, "locked"),
rejectedAttempts: count(input.claimAttempts, "rejected"),
rateLimitedAttempts: count(input.claimAttempts, "rate_limited"),
claimRatePercent: percent(pairingClaimed, pairingCreated),
averageClaimSeconds: duration(input.pairings, "average_claim_seconds"),
},
provisioning: {
created: provisioningCreated,
succeeded: provisioningSucceeded,
failed: count(input.provisioning, "failed"),
inProgress: count(input.provisioning, "in_progress"),
successRatePercent: percent(provisioningSucceeded, provisioningCreated),
averageCompletionSeconds: duration(
input.provisioning,
"average_completion_seconds",
),
},
support: {
created: supportCreated,
connectionIssues: count(input.support, "connection_issues"),
open: count(input.support, "open"),
resolved: supportResolved,
resolutionRatePercent: percent(supportResolved, supportCreated),
averageResolutionSeconds: duration(
input.support,
"average_resolution_seconds",
),
},
accessRequests: {
submitted: accessSubmitted,
requestedSlotDemand: count(input.accessRequests, "requested_slot_demand"),
averageRequestedSlots: average(input.accessRequests, "average_requested_slots"),
windows: count(input.accessRequests, "windows"),
linux: count(input.accessRequests, "linux"),
both: count(input.accessRequests, "both"),
pending: count(input.accessRequests, "pending"),
approved: accessApproved,
declined: accessDeclined,
cancelled: count(input.accessRequests, "cancelled"),
decided: accessDecided,
approvalRatePercent: percent(accessApproved, accessDecided),
averageReviewSeconds: duration(input.accessRequests, "average_review_seconds"),
approvedWithPostApprovalClaim,
postApprovalClaimRatePercent: percent(
approvedWithPostApprovalClaim,
accessApproved,
),
},
unavailable: [
"relay_reconnect_rate",
"relay_latency",
"runtime_resource_cost",
"support_effort",
],
};
}
+367
View File
@@ -0,0 +1,367 @@
import { env } from "cloudflare:workers";
import migrationSql from "../drizzle/0000_condemned_legion.sql?raw";
import claimMigrationSql from "../drizzle/0001_mushy_vance_astro.sql?raw";
import claimOwnershipMigrationSql from "../drizzle/0002_wild_ravenous.sql?raw";
import provisioningMigrationSql from "../drizzle/0003_medical_rocket_racer.sql?raw";
import provisioningFenceMigrationSql from "../drizzle/0004_loud_prodigy.sql?raw";
import feedbackMigrationSql from "../drizzle/0005_pale_corsair.sql?raw";
import serviceIncidentMigrationSql from "../drizzle/0006_clever_shocker.sql?raw";
import accountLifecycleMigrationSql from "../drizzle/0007_zippy_nomad.sql?raw";
import provisionerLivenessMigrationSql from "../drizzle/0008_far_justice.sql?raw";
import maintenanceJobMigrationSql from "../drizzle/0009_flat_robbie_robertson.sql?raw";
import betaAccessRequestMigrationSql from "../drizzle/0010_windy_toxin.sql?raw";
import betaAccessRequestTimeIndexMigrationSql from "../drizzle/0011_next_thunderball.sql?raw";
import sharedRelayControlPlaneMigrationSql from "../drizzle/0012_shared_relay_control_plane.sql?raw";
import relayMigrationFencingMigrationSql from "../drizzle/0013_relay_migration_fencing.sql?raw";
import relayTenantPurgeMigrationSql from "../drizzle/0014_relay_tenant_purge.sql?raw";
import phoneHandoffIdempotencyMigrationSql from "../drizzle/0015_phone_handoff_idempotency.sql?raw";
import phoneHandoffActivationMigrationSql from "../drizzle/0016_phone_handoff_activation.sql?raw";
import provisioningInvariantMigrationSql from "../drizzle/9000_provisioning_invariants.sql?raw";
import provisioningSlugMigrationSql from "../drizzle/9001_provisioning_slug_backfill.sql?raw";
import readyCredentialReconciliationMigrationSql from "../drizzle/9002_ready_credential_reconciliation.sql?raw";
import { LAUNCH_GATE_SEEDS } from "./launch-gates.ts";
let schemaPromise: Promise<void> | null = null;
function getDatabase(): D1Database {
if (!env.DB) {
throw new Error("Cloudflare D1 binding `DB` is unavailable.");
}
return env.DB;
}
function migrationStatements(sql: string): string[] {
return sql
.split("--> statement-breakpoint")
.map((statement) => statement.trim())
.filter(Boolean);
}
async function applyTrackedMigration(
db: D1Database,
id: string,
sql: string,
): Promise<void> {
const applied = await db
.prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?")
.bind(id)
.first<{ id: string }>();
if (applied) return;
const now = new Date().toISOString();
try {
await db.batch([
...migrationStatements(sql).map((statement) => db.prepare(statement)),
db
.prepare(
`INSERT INTO cloud_schema_migrations (id, applied_at)
VALUES (?, ?)`,
)
.bind(id, now),
]);
} catch (error) {
const racedMigration = await db
.prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?")
.bind(id)
.first<{ id: string }>();
if (!racedMigration) throw error;
}
}
async function migrateSchema(db: D1Database): Promise<void> {
let accountsTable = await db
.prepare(
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'accounts'",
)
.first<{ name: string }>();
if (!accountsTable) {
try {
await db.batch(
migrationStatements(migrationSql).map((statement) => db.prepare(statement)),
);
} catch (error) {
accountsTable = await db
.prepare(
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'accounts'",
)
.first<{ name: string }>();
if (!accountsTable) throw error;
}
}
const ledgerTable = await db
.prepare(
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'cloud_schema_migrations'",
)
.first<{ name: string }>();
const claimMigration = ledgerTable
? await db
.prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?")
.bind("0001_mushy_vance_astro")
.first<{ id: string }>()
: null;
if (!claimMigration) {
const statements = migrationStatements(claimMigrationSql);
const applicableStatements = ledgerTable ? statements.slice(1) : statements;
const now = new Date().toISOString();
try {
await db.batch([
...applicableStatements.map((statement) => db.prepare(statement)),
db
.prepare(
`INSERT OR IGNORE INTO cloud_schema_migrations (id, applied_at)
VALUES ('0000_condemned_legion', ?), ('0001_mushy_vance_astro', ?)`,
)
.bind(now, now),
]);
} catch (error) {
const racedMigration = await db
.prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?")
.bind("0001_mushy_vance_astro")
.first<{ id: string }>();
if (!racedMigration) throw error;
}
}
await applyTrackedMigration(
db,
"0002_wild_ravenous",
claimOwnershipMigrationSql,
);
await applyTrackedMigration(
db,
"0003_medical_rocket_racer",
provisioningMigrationSql,
);
await applyTrackedMigration(
db,
"0004_loud_prodigy",
provisioningFenceMigrationSql,
);
await applyTrackedMigration(db, "0005_pale_corsair", feedbackMigrationSql);
await applyTrackedMigration(
db,
"0006_clever_shocker",
serviceIncidentMigrationSql,
);
await applyTrackedMigration(
db,
"0007_zippy_nomad",
accountLifecycleMigrationSql,
);
await applyTrackedMigration(
db,
"0008_far_justice",
provisionerLivenessMigrationSql,
);
await applyTrackedMigration(
db,
"0009_flat_robbie_robertson",
maintenanceJobMigrationSql,
);
await applyTrackedMigration(
db,
"0010_windy_toxin",
betaAccessRequestMigrationSql,
);
await applyTrackedMigration(
db,
"0011_next_thunderball",
betaAccessRequestTimeIndexMigrationSql,
);
await applyTrackedMigration(
db,
"9000_provisioning_invariants",
provisioningInvariantMigrationSql,
);
await applyTrackedMigration(
db,
"9001_provisioning_slug_backfill",
provisioningSlugMigrationSql,
);
await applyTrackedMigration(
db,
"9002_ready_credential_reconciliation",
readyCredentialReconciliationMigrationSql,
);
await applyTrackedMigration(
db,
"0012_shared_relay_control_plane",
sharedRelayControlPlaneMigrationSql,
);
await applyTrackedMigration(
db,
"0013_relay_migration_fencing",
relayMigrationFencingMigrationSql,
);
await applyTrackedMigration(
db,
"0014_relay_tenant_purge",
relayTenantPurgeMigrationSql,
);
await applyTrackedMigration(
db,
"0015_phone_handoff_idempotency",
phoneHandoffIdempotencyMigrationSql,
);
await applyTrackedMigration(
db,
"0016_phone_handoff_activation",
phoneHandoffActivationMigrationSql,
);
await db.prepare("PRAGMA optimize").run();
}
async function seedCatalogAndGates(db: D1Database): Promise<void> {
const now = new Date().toISOString();
const gateStatements = LAUNCH_GATE_SEEDS.map(([key, priority, category, title]) =>
db
.prepare(
`INSERT OR IGNORE INTO launch_gates
(key, priority, category, title, status, notes, updated_at)
VALUES (?, ?, ?, ?, 'blocked', '', ?)`,
)
.bind(key, priority, category, title, now),
);
const gateReconciliationStatements = LAUNCH_GATE_SEEDS.map(
([key, priority, category, title]) =>
db
.prepare(
`UPDATE launch_gates
SET priority = ?, category = ?, title = ?, updated_at = ?
WHERE key = ?
AND (priority <> ? OR category <> ? OR title <> ?)`,
)
.bind(priority, category, title, now, key, priority, category, title),
);
const gateReconciliationAuditStatements = LAUNCH_GATE_SEEDS.map(
([key, priority, category, title]) =>
db
.prepare(
`INSERT OR IGNORE INTO audit_events
(id, actor_id, action, target_type, target_id, reason,
before_json, after_json, correlation_id, created_at)
SELECT 'audit_reclassify_' || key || '_free_beta_v1',
'system:bootstrap', 'launch_gate.reclassified',
'launch_gate', key,
'免费公测门禁与未来收费门禁分离',
json_object('priority', priority, 'category', category, 'title', title),
json_object('priority', ?, 'category', ?, 'title', ?),
'corr_reclassify_' || key || '_free_beta_v1', ?
FROM launch_gates
WHERE key = ?
AND (priority <> ? OR category <> ? OR title <> ?)`,
)
.bind(priority, category, title, now, key, priority, category, title),
);
await db.batch([
db
.prepare(
`INSERT OR IGNORE INTO price_versions
(id, product_code, billing_period, unit_slots, amount_minor, currency,
tax_mode, quote_ttl_seconds, status, effective_from, created_by)
VALUES (?, 'host_slot', 'month', 1, 1000, 'CNY', 'undecided', 900,
'retired', ?, 'system:seed')`,
)
.bind("price_host_month_v1", now),
db
.prepare(
`INSERT OR IGNORE INTO price_versions
(id, product_code, billing_period, unit_slots, amount_minor, currency,
tax_mode, quote_ttl_seconds, status, effective_from, created_by)
VALUES (?, 'host_slot', 'year', 1, 10000, 'CNY', 'undecided', 900,
'retired', ?, 'system:seed')`,
)
.bind("price_host_year_v1", now),
db
.prepare(
`INSERT OR IGNORE INTO beta_programs
(id, state, capacity_slots, starts_at, ends_at, grace_days, created_by)
VALUES (?, 'active', NULL, ?, NULL, 0, 'system:seed')`,
)
.bind("beta_public_v1", now),
...gateStatements,
...gateReconciliationAuditStatements,
...gateReconciliationStatements,
db
.prepare(
`INSERT OR IGNORE INTO audit_events
(id, actor_id, action, target_type, target_id, reason,
before_json, after_json, correlation_id, created_at)
SELECT 'audit_defer_paid_catalog_v1', 'system:bootstrap',
'price_catalog.retired', 'price_catalog', 'host_slot',
'免费公测阶段暂缓收费决策,保留历史价格但撤销发布状态',
'{"status":"published"}', '{"status":"retired"}',
'corr_defer_paid_catalog_v1', ?
WHERE EXISTS (
SELECT 1 FROM price_versions
WHERE product_code = 'host_slot' AND status = 'published'
)`,
)
.bind(now),
db.prepare(
`UPDATE price_versions
SET status = 'retired'
WHERE product_code = 'host_slot' AND status = 'published'`,
),
db
.prepare(
`INSERT OR IGNORE INTO audit_events
(id, actor_id, action, target_type, target_id, reason,
before_json, after_json, correlation_id, created_at)
SELECT 'audit_repair_p0_' || key, 'system:bootstrap',
'launch_gate.repaired', 'launch_gate', key,
'旧版 P0 状态缺少有效证据或使用了不适用,启动时恢复为阻止',
'{"status":"legacy_invalid"}', '{"status":"blocked"}',
'corr_repair_p0_' || key, ?
FROM launch_gates
WHERE priority = 'P0' AND (
status = 'not_applicable'
OR (status = 'passed' AND (
trim(COALESCE(owner, '')) = ''
OR trim(COALESCE(notes, '')) = ''
OR evidence_url IS NULL
OR evidence_url NOT LIKE 'https://%'
))
)`,
)
.bind(now),
db
.prepare(
`UPDATE launch_gates
SET status = 'blocked', reviewed_at = ?, updated_at = ?
WHERE priority = 'P0' AND (
status = 'not_applicable'
OR (status = 'passed' AND (
trim(COALESCE(owner, '')) = ''
OR trim(COALESCE(notes, '')) = ''
OR evidence_url IS NULL
OR evidence_url NOT LIKE 'https://%'
))
)`,
)
.bind(now, now),
]);
}
export async function ensureDatabase(): Promise<void> {
if (!schemaPromise) {
schemaPromise = (async () => {
const db = getDatabase();
await migrateSchema(db);
await seedCatalogAndGates(db);
})().catch((error) => {
schemaPromise = null;
throw error;
});
}
await schemaPromise;
}
export function getD1(): D1Database {
return getDatabase();
}
+218
View File
@@ -0,0 +1,218 @@
export type DeviceIdentity = {
ed25519Public: string;
x25519Public: string;
fingerprint: string;
};
const PAIRING_ID = /^pair_[0-9a-f]{32}$/;
const PAIRING_CODE = /^[0-9A-F]{20}$/;
const PUBLIC_KEY = /^[A-Za-z0-9_-]{43}$/;
const ED25519_SIGNATURE = /^[A-Za-z0-9_-]{86}$/;
const HEX_64 = /^[0-9a-f]{64}$/;
const REGISTRATION_PROOF_DOMAIN = new TextEncoder().encode(
"nekonest-cloud/device-registration-proof/v1",
);
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
}
function concatBytes(left: Uint8Array, right: Uint8Array): Uint8Array {
const result = new Uint8Array(left.length + right.length);
result.set(left);
result.set(right, left.length);
return result;
}
function decodeBase64Url(value: string): Uint8Array {
if (!PUBLIC_KEY.test(value)) throw new Error("invalid_public_key");
const base64 = `${value.replaceAll("-", "+").replaceAll("_", "/")}=`;
let decoded: string;
try {
decoded = atob(base64);
} catch {
throw new Error("invalid_public_key");
}
const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0));
if (bytes.length !== 32) throw new Error("invalid_public_key");
return bytes;
}
function decodeEd25519Signature(value: string): Uint8Array {
if (!ED25519_SIGNATURE.test(value)) throw new Error("invalid_registration_proof");
const base64 = `${value.replaceAll("-", "+").replaceAll("_", "/")}==`;
let decoded: string;
try {
decoded = atob(base64);
} catch {
throw new Error("invalid_registration_proof");
}
const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0));
if (bytes.length !== 64) throw new Error("invalid_registration_proof");
return bytes;
}
function lengthPrefixed(value: string): Uint8Array {
const encoded = new TextEncoder().encode(value);
const result = new Uint8Array(4 + encoded.length);
new DataView(result.buffer).setUint32(0, encoded.length, false);
result.set(encoded, 4);
return result;
}
export function deviceRegistrationProofTranscript(input: {
bootstrapToken: string;
os: string;
ed25519Public: string;
x25519Public: string;
identityFingerprint: string;
transportMode: string;
}): Uint8Array {
const fields = [
input.bootstrapToken.trim(),
input.os.trim().toLowerCase(),
input.ed25519Public.trim(),
input.x25519Public.trim(),
input.identityFingerprint.trim().toLowerCase(),
input.transportMode.trim(),
].map(lengthPrefixed);
const totalLength = fields.reduce(
(total, field) => total + field.length,
REGISTRATION_PROOF_DOMAIN.length,
);
const transcript = new Uint8Array(totalLength);
transcript.set(REGISTRATION_PROOF_DOMAIN);
let offset = REGISTRATION_PROOF_DOMAIN.length;
for (const field of fields) {
transcript.set(field, offset);
offset += field.length;
}
return transcript;
}
export async function verifyDeviceRegistrationProof(input: {
bootstrapToken: string;
os: string;
ed25519Public: string;
x25519Public: string;
identityFingerprint: string;
transportMode: string;
registrationProof: string;
}): Promise<boolean> {
try {
const publicKey = decodeBase64Url(input.ed25519Public.trim());
const signature = decodeEd25519Signature(input.registrationProof.trim());
const key = await crypto.subtle.importKey(
"raw",
Uint8Array.from(publicKey).buffer,
{ name: "Ed25519" },
false,
["verify"],
);
return crypto.subtle.verify(
{ name: "Ed25519" },
key,
Uint8Array.from(signature).buffer,
Uint8Array.from(deviceRegistrationProofTranscript(input)).buffer,
);
} catch {
return false;
}
}
export function normalizePairingCode(value: string): string {
const normalized = value.trim().toUpperCase();
if (!PAIRING_CODE.test(normalized)) throw new Error("invalid_pairing_code");
return normalized;
}
export function parseBootstrapToken(value: string): {
pairingId: string;
code: string;
} {
const trimmed = value.trim();
const separator = trimmed.indexOf(".");
if (separator < 0 || separator !== trimmed.lastIndexOf(".")) {
throw new Error("invalid_bootstrap_token");
}
const pairingId = trimmed.slice(0, separator);
if (!PAIRING_ID.test(pairingId)) throw new Error("invalid_bootstrap_token");
return {
pairingId,
code: normalizePairingCode(trimmed.slice(separator + 1)),
};
}
export async function sha256Hex(value: string | Uint8Array): Promise<string> {
const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
const input = Uint8Array.from(bytes).buffer;
return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", input)));
}
export function constantTimeEqualHex(left: string, right: string): boolean {
const leftNormalized = left.toLowerCase();
const rightNormalized = right.toLowerCase();
let difference = leftNormalized.length ^ rightNormalized.length;
const length = Math.max(leftNormalized.length, rightNormalized.length);
for (let index = 0; index < length; index += 1) {
difference |=
(leftNormalized.charCodeAt(index) || 0) ^
(rightNormalized.charCodeAt(index) || 0);
}
return difference === 0;
}
export async function validateDeviceIdentity(input: {
ed25519Public: string;
x25519Public: string;
identityFingerprint: string;
}): Promise<DeviceIdentity> {
const ed25519Public = input.ed25519Public.trim();
const x25519Public = input.x25519Public.trim();
const fingerprint = input.identityFingerprint.trim().toLowerCase();
const ed25519Bytes = decodeBase64Url(ed25519Public);
const x25519Bytes = decodeBase64Url(x25519Public);
if (!HEX_64.test(fingerprint)) throw new Error("invalid_identity_fingerprint");
const expected = await sha256Hex(concatBytes(ed25519Bytes, x25519Bytes));
if (!constantTimeEqualHex(expected, fingerprint)) {
throw new Error("invalid_identity_fingerprint");
}
return { ed25519Public, x25519Public, fingerprint };
}
export function randomDeviceToken(): string {
return bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
}
export function rateWindowStart(now: Date): string {
const windowMilliseconds = 10 * 60_000;
return new Date(
Math.floor(now.getTime() / windowMilliseconds) * windowMilliseconds,
).toISOString();
}
export async function sourceFingerprint(
rootSecret: string,
source: string,
): Promise<string> {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(rootSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const signature = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(`pairing-source-v1\0${source}`),
);
return bytesToHex(new Uint8Array(signature));
}
export function requestSource(request: Request, production: boolean): string {
const edgeSource = request.headers.get("cf-connecting-ip")?.trim() ?? "";
if (/^[0-9A-Fa-f:.]{3,64}$/.test(edgeSource)) return edgeSource.toLowerCase();
if (production) throw new Error("trusted_source_unavailable");
return "local-development";
}
+191
View File
@@ -0,0 +1,191 @@
export const CONTROL_PLANE_CONTACT_FRESH_MS = 10 * 60 * 1_000;
export const CONTROL_PLANE_CONTACT_DELAYED_MS = 30 * 60 * 1_000;
const CONTROL_PLANE_CONTACT_FUTURE_SKEW_MS = 5 * 60 * 1_000;
const ADMIN_HOST_CONTACT_CTE = `
WITH latest_contact AS (
SELECT hosts.id, hosts.account_id, hosts.name, hosts.os,
hosts.daemon_version, accounts.email,
MAX(credentials.last_used_at) AS control_plane_last_seen_at
FROM hosts
INNER JOIN accounts ON accounts.id = hosts.account_id
LEFT JOIN device_credentials AS credentials ON credentials.host_id = hosts.id
WHERE hosts.lifecycle = 'active' AND hosts.slot_state = 'active'
GROUP BY hosts.id, hosts.account_id, hosts.name, hosts.os,
hosts.daemon_version, accounts.email
), categorized AS (
SELECT *, CASE
WHEN control_plane_last_seen_at IS NULL THEN 'never'
WHEN julianday(control_plane_last_seen_at) IS NULL
OR julianday(control_plane_last_seen_at) > julianday(?1) THEN 'invalid'
WHEN julianday(control_plane_last_seen_at) >= julianday(?2) THEN 'fresh'
WHEN julianday(control_plane_last_seen_at) >= julianday(?3) THEN 'delayed'
ELSE 'stale'
END AS contact_state
FROM latest_contact
)
`;
export const ADMIN_HOST_CONTROL_PLANE_SUMMARY_SQL = `${ADMIN_HOST_CONTACT_CTE}
SELECT COUNT(*) AS total_active,
SUM(CASE WHEN contact_state = 'fresh' THEN 1 ELSE 0 END) AS fresh,
SUM(CASE WHEN contact_state = 'delayed' THEN 1 ELSE 0 END) AS delayed,
SUM(CASE WHEN contact_state = 'stale' THEN 1 ELSE 0 END) AS stale,
SUM(CASE WHEN contact_state = 'never' THEN 1 ELSE 0 END) AS never,
SUM(CASE WHEN contact_state = 'invalid' THEN 1 ELSE 0 END) AS invalid,
SUM(CASE WHEN daemon_version IS NULL THEN 1 ELSE 0 END) AS version_unknown
FROM categorized
`;
export const ADMIN_HOST_CONTROL_PLANE_ATTENTION_SQL = `${ADMIN_HOST_CONTACT_CTE}
SELECT id, account_id, name, os, daemon_version, email,
control_plane_last_seen_at, contact_state
FROM categorized
WHERE contact_state <> 'fresh'
ORDER BY CASE contact_state
WHEN 'invalid' THEN 0
WHEN 'never' THEN 1
WHEN 'stale' THEN 2
WHEN 'delayed' THEN 3
ELSE 4
END,
control_plane_last_seen_at ASC, id ASC
LIMIT 25
`;
export const OWNED_HOSTS_WITH_CONTROL_PLANE_CONTACT_SQL = `
SELECT hosts.*,
(SELECT MAX(credentials.last_used_at)
FROM device_credentials AS credentials
WHERE credentials.host_id = hosts.id)
AS control_plane_last_seen_at
FROM hosts
WHERE hosts.account_id = ?
ORDER BY hosts.lifecycle = 'active' DESC, hosts.created_at DESC
`;
export type ControlPlaneContactState =
| "never"
| "fresh"
| "delayed"
| "stale"
| "invalid";
export type ControlPlaneContact = {
state: ControlPlaneContactState;
label: string;
detail: string;
tone: "good" | "warn" | "danger" | "neutral";
};
type AdminHostControlPlaneAggregateRow = Record<string, number | null>;
export type AdminHostControlPlaneAttentionRow = {
id: string;
account_id: string;
name: string;
os: string;
daemon_version: string | null;
email: string;
control_plane_last_seen_at: string | null;
contact_state: Exclude<ControlPlaneContactState, "fresh">;
};
export type AdminHostControlPlaneSnapshot = {
generatedAt: string;
totalActive: number;
fresh: number;
delayed: number;
stale: number;
never: number;
invalid: number;
versionUnknown: number;
attentionHosts: AdminHostControlPlaneAttentionRow[];
};
function aggregateCount(
row: AdminHostControlPlaneAggregateRow,
key: string,
): number {
const value = Number(row[key] ?? 0);
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
}
export function controlPlaneContactCutoffs(generatedAt: string) {
const nowMs = Date.parse(generatedAt);
if (!Number.isFinite(nowMs)) throw new Error("invalid_control_plane_contact_time");
return {
futureLimitAt: new Date(nowMs + CONTROL_PLANE_CONTACT_FUTURE_SKEW_MS).toISOString(),
freshCutoff: new Date(nowMs - CONTROL_PLANE_CONTACT_FRESH_MS).toISOString(),
delayedCutoff: new Date(nowMs - CONTROL_PLANE_CONTACT_DELAYED_MS).toISOString(),
};
}
export function deriveAdminHostControlPlaneSnapshot(input: {
generatedAt: string;
summary: AdminHostControlPlaneAggregateRow;
attentionHosts: AdminHostControlPlaneAttentionRow[];
}): AdminHostControlPlaneSnapshot {
return {
generatedAt: input.generatedAt,
totalActive: aggregateCount(input.summary, "total_active"),
fresh: aggregateCount(input.summary, "fresh"),
delayed: aggregateCount(input.summary, "delayed"),
stale: aggregateCount(input.summary, "stale"),
never: aggregateCount(input.summary, "never"),
invalid: aggregateCount(input.summary, "invalid"),
versionUnknown: aggregateCount(input.summary, "version_unknown"),
attentionHosts: input.attentionHosts,
};
}
export function deriveControlPlaneContact(
lastSeenAt: string | null,
nowMs = Date.now(),
): ControlPlaneContact {
if (!lastSeenAt) {
return {
state: "never",
label: "尚未联系",
detail: "daemon 还没有成功查询 Cloud 开通状态。",
tone: "neutral",
};
}
const seenAtMs = Date.parse(lastSeenAt);
if (
!Number.isFinite(seenAtMs) ||
seenAtMs > nowMs + CONTROL_PLANE_CONTACT_FUTURE_SKEW_MS
) {
return {
state: "invalid",
label: "时间待核实",
detail: "最近一次控制面签到时间无效或明显超前。",
tone: "warn",
};
}
const ageMs = Math.max(0, nowMs - seenAtMs);
if (ageMs <= CONTROL_PLANE_CONTACT_FRESH_MS) {
return {
state: "fresh",
label: "控制面正常",
detail: "daemon 最近成功查询了 Cloud 开通状态。",
tone: "good",
};
}
if (ageMs <= CONTROL_PLANE_CONTACT_DELAYED_MS) {
return {
state: "delayed",
label: "联系延迟",
detail: "daemon 一段时间没有再次查询 Cloud 开通状态。",
tone: "warn",
};
}
return {
state: "stale",
label: "长时间未联系",
detail: "daemon 已超过半小时没有查询 Cloud 开通状态。",
tone: "danger",
};
}
+23
View File
@@ -0,0 +1,23 @@
export class DomainError extends Error {
readonly code: string;
readonly status: number;
readonly retryable: boolean;
readonly retryAfterSeconds?: number;
readonly actionUrl?: string;
constructor(
code: string,
message: string,
status = 400,
retryable = status >= 500 || status === 429,
retryAfterSeconds?: number,
actionUrl?: string,
) {
super(message);
this.code = code;
this.status = status;
this.retryable = retryable;
this.retryAfterSeconds = retryAfterSeconds;
this.actionUrl = actionUrl;
}
}
+149
View File
@@ -0,0 +1,149 @@
export type BillingPeriod = "month" | "year";
export function getPublicBetaPresentation(active: boolean) {
return active
? {
status: "公测期间服务费全免",
subline: "不绑支付方式 · 不会自动扣款",
factValue: "¥0",
factLabel: "公测服务费",
priceHeading: "当前公测",
priceValue: "¥0",
priceDetail: "全部已允许主机",
cardStatus: "当前有效",
cardTitle: "公开公测",
cardDescription: "公测期间,所有已经允许接入的主机槽位不收服务费。",
cta: "进入公测控制台",
comparison: "当前公测免费;未来方案未定",
}
: {
status: "公测免费政策已结束",
subline: "不会自动扣款 · 付费入口仍关闭",
factValue: "关闭",
factLabel: "公测免费",
priceHeading: "公测政策",
priceValue: "已结束",
priceDetail: "不会转为自动扣款",
cardStatus: "已经结束",
cardTitle: "公开公测",
cardDescription: "免费政策已结束;付费入口仍由上线门禁关闭,不会自动扣款。",
cta: "进入控制台",
comparison: "公测已结束;收费入口未开放",
};
}
export function getBillingEntitlementPresentation(
mode: "public_beta" | "grant" | "none",
publicBetaState: "open" | "gated" | "inactive" = "inactive",
) {
if (mode === "public_beta") {
return {
tone: "good" as const,
status: "公测免费",
title: "当前应付 ¥0",
description: "不需要支付方式;公测结束不会自动生成付款或扣款。",
emptyOrders: "免费公测不提供报价,也不会生成订单或付款单。",
};
}
if (mode === "grant") {
return {
tone: "info" as const,
status: "闭测邀请",
title: "当前权益有效",
description: "当前由有期限的非货币权益覆盖;不会生成付款或自动扣款。",
emptyOrders: "闭测邀请不会生成报价、订单或付款单。",
};
}
if (publicBetaState === "gated") {
return {
tone: "warn" as const,
status: "公开接入冻结",
title: "当前没有有效闭测邀请",
description: "免费政策已经预设,但安全门禁尚未齐全;不会要求付款或自动创建订单。",
emptyOrders: "公开接入冻结期间不会生成报价、订单或付款单。",
};
}
return {
tone: "neutral" as const,
status: "无有效免费资格",
title: "当前无免费权益",
description: "公开公测当前未开放,该账户也没有有效闭测邀请;不会自动生成付款或扣款。",
emptyOrders: "收费功能仍未开放,不会自动生成报价、订单或付款单。",
};
}
/**
* Advance a fixed-term order by one calendar billing period while clamping
* month-end dates. This prevents January 31 from rolling into March and keeps
* leap-day yearly orders on the final valid day of February.
*/
export function addBillingPeriod(
startInput: Date | string,
period: BillingPeriod,
): string {
const start = new Date(startInput);
if (Number.isNaN(start.valueOf())) {
throw new RangeError("Invalid billing term start");
}
const requestedDay = start.getUTCDate();
const result = new Date(start);
result.setUTCDate(1);
if (period === "month") {
result.setUTCMonth(result.getUTCMonth() + 1);
} else {
result.setUTCFullYear(result.getUTCFullYear() + 1);
}
const lastDay = new Date(
Date.UTC(result.getUTCFullYear(), result.getUTCMonth() + 1, 0),
).getUTCDate();
result.setUTCDate(Math.min(requestedDay, lastDay));
return result.toISOString();
}
/** Return the next time the currently summarized entitlement can change. */
export function nextEntitlementExpiry(
values: Array<string | null>,
): string | null {
return values
.filter((value): value is string => Boolean(value))
.sort()
.at(0) ?? null;
}
export type EntitlementComponent = {
source: string;
capacity: number | null;
endsAt: string | null;
};
export function summarizeEntitlementComponents(
components: EntitlementComponent[],
activeSlots: number,
reservedSlots: number,
) {
const unlimitedComponents = components.filter(
(component) => component.capacity === null,
);
const unlimited = unlimitedComponents.length > 0;
const capacitySlots = unlimited
? null
: components.reduce((sum, component) => sum + (component.capacity ?? 0), 0);
const effectiveUntil = unlimited
? unlimitedComponents.some((component) => component.endsAt === null)
? null
: nextEntitlementExpiry(unlimitedComponents.map((component) => component.endsAt))
: nextEntitlementExpiry(components.map((component) => component.endsAt));
return {
unlimited,
capacitySlots,
availableSlots: unlimited
? null
: Math.max(0, (capacitySlots ?? 0) - activeSlots - reservedSlots),
effectiveUntil,
sources: [...new Set(components.map((component) => component.source))],
};
}
+13
View File
@@ -0,0 +1,13 @@
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./schema";
export function getDb() {
if (!env.DB) {
throw new Error(
"Cloudflare D1 binding `DB` is unavailable. Set the `d1` field in .openai/hosting.json to `DB` or let your control plane inject the real binding values before using the database."
);
}
return drizzle(env.DB, { schema });
}
+58
View File
@@ -0,0 +1,58 @@
export type InvitationDisplayState = "active" | "expired" | "revoked";
export function deriveInvitationDisplayState(
invitation: { state: string; ends_at: string | null; revoked_at: string | null },
now = new Date().toISOString(),
): InvitationDisplayState {
if (invitation.state === "revoked" || invitation.revoked_at) return "revoked";
if (invitation.ends_at && invitation.ends_at <= now) return "expired";
return "active";
}
/**
* Create the replay record only while the requested administrator invitation
* is still revocable. D1 batch serialization makes this predicate the fence
* for concurrent revocations with different idempotency keys.
*
* Parameters: scope, key, request hash, response JSON, expiry, now, grant id.
*/
export const CREATE_INVITATION_REVOCATION_IDEMPOTENCY_SQL = `
INSERT INTO idempotency_records
(scope, key, request_hash, response_json, status_code, expires_at, created_at)
SELECT ?1, ?2, ?3, ?4, 200, ?5, ?6
FROM entitlement_grants
WHERE id = ?7 AND source = 'admin_exemption'
AND state = 'active' AND revoked_at IS NULL
`;
/** Parameters: revoked time, grant id, scope, key, request hash. */
export const REVOKE_INVITATION_SQL = `
UPDATE entitlement_grants
SET state = 'revoked', revoked_at = ?1
WHERE id = ?2 AND source = 'admin_exemption'
AND state = 'active' AND revoked_at IS NULL
AND EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?3 AND key = ?4 AND request_hash = ?5
)
`;
/**
* Append exactly one audit row for a successful revocation. The audit id is
* deterministic per idempotency key, so a replay cannot duplicate history.
*/
export const AUDIT_INVITATION_REVOCATION_SQL = `
INSERT OR IGNORE INTO audit_events
(id, actor_id, action, target_type, target_id, reason,
before_json, after_json, correlation_id, created_at)
SELECT ?1, ?2, 'entitlement.invitation_revoked', 'entitlement_grant', ?3,
?4, ?5, ?6, ?7, ?8
WHERE EXISTS (
SELECT 1 FROM idempotency_records
WHERE scope = ?9 AND key = ?10 AND request_hash = ?11
)
AND EXISTS (
SELECT 1 FROM entitlement_grants
WHERE id = ?3 AND state = 'revoked' AND revoked_at = ?8
)
`;
+87
View File
@@ -0,0 +1,87 @@
export const LAUNCH_GATE_SEEDS = [
["sealed-e2e", "P0", "security", "sealed 命令与附件端到端实证"],
["expiry-boundary", "P0", "entitlement", "到期后的操作边界与重放测试"],
["tenant-isolation", "P0", "infrastructure", "租户隔离、备份与删除验证"],
["billing-idempotency", "PAID", "billing", "订单到权益的幂等与对账"],
["pairing-claim-security", "P0", "security", "主机配对认领、限速与尝试预算"],
["public-auth", "P0", "identity", "国内个人用户登录、恢复与管理员身份"],
["legal-entity", "P0", "compliance", "免费公测主体、域名与备案路径"],
["payment-provider", "PAID", "billing", "支付商户准入、验签、退款与对账"],
["tax-invoice", "PAID", "compliance", "税务、含税口径与数电发票"],
["privacy-retention", "P0", "privacy", "数据清单、保存、删除与跨境路径"],
["terms-consumer", "PAID", "compliance", "付费服务条款、取消、退款与消费者规则"],
["backup-restore", "P1", "operations", "备份与恢复演练"],
["capacity-economics", "P1", "operations", "容量、成本与支持工时实测"],
["beta-policy", "P1", "product", "公测结束、通知、宽限与反滥用限制"],
["build-toolchain-audit", "P1", "security", "构建工具链残余公告与上游替换"],
["account-lifecycle", "P1", "privacy", "导出、注销、保留例外与备份擦除"],
["host-lifecycle-recovery", "P1", "operations", "主机恢复、换绑、停用、重装与凭据轮换"],
["incident-response", "P1", "security", "安全事件分级、值守、通知与服务流程"],
["release-provenance", "P1", "security", "PWA 构建来源、CSP、依赖与回滚 provenance"],
] as const;
export const REQUIRED_PUBLIC_BETA_P0_KEYS = LAUNCH_GATE_SEEDS
.filter(([, priority]) => priority === "P0")
.map(([key]) => key);
export type LaunchGateEvidence = {
key: string;
priority: string;
status: string;
owner: string | null;
notes: string;
evidence_url: string | null;
};
export function hasPassingGateEvidence(gate: LaunchGateEvidence): boolean {
if (
gate.status !== "passed"
|| !gate.owner?.trim()
|| !gate.notes.trim()
|| !gate.evidence_url
) {
return false;
}
try {
return new URL(gate.evidence_url).protocol === "https:";
} catch {
return false;
}
}
export function countBlockedPublicBetaP0(gates: readonly LaunchGateEvidence[]): number {
const required = new Set<string>(REQUIRED_PUBLIC_BETA_P0_KEYS);
const presentRequired = new Set(
gates
.filter((gate) => gate.priority === "P0" && required.has(gate.key))
.map((gate) => gate.key),
);
const missingRequired = REQUIRED_PUBLIC_BETA_P0_KEYS.length - presentRequired.size;
const invalidP0 = gates.filter(
(gate) => gate.priority === "P0" && !hasPassingGateEvidence(gate),
).length;
return missingRequired + invalidP0;
}
const requiredP0SqlList = REQUIRED_PUBLIC_BETA_P0_KEYS
.map((key) => `'${key.replaceAll("'", "''")}'`)
.join(", ");
/**
* Fail closed when a required P0 row is missing or any P0 row lacks complete
* passing evidence. This fragment is embedded only in repository-owned SQL.
*/
export const PUBLIC_BETA_GATE_READY_SQL = `
(SELECT COUNT(*) FROM launch_gates
WHERE priority = 'P0' AND key IN (${requiredP0SqlList})) = ${REQUIRED_PUBLIC_BETA_P0_KEYS.length}
AND NOT EXISTS (
SELECT 1 FROM launch_gates
WHERE priority = 'P0'
AND (
status != 'passed'
OR trim(COALESCE(owner, '')) = ''
OR trim(COALESCE(notes, '')) = ''
OR evidence_url IS NULL
OR lower(evidence_url) NOT LIKE 'https://%'
)
)`;
+351
View File
@@ -0,0 +1,351 @@
import { PUBLIC_BETA_GATE_READY_SQL } from "./launch-gates.ts";
/**
* Reserve a pairing slot in one SQLite statement.
*
* Parameters:
* ?1 request id, ?2 account id, ?3 requested name, ?4 OS,
* ?5 code hash, ?6 expiry, ?7 current UTC timestamp.
*
* Keeping the capacity and pending-limit predicates inside the INSERT makes
* concurrent D1 writes serialize around the actual reservation instead of
* trusting an earlier read that may already be stale.
*/
export const RESERVE_PAIRING_SQL = `
WITH active_beta AS (
SELECT capacity_slots
FROM beta_programs
WHERE state = 'active' AND starts_at <= ?7
AND (ends_at IS NULL OR ends_at > ?7)
AND ${PUBLIC_BETA_GATE_READY_SQL}
ORDER BY created_at DESC, id DESC
LIMIT 1
)
INSERT INTO pairing_requests
(id, account_id, requested_name, os, code_hash, status, expires_at, created_at)
SELECT ?1, ?2, ?3, ?4, ?5, 'waiting', ?6, ?7
WHERE
(
SELECT COUNT(*) FROM pairing_requests
WHERE account_id = ?2 AND status = 'waiting' AND expires_at > ?7
) < 5
AND (
EXISTS (
SELECT 1 FROM active_beta WHERE capacity_slots IS NULL
)
OR EXISTS (
SELECT 1 FROM entitlement_grants
WHERE account_id = ?2 AND state = 'active' AND starts_at <= ?7
AND (ends_at IS NULL OR ends_at > ?7)
AND revoked_at IS NULL AND capacity_slots IS NULL
)
OR (
COALESCE((
SELECT SUM(capacity_slots) FROM active_beta
WHERE capacity_slots IS NOT NULL
), 0)
+ COALESCE((
SELECT SUM(capacity_slots) FROM entitlement_grants
WHERE account_id = ?2 AND state = 'active' AND starts_at <= ?7
AND (ends_at IS NULL OR ends_at > ?7)
AND revoked_at IS NULL AND capacity_slots IS NOT NULL
), 0)
>
(
SELECT COUNT(*) FROM hosts
WHERE account_id = ?2 AND lifecycle = 'active' AND slot_state = 'active'
)
+ (
SELECT COUNT(*) FROM pairing_requests
WHERE account_id = ?2 AND status = 'waiting' AND expires_at > ?7
)
)
)
`;
export const CONSUME_CLAIM_RATE_SQL = `
INSERT INTO pairing_claim_rate_limits
(source_hash, window_start, attempts, updated_at)
VALUES (?1, ?2, 1, ?3)
ON CONFLICT(source_hash, window_start) DO UPDATE SET
attempts = pairing_claim_rate_limits.attempts + 1,
updated_at = excluded.updated_at
WHERE pairing_claim_rate_limits.attempts < 20
`;
export const RECORD_FAILED_CODE_SQL = `
UPDATE pairing_requests
SET failed_attempts = failed_attempts + 1,
last_attempt_at = ?1,
locked_at = CASE WHEN failed_attempts + 1 >= 5 THEN ?1 ELSE locked_at END,
status = CASE WHEN failed_attempts + 1 >= 5 THEN 'locked' ELSE status END
WHERE id = ?2 AND status = 'waiting' AND locked_at IS NULL
AND expires_at > ?1 AND failed_attempts < 5
`;
/**
* Invalidate an unclaimed pairing request owned by one account.
*
* Parameters:
* ?1 request id, ?2 account id, ?3 replacement code hash.
*
* Replacing the hash makes an accidentally reverted status insufficient to
* revive the original bearer code.
*/
export const CANCEL_PAIRING_SQL = `
UPDATE pairing_requests
SET status = 'cancelled', code_hash = ?3
WHERE id = ?1 AND account_id = ?2 AND status = 'waiting'
`;
export const OWNED_PAIRING_PROGRESS_SQL = `
SELECT pairing_requests.id, pairing_requests.status,
pairing_requests.expires_at, pairing_requests.claimed_host_id,
pairing_requests.claimed_at,
(SELECT MAX(attempts.created_at)
FROM pairing_claim_attempts AS attempts
WHERE attempts.pairing_request_id = pairing_requests.id)
AS last_claim_attempt_at
FROM pairing_requests
WHERE pairing_requests.id = ? AND pairing_requests.account_id = ?
`;
export type PairingProgressRow = {
id: string;
status: string;
expires_at: string;
claimed_host_id: string | null;
claimed_at: string | null;
last_claim_attempt_at: string | null;
};
export type PairingProgress = {
id: string;
status: "waiting" | "claimed" | "expired" | "locked" | "cancelled";
expiresAt: string;
claimedHostId: string | null;
claimedAt: string | null;
claimAttemptState: "not_seen" | "seen" | "invalid";
lastClaimAttemptAt: string | null;
};
export function derivePairingProgress(
row: PairingProgressRow,
nowInput: string,
): PairingProgress {
const now = Date.parse(nowInput);
const expiresAt = Date.parse(row.expires_at);
if (!Number.isFinite(now) || !Number.isFinite(expiresAt)) {
throw new RangeError("invalid_pairing_progress_time");
}
const claimAttemptAt = row.last_claim_attempt_at
? Date.parse(row.last_claim_attempt_at)
: null;
const claimAttemptState = claimAttemptAt === null
? "not_seen"
: !Number.isFinite(claimAttemptAt) || claimAttemptAt > now + 5 * 60_000
? "invalid"
: "seen";
const lastClaimAttemptAt = claimAttemptState === "seen"
? row.last_claim_attempt_at
: null;
if (row.status === "waiting") {
return {
id: row.id,
status: expiresAt <= now ? "expired" : "waiting",
expiresAt: row.expires_at,
claimedHostId: null,
claimedAt: null,
claimAttemptState,
lastClaimAttemptAt,
};
}
if (row.status === "claimed") {
if (!row.claimed_host_id || !row.claimed_at) {
throw new RangeError("incomplete_claimed_pairing_progress");
}
return {
id: row.id,
status: "claimed",
expiresAt: row.expires_at,
claimedHostId: row.claimed_host_id,
claimedAt: row.claimed_at,
claimAttemptState,
lastClaimAttemptAt,
};
}
if (row.status === "locked" || row.status === "cancelled") {
return {
id: row.id,
status: row.status,
expiresAt: row.expires_at,
claimedHostId: null,
claimedAt: null,
claimAttemptState,
lastClaimAttemptAt,
};
}
throw new RangeError("unknown_pairing_progress_status");
}
const CURRENT_PAIRING_ACCESS = `
(
EXISTS (
SELECT 1 FROM beta_programs
WHERE state = 'active' AND starts_at <= ? AND (ends_at IS NULL OR ends_at > ?)
AND ${PUBLIC_BETA_GATE_READY_SQL}
)
OR EXISTS (
SELECT 1 FROM entitlement_grants
WHERE account_id = pairing_requests.account_id
AND state = 'active' AND starts_at <= ? AND (ends_at IS NULL OR ends_at > ?)
AND revoked_at IS NULL
)
)`;
const VALID_CLAIM = `
id = ? AND code_hash = ? AND status = 'waiting'
AND expires_at > ? AND locked_at IS NULL AND failed_attempts < 5 AND os = ?
AND ${CURRENT_PAIRING_ACCESS}
`;
/**
* Claim-time capacity check for CLAIM_HOST_SQL.
*
* The numbered parameters intentionally reuse that statement's current-time
* bindings: ?11/?12 for the latest public beta and ?13/?14 for grants. A
* waiting request reserves capacity while it is waiting, but claim converts
* it into an active host. Therefore the atomic claim boundary compares the
* current finite entitlement with active hosts only. If capacity was reduced
* after several pairing codes were issued, the first claims up to the new
* limit may succeed and later claims fail without touching existing hosts.
*/
const HOST_CLAIM_CAPACITY = `
(
EXISTS (
SELECT 1 FROM current_beta WHERE capacity_slots IS NULL
)
OR EXISTS (
SELECT 1 FROM entitlement_grants
WHERE account_id = pairing_requests.account_id
AND state = 'active' AND starts_at <= ?13
AND (ends_at IS NULL OR ends_at > ?14)
AND revoked_at IS NULL AND capacity_slots IS NULL
)
OR (
COALESCE((SELECT capacity_slots FROM current_beta), 0)
+ COALESCE((
SELECT SUM(capacity_slots) FROM entitlement_grants
WHERE account_id = pairing_requests.account_id
AND state = 'active' AND starts_at <= ?13
AND (ends_at IS NULL OR ends_at > ?14)
AND revoked_at IS NULL AND capacity_slots IS NOT NULL
), 0)
> (
SELECT COUNT(*) FROM hosts
WHERE account_id = pairing_requests.account_id
AND lifecycle = 'active' AND slot_state = 'active'
)
)
)`;
export const CLAIM_HOST_SQL = `
WITH current_beta AS (
SELECT capacity_slots
FROM beta_programs
WHERE state = 'active' AND starts_at <= ?11
AND (ends_at IS NULL OR ends_at > ?12)
AND ${PUBLIC_BETA_GATE_READY_SQL}
ORDER BY created_at DESC, id DESC
LIMIT 1
)
INSERT INTO hosts
(id, account_id, name, os, lifecycle, slot_state, connection_state,
daemon_version, ed25519_public, x25519_public, identity_fingerprint, claim_request_id,
claimed_at, created_at)
SELECT ?1, account_id, requested_name, os, 'active', 'active', 'offline',
?15, ?2, ?3, ?4, id, ?5, ?6
FROM pairing_requests
WHERE id = ?7 AND code_hash = ?8 AND status = 'waiting'
AND expires_at > ?9 AND locked_at IS NULL AND failed_attempts < 5
AND os = ?10
AND (
EXISTS (SELECT 1 FROM current_beta)
OR EXISTS (
SELECT 1 FROM entitlement_grants
WHERE account_id = pairing_requests.account_id
AND state = 'active' AND starts_at <= ?13
AND (ends_at IS NULL OR ends_at > ?14)
AND revoked_at IS NULL
)
)
AND ${HOST_CLAIM_CAPACITY}
ON CONFLICT(identity_fingerprint) DO UPDATE SET
name = excluded.name,
os = excluded.os,
lifecycle = 'active',
slot_state = 'active',
connection_state = 'offline',
daemon_version = COALESCE(excluded.daemon_version, hosts.daemon_version),
ed25519_public = excluded.ed25519_public,
x25519_public = excluded.x25519_public,
claim_request_id = excluded.claim_request_id,
claimed_at = excluded.claimed_at,
deactivated_at = NULL
WHERE hosts.account_id = excluded.account_id
AND hosts.lifecycle = 'deactivated'
AND hosts.slot_state = 'released'
`;
export const CLAIM_CREDENTIAL_SQL = `
INSERT INTO device_credentials
(id, host_id, token_hash, status, issued_at, created_at)
SELECT ?, ?, ?, 'active', ?, ?
FROM pairing_requests
WHERE ${VALID_CLAIM}
AND EXISTS (
SELECT 1 FROM hosts
WHERE id = ? AND identity_fingerprint = ?
AND claim_request_id = pairing_requests.id
)
`;
export const COMMIT_PAIRING_CLAIM_SQL = `
UPDATE pairing_requests
SET status = 'claimed', claimed_host_id = ?, claimed_at = ?, last_attempt_at = ?,
code_hash = ?
WHERE ${VALID_CLAIM}
AND EXISTS (
SELECT 1 FROM device_credentials
WHERE id = ? AND host_id = ? AND status = 'active'
)
`;
/**
* Publish a successfully claimed device set to the tenant reconciler.
*
* Parameters:
* ?1 current UTC timestamp, ?2 account id, ?3 pairing id, ?4 host id,
* ?5 claimed timestamp, ?6 credential id, ?7 device token hash.
*
* The positive pairing and credential predicates keep this update in the
* same fail-closed D1 batch as the claim. A failed or partial claim therefore
* cannot create provisioning work.
*/
export const ADVANCE_TENANT_CREDENTIAL_AFTER_CLAIM_SQL = `
UPDATE tenant_instances
SET credential_revision = credential_revision + 1,
lifecycle = CASE WHEN lifecycle = 'ready' THEN 'degraded' ELSE lifecycle END,
relay_ready = 0,
updated_at = ?
WHERE account_id = ?
AND EXISTS (
SELECT 1 FROM pairing_requests
WHERE id = ? AND status = 'claimed' AND claimed_host_id = ? AND claimed_at = ?
)
AND EXISTS (
SELECT 1 FROM device_credentials
WHERE id = ? AND host_id = ? AND token_hash = ? AND status = 'active'
)
`;
+62
View File
@@ -0,0 +1,62 @@
const REPLAY_DOMAIN = "nekonest-cloud/device-registration-replay/v1";
export const DEVICE_REGISTRATION_REPLAY_TTL_MS = 10 * 60_000;
function bytesToBase64(bytes: Uint8Array): string {
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary);
}
function base64ToBytes(value: string): Uint8Array {
const binary = atob(value);
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}
async function registrationReplayKey(retryKey: string): Promise<CryptoKey> {
const material = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(`${REPLAY_DOMAIN}\0${retryKey}`),
);
return crypto.subtle.importKey("raw", material, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
}
export async function encryptRegistrationReplay<T>(input: {
retryKey: string;
pairingId: string;
requestHash: string;
response: T;
}): Promise<{ ciphertext: string; nonce: string }> {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce,
additionalData: new TextEncoder().encode(`${input.pairingId}\0${input.requestHash}`),
},
await registrationReplayKey(input.retryKey),
new TextEncoder().encode(JSON.stringify(input.response)),
);
return {
ciphertext: bytesToBase64(new Uint8Array(ciphertext)),
nonce: bytesToBase64(nonce),
};
}
export async function decryptRegistrationReplay<T>(input: {
retryKey: string;
pairingId: string;
requestHash: string;
ciphertext: string;
nonce: string;
}): Promise<T> {
const plaintext = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: Uint8Array.from(base64ToBytes(input.nonce)).buffer,
additionalData: new TextEncoder().encode(`${input.pairingId}\0${input.requestHash}`),
},
await registrationReplayKey(input.retryKey),
Uint8Array.from(base64ToBytes(input.ciphertext)).buffer,
);
return JSON.parse(new TextDecoder().decode(plaintext)) as T;
}
+226
View File
@@ -0,0 +1,226 @@
export const RELAY_AUTHORIZATION_SNAPSHOT_VERSION = 1 as const;
export const RELAY_AUTHORIZATION_MAX_TTL_SECONDS = 5 * 60;
export const RELAY_AUTHORIZATION_REFRESH_SECONDS = 60;
export const RELAY_AUTHORIZATION_DELTA_SECONDS = 15;
const SNAPSHOT_DOMAIN = "nekonest-cloud/relay-authorization-snapshot/v1\n";
export const RELAY_SIGNING_KEY_FOR_SNAPSHOT_SQL = `
SELECT kid, public_key_jwk, private_key_ref
FROM relay_signing_keys
WHERE status = 'active' AND not_before <= ?1 AND not_after >= ?2
ORDER BY not_before DESC, kid DESC LIMIT 1`;
export type AuthorizedDevice = {
device_id: string;
name: string;
os: "windows" | "linux";
ed25519_public: string;
x25519_public: string;
credential_hash: string;
identity_fingerprint: string;
};
export type AuthorizedPhone = {
phone_id: string;
name: string;
credential_hash: string;
ed25519_public: string;
x25519_public: string;
identity_fingerprint: string;
};
export type RelayAuthorizationSnapshotPayload = {
snapshot_version: typeof RELAY_AUTHORIZATION_SNAPSHOT_VERSION;
tenant_id: string;
tenant_status: "active" | "suspended";
home_region: string;
relay_node_id: string;
placement_generation: number;
authorization_revision: number;
devices: AuthorizedDevice[];
phones?: AuthorizedPhone[];
issued_at: string;
expires_at: string;
};
export type SignedRelayAuthorizationSnapshot = {
algorithm: "Ed25519";
kid: string;
payload: RelayAuthorizationSnapshotPayload;
signature: string;
};
export type SnapshotPlacementAdmission = {
relay_node_id: string | null;
generation: number;
tenant_status: "active" | "suspended";
placement_state: string;
};
export function classifySnapshotPlacement(input: {
placement: SnapshotPlacementAdmission | null;
nodeId: string;
expectedGeneration: number;
}): "ready" | "wrong_node" | "stale_generation" | "suspended" | "provisioning" {
const placement = input.placement;
if (!placement || placement.relay_node_id !== input.nodeId) return "wrong_node";
if (placement.generation !== input.expectedGeneration) return "stale_generation";
if (placement.tenant_status !== "active") return "suspended";
if (!["active", "draining"].includes(placement.placement_state)) return "provisioning";
return "ready";
}
function canonicalNumber(value: number): string {
if (!Number.isFinite(value)) throw new TypeError("non_finite_json_number");
return Object.is(value, -0) ? "0" : JSON.stringify(value);
}
/** RFC 8785-compatible for the JSON subset used by authorization snapshots. */
export function canonicalJson(value: unknown): string {
if (value === null) return "null";
if (typeof value === "string") return JSON.stringify(value);
if (typeof value === "boolean") return value ? "true" : "false";
if (typeof value === "number") return canonicalNumber(value);
if (Array.isArray(value)) {
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
}
if (typeof value === "object") {
const entries = Object.entries(value as Record<string, unknown>)
.filter(([, item]) => item !== undefined)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
return `{${entries
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
.join(",")}}`;
}
throw new TypeError("unsupported_json_value");
}
function snapshotBytes(payload: RelayAuthorizationSnapshotPayload): Uint8Array {
return new TextEncoder().encode(`${SNAPSHOT_DOMAIN}${canonicalJson(payload)}`);
}
function base64UrlEncode(bytes: Uint8Array): string {
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
}
function base64UrlDecode(value: string): Uint8Array {
if (!/^[A-Za-z0-9_-]+$/u.test(value)) throw new TypeError("invalid_base64url");
const padding = "=".repeat((4 - (value.length % 4)) % 4);
const binary = atob(`${value.replaceAll("-", "+").replaceAll("_", "/")}${padding}`);
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}
function assertSnapshotLifetime(
payload: RelayAuthorizationSnapshotPayload,
nowMs?: number,
): void {
const issuedAt = Date.parse(payload.issued_at);
const expiresAt = Date.parse(payload.expires_at);
if (!Number.isFinite(issuedAt) || !Number.isFinite(expiresAt)) {
throw new RangeError("invalid_snapshot_time");
}
if (expiresAt <= issuedAt) throw new RangeError("invalid_snapshot_lifetime");
if (expiresAt - issuedAt > RELAY_AUTHORIZATION_MAX_TTL_SECONDS * 1_000) {
throw new RangeError("snapshot_ttl_exceeds_maximum");
}
if (nowMs !== undefined && (nowMs < issuedAt - 30_000 || nowMs >= expiresAt)) {
throw new RangeError("snapshot_not_current");
}
}
function assertSnapshotShape(payload: RelayAuthorizationSnapshotPayload): void {
if (payload.snapshot_version !== RELAY_AUTHORIZATION_SNAPSHOT_VERSION) {
throw new RangeError("unsupported_snapshot_version");
}
if (!/^tenant_[0-9a-f]{32}$/u.test(payload.tenant_id)) {
throw new TypeError("invalid_snapshot_tenant");
}
if (!/^node_[A-Za-z0-9._:-]{1,96}$/u.test(payload.relay_node_id)) {
throw new TypeError("invalid_snapshot_node");
}
if (!Number.isSafeInteger(payload.placement_generation) || payload.placement_generation < 1) {
throw new RangeError("invalid_placement_generation");
}
if (!Number.isSafeInteger(payload.authorization_revision) || payload.authorization_revision < 0) {
throw new RangeError("invalid_authorization_revision");
}
for (const device of payload.devices) {
if (typeof device.name !== "string" || device.name.length < 1 || device.name.length > 48) {
throw new TypeError("invalid_snapshot_device_name");
}
if (device.os !== "windows" && device.os !== "linux") {
throw new TypeError("invalid_snapshot_device_os");
}
if (!/^[A-Za-z0-9_-]{43}$/u.test(device.ed25519_public)
|| !/^[A-Za-z0-9_-]{43}$/u.test(device.x25519_public)) {
throw new TypeError("invalid_snapshot_device_public_key");
}
}
const ordered = [...payload.devices].sort((left, right) =>
left.device_id < right.device_id ? -1 : left.device_id > right.device_id ? 1 : 0,
);
if (ordered.some((device, index) => device !== payload.devices[index])) {
throw new TypeError("snapshot_devices_not_ordered");
}
const phones = payload.phones ?? [];
for (const phone of phones) {
if (!/^phone_[A-Za-z0-9._:-]{1,120}$/u.test(phone.phone_id)
|| typeof phone.name !== "string" || phone.name.length < 1 || phone.name.length > 48
|| !/^[0-9a-f]{64}$/u.test(phone.credential_hash)
|| !/^[A-Za-z0-9_-]{43}$/u.test(phone.ed25519_public)
|| !/^[A-Za-z0-9_-]{43}$/u.test(phone.x25519_public)
|| !/^[0-9a-f]{64}$/u.test(phone.identity_fingerprint)) {
throw new TypeError("invalid_snapshot_phone");
}
}
const orderedPhones = [...phones].sort((left, right) =>
left.phone_id < right.phone_id ? -1 : left.phone_id > right.phone_id ? 1 : 0,
);
if (orderedPhones.some((phone, index) => phone !== phones[index])) {
throw new TypeError("snapshot_phones_not_ordered");
}
assertSnapshotLifetime(payload);
}
export async function signRelayAuthorizationSnapshot(input: {
kid: string;
privateKey: CryptoKey;
payload: RelayAuthorizationSnapshotPayload;
}): Promise<SignedRelayAuthorizationSnapshot> {
if (!/^[A-Za-z0-9._:-]{1,128}$/u.test(input.kid)) throw new TypeError("invalid_snapshot_kid");
assertSnapshotShape(input.payload);
const signature = await crypto.subtle.sign(
{ name: "Ed25519" },
input.privateKey,
Uint8Array.from(snapshotBytes(input.payload)).buffer,
);
return {
algorithm: "Ed25519",
kid: input.kid,
payload: input.payload,
signature: base64UrlEncode(new Uint8Array(signature)),
};
}
export async function verifyRelayAuthorizationSnapshot(input: {
snapshot: SignedRelayAuthorizationSnapshot;
publicKey: CryptoKey;
nowMs?: number;
}): Promise<boolean> {
try {
if (input.snapshot.algorithm !== "Ed25519") return false;
assertSnapshotShape(input.snapshot.payload);
assertSnapshotLifetime(input.snapshot.payload, input.nowMs ?? Date.now());
return crypto.subtle.verify(
{ name: "Ed25519" },
input.publicKey,
Uint8Array.from(base64UrlDecode(input.snapshot.signature)).buffer,
Uint8Array.from(snapshotBytes(input.snapshot.payload)).buffer,
);
} catch {
return false;
}
}
File diff suppressed because it is too large Load Diff
+214
View File
@@ -0,0 +1,214 @@
export const ADVANCE_AUTHORIZATION_AFTER_CLAIM_SQL = `
UPDATE tenant_authorization_state
SET revision = revision + 1, updated_at = ?1
WHERE tenant_id = (SELECT id FROM tenant_instances WHERE account_id = ?2)
AND EXISTS (
SELECT 1 FROM device_credentials
WHERE id = ?3 AND host_id = ?4 AND token_hash = ?5 AND status = 'active'
)
`;
export const ADVANCE_AUTHORIZATION_AFTER_REVOKE_SQL = `
UPDATE tenant_authorization_state
SET revision = revision + 1, updated_at = ?1
WHERE tenant_id = (SELECT id FROM tenant_instances WHERE account_id = ?2)
AND EXISTS (
SELECT 1 FROM hosts
WHERE id = ?3 AND lifecycle = 'deactivated' AND deactivated_at = ?4
)
`;
export const CONSUME_PHONE_HANDOFF_SQL = `
UPDATE phone_handoff_tickets
SET consumed_at = ?1, consumed_by_node_id = ?5,
pending_phone_name = ?6, pending_ed25519_public = ?7,
pending_x25519_public = ?8, pending_identity_fingerprint = ?9
WHERE id = ?2 AND ticket_hash = ?3 AND expected_origin = ?4
AND consumed_at IS NULL AND expires_at > ?1
`;
export const DELETE_SUPERSEDED_PENDING_PHONE_ROUTES_SQL = `
DELETE FROM phone_route_handles
WHERE status = 'pending'
AND EXISTS (
SELECT 1
FROM phone_handoff_tickets AS previous
INNER JOIN phone_handoff_tickets AS current ON current.id = ?1
WHERE previous.id != current.id
AND previous.tenant_id = current.tenant_id
AND previous.pending_identity_fingerprint = current.pending_identity_fingerprint
AND previous.completed_phone_id = phone_route_handles.phone_id
AND previous.completed_route_handle_hash = phone_route_handles.handle_hash
AND previous.expires_at <= ?2
AND previous.activation_nonce IS NULL
)
`;
export const DELETE_SUPERSEDED_PENDING_PHONE_PRINCIPALS_SQL = `
DELETE FROM relay_phone_principals
WHERE status = 'pending'
AND EXISTS (
SELECT 1
FROM phone_handoff_tickets AS previous
INNER JOIN phone_handoff_tickets AS current ON current.id = ?1
WHERE previous.id != current.id
AND previous.tenant_id = current.tenant_id
AND previous.pending_identity_fingerprint = current.pending_identity_fingerprint
AND previous.completed_phone_id = relay_phone_principals.phone_id
AND previous.completed_phone_token_hash = relay_phone_principals.token_hash
AND previous.expires_at <= ?2
AND previous.activation_nonce IS NULL
)
`;
export const CLAIM_PHONE_HANDOFF_ACTIVATION_SQL = `
UPDATE phone_handoff_tickets
SET activation_nonce = ?1
WHERE completed_route_handle_hash = ?2
AND completed_phone_token_hash = ?3
AND completed_at > ?4
AND consumed_by_node_id = ?5
AND activation_nonce IS NULL
AND EXISTS (
SELECT 1
FROM phone_route_handles AS handles
INNER JOIN relay_phone_principals AS phones ON phones.phone_id = handles.phone_id
INNER JOIN tenant_placements AS placements ON placements.tenant_id = handles.tenant_id
INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = handles.tenant_id
WHERE handles.handle_hash = ?2 AND handles.status = 'pending'
AND handles.revoked_at IS NULL AND phones.token_hash = ?3
AND phones.status = 'pending' AND phones.revoked_at IS NULL
AND phones.tenant_id = phone_handoff_tickets.tenant_id
AND phones.phone_id = phone_handoff_tickets.completed_phone_id
AND handles.tenant_id = phone_handoff_tickets.tenant_id
AND placements.relay_node_id = ?5
AND placements.state IN ('active', 'draining')
AND authorizations.status = 'active'
)
`;
export const ACTIVATE_PHONE_PRINCIPAL_SQL = `
UPDATE relay_phone_principals
SET status = 'active'
WHERE token_hash = ?2 AND status = 'pending' AND revoked_at IS NULL
AND EXISTS (
SELECT 1 FROM phone_handoff_tickets AS tickets
WHERE tickets.activation_nonce = ?1
AND tickets.completed_phone_id = relay_phone_principals.phone_id
AND tickets.completed_phone_token_hash = ?2
AND tickets.tenant_id = relay_phone_principals.tenant_id
)
`;
export const ACTIVATE_PHONE_ROUTE_SQL = `
UPDATE phone_route_handles
SET status = 'active'
WHERE handle_hash = ?2 AND status = 'pending' AND revoked_at IS NULL
AND EXISTS (
SELECT 1 FROM phone_handoff_tickets AS tickets
WHERE tickets.activation_nonce = ?1
AND tickets.completed_route_handle_hash = ?2
AND tickets.completed_phone_token_hash = ?3
AND tickets.completed_phone_id = phone_route_handles.phone_id
AND tickets.tenant_id = phone_route_handles.tenant_id
)
AND EXISTS (
SELECT 1 FROM relay_phone_principals AS phones
WHERE phones.phone_id = phone_route_handles.phone_id
AND phones.tenant_id = phone_route_handles.tenant_id
AND phones.token_hash = ?3
AND phones.status = 'active' AND phones.revoked_at IS NULL
)
`;
export const ADVANCE_AUTHORIZATION_AFTER_PHONE_ACTIVATION_SQL = `
UPDATE tenant_authorization_state
SET revision = revision + 1, updated_at = ?1
WHERE status = 'active'
AND tenant_id = (
SELECT tenant_id FROM phone_handoff_tickets
WHERE activation_nonce = ?2
)
AND EXISTS (
SELECT 1
FROM phone_handoff_tickets AS tickets
INNER JOIN relay_phone_principals AS phones
ON phones.phone_id = tickets.completed_phone_id
AND phones.tenant_id = tickets.tenant_id
INNER JOIN phone_route_handles AS handles
ON handles.phone_id = tickets.completed_phone_id
AND handles.tenant_id = tickets.tenant_id
AND handles.handle_hash = tickets.completed_route_handle_hash
WHERE tickets.activation_nonce = ?2
AND phones.status = 'active' AND phones.revoked_at IS NULL
AND handles.status = 'active' AND handles.revoked_at IS NULL
AND phones.token_hash = tickets.completed_phone_token_hash
)
`;
export const FINALIZE_PHONE_HANDOFF_ACTIVATION_SQL = `
UPDATE phone_handoff_tickets
SET activated_at = ?1
WHERE activation_nonce = ?2 AND activated_at IS NULL
AND EXISTS (
SELECT 1 FROM relay_phone_principals AS phones
WHERE phones.phone_id = phone_handoff_tickets.completed_phone_id
AND phones.tenant_id = phone_handoff_tickets.tenant_id
AND phones.token_hash = phone_handoff_tickets.completed_phone_token_hash
AND phones.status = 'active' AND phones.revoked_at IS NULL
)
AND EXISTS (
SELECT 1 FROM phone_route_handles AS handles
WHERE handles.phone_id = phone_handoff_tickets.completed_phone_id
AND handles.tenant_id = phone_handoff_tickets.tenant_id
AND handles.handle_hash = phone_handoff_tickets.completed_route_handle_hash
AND handles.status = 'active' AND handles.revoked_at IS NULL
)
`;
export const AUTHORIZE_PHONE_ROUTE_SQL = `
SELECT handles.tenant_id, regions.code AS home_region,
placements.relay_node_id, placements.generation,
phones.phone_id, phones.name, phones.ed25519_public,
phones.x25519_public, phones.identity_fingerprint
FROM phone_route_handles AS handles
INNER JOIN relay_phone_principals AS phones ON phones.phone_id = handles.phone_id
INNER JOIN tenant_placements AS placements ON placements.tenant_id = handles.tenant_id
INNER JOIN relay_regions AS regions ON regions.id = placements.home_region_id
INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = handles.tenant_id
WHERE handles.handle_hash = ?1 AND handles.status = 'active'
AND handles.revoked_at IS NULL AND phones.token_hash = ?2
AND phones.status = 'active' AND phones.revoked_at IS NULL
AND phones.tenant_id = handles.tenant_id
AND authorizations.status = 'active'
AND placements.state IN ('active', 'draining')
`;
export const PHONE_FOR_NODE_REVOCATION_SQL = `
SELECT placements.relay_node_id, phones.status
FROM tenant_placements AS placements
INNER JOIN relay_phone_principals AS phones
ON phones.tenant_id = placements.tenant_id
WHERE placements.tenant_id = ?1 AND phones.phone_id = ?2`;
export const REVOKE_PHONE_PRINCIPAL_SQL = `
UPDATE relay_phone_principals
SET status = 'revoked', revoked_at = ?1
WHERE phone_id = ?2 AND tenant_id = ?3
AND status = 'active' AND revoked_at IS NULL`;
export const REVOKE_PHONE_ROUTES_SQL = `
UPDATE phone_route_handles
SET status = 'revoked', revoked_at = ?1
WHERE phone_id = ?2 AND tenant_id = ?3
AND status = 'active' AND revoked_at IS NULL`;
export const ADVANCE_AUTHORIZATION_AFTER_PHONE_REVOKE_SQL = `
UPDATE tenant_authorization_state
SET revision = revision + 1, updated_at = ?1
WHERE tenant_id = ?2
AND EXISTS (
SELECT 1 FROM relay_phone_principals
WHERE phone_id = ?3 AND tenant_id = ?2
AND status = 'revoked' AND revoked_at = ?1
)`;
+17
View File
@@ -0,0 +1,17 @@
export type MigrationFailureFence = {
state: string;
source_node_id: string;
target_node_id: string;
source_generation: number;
target_generation: number;
};
export function authorityAfterMigrationFailure(record: MigrationFailureFence): {
nodeId: string;
generation: number;
} {
if (record.state === "draining") {
return { nodeId: record.target_node_id, generation: record.target_generation };
}
return { nodeId: record.source_node_id, generation: record.source_generation };
}
+380
View File
@@ -0,0 +1,380 @@
import { ensureDatabase, getD1 } from "./bootstrap";
import { type RelayNodePrincipal } from "./relay-control-plane";
import { DomainError } from "./repository";
import { authorityAfterMigrationFailure } from "./relay-migration-state";
export type RelayMigrationState =
| "quiescing"
| "copying"
| "switching"
| "draining"
| "completed"
| "failed";
type MigrationRecord = {
id: string;
tenant_id: string;
source_node_id: string;
target_node_id: string;
source_generation: number;
target_generation: number;
state: RelayMigrationState;
backup_ref: string | null;
manifest_sha256: string | null;
requested_by: string;
reason: string;
started_at: string;
updated_at: string;
switched_at: string | null;
completed_at: string | null;
last_error_code: string | null;
};
export type RelayMigrationAssignment = {
migration_id: string;
tenant_id: string;
role: "source" | "target";
source_node_id: string;
target_node_id: string;
source_generation: number;
target_generation: number;
state: "quiescing" | "copying" | "switching" | "draining";
backup_ref?: string;
manifest_sha256?: string;
finalize_after?: string;
};
function changed(result: D1Result<unknown>): boolean {
return Number(result.meta.changes ?? 0) === 1;
}
function validTenantId(value: string): boolean {
return /^tenant_[0-9a-f]{32}$/u.test(value);
}
function validNodeId(value: string): boolean {
return /^node_[A-Za-z0-9][A-Za-z0-9._:-]{0,95}$/u.test(value);
}
function validMigrationId(value: string): boolean {
return /^migration_[0-9a-f]{32}$/u.test(value);
}
function validBackupRef(value: string): boolean {
return /^[0-9a-f]{32}\/g[0-9]{20}-[0-9]{8}T[0-9]{6}Z-[0-9a-f]{16}$/u.test(value);
}
function validHash(value: string): boolean {
return /^[0-9a-f]{64}$/u.test(value);
}
function transitionAudit(
db: D1Database,
record: MigrationRecord,
actorId: string,
action: string,
expectedState: RelayMigrationState,
now: string,
): D1PreparedStatement {
return db.prepare(
`INSERT INTO audit_events
(id, actor_id, action, target_type, target_id, reason, created_at)
SELECT ?1, ?2, ?3, 'relay_migration', ?4, ?5, ?6
WHERE EXISTS (
SELECT 1 FROM relay_migrations WHERE id = ?4 AND state = ?7
)`,
).bind(
`audit_${crypto.randomUUID().replaceAll("-", "")}`,
actorId,
action,
record.id,
`migration ${record.tenant_id} ${expectedState}`,
now,
expectedState,
);
}
async function deterministicMigrationId(actorId: string, idempotencyKey: string): Promise<string> {
if (!/^[A-Za-z0-9._:-]{8,128}$/u.test(idempotencyKey)) {
throw new DomainError("invalid_idempotency_key", "迁移幂等键无效");
}
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(`nekonest-cloud/relay-migration/v1\0${actorId}\0${idempotencyKey}`),
);
return `migration_${Array.from(new Uint8Array(digest).slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}
export async function beginRelayMigration(input: {
tenantId: string;
targetNodeId: string;
actorId: string;
reason: string;
idempotencyKey: string;
}): Promise<MigrationRecord> {
await ensureDatabase();
const tenantId = input.tenantId.trim();
const targetNodeId = input.targetNodeId.trim();
const actorId = input.actorId.trim();
const reason = input.reason.trim();
if (!validTenantId(tenantId) || !validNodeId(targetNodeId) || !actorId || reason.length < 8 || reason.length > 500) {
throw new DomainError("invalid_relay_migration", "迁移租户、目标节点或原因无效");
}
const migrationId = await deterministicMigrationId(actorId, input.idempotencyKey.trim());
const db = getD1();
const existing = await db
.prepare("SELECT * FROM relay_migrations WHERE id = ?")
.bind(migrationId)
.first<MigrationRecord>();
if (existing) {
if (
existing.tenant_id !== tenantId ||
existing.target_node_id !== targetNodeId ||
existing.requested_by !== actorId ||
existing.reason !== reason
) {
throw new DomainError("idempotency_conflict", "迁移幂等键已用于不同请求", 409);
}
return existing;
}
const now = new Date().toISOString();
const results = await db.batch([
db
.prepare(
`INSERT INTO relay_migrations
(id, tenant_id, source_node_id, target_node_id,
source_generation, target_generation, state,
requested_by, reason, started_at, updated_at)
SELECT ?1, placements.tenant_id, placements.relay_node_id, targets.id,
placements.generation, placements.generation + 1, 'quiescing',
?4, ?5, ?6, ?6
FROM tenant_placements AS placements
INNER JOIN relay_nodes AS sources ON sources.id = placements.relay_node_id
INNER JOIN relay_nodes AS targets ON targets.id = ?3
WHERE placements.tenant_id = ?2 AND placements.state = 'active'
AND placements.relay_node_id IS NOT NULL
AND sources.status IN ('active', 'draining') AND targets.status = 'active'
AND targets.id <> placements.relay_node_id
AND NOT EXISTS (
SELECT 1 FROM relay_migrations AS active
WHERE active.tenant_id = placements.tenant_id
AND active.state IN ('quiescing', 'copying', 'switching', 'draining')
)
AND (
targets.capacity_tenants = 0 OR
(SELECT COUNT(*) FROM tenant_placements AS occupied
WHERE occupied.relay_node_id = targets.id
AND occupied.state IN ('active', 'draining')) < targets.capacity_tenants
)`,
)
.bind(migrationId, tenantId, targetNodeId, actorId, reason, now),
db
.prepare(
`UPDATE tenant_placements
SET state = 'quiescing', last_error_code = NULL, updated_at = ?1
WHERE tenant_id = ?2 AND state = 'active'
AND EXISTS (
SELECT 1 FROM relay_migrations
WHERE id = ?3 AND tenant_id = ?2
AND source_node_id = tenant_placements.relay_node_id
AND source_generation = tenant_placements.generation
AND state = 'quiescing'
)`,
)
.bind(now, tenantId, migrationId),
db
.prepare(
`INSERT INTO audit_events
(id, actor_id, action, target_type, target_id, reason, created_at)
SELECT ?1, ?2, 'relay.migration.started', 'relay_migration', ?3, ?4, ?5
WHERE EXISTS (
SELECT 1 FROM relay_migrations
WHERE id = ?3 AND tenant_id = ?6 AND state = 'quiescing'
)`,
)
.bind(`audit_${crypto.randomUUID().replaceAll("-", "")}`, actorId, migrationId, reason, now, tenantId),
]);
if (!changed(results[0]) || !changed(results[1])) {
const raced = await db
.prepare("SELECT * FROM relay_migrations WHERE id = ?")
.bind(migrationId)
.first<MigrationRecord>();
if (raced) return raced;
throw new DomainError("relay_migration_conflict", "租户当前不可迁移或目标节点容量不足", 409);
}
const created = await db
.prepare("SELECT * FROM relay_migrations WHERE id = ?")
.bind(migrationId)
.first<MigrationRecord>();
if (!created) throw new DomainError("relay_migration_indeterminate", "迁移创建结果不确定", 503, true, 5);
return created;
}
export async function relayMigrationAssignments(
principal: RelayNodePrincipal,
): Promise<RelayMigrationAssignment[]> {
await ensureDatabase();
const rows = await getD1()
.prepare(
`SELECT * FROM relay_migrations
WHERE state IN ('quiescing', 'copying', 'switching', 'draining')
AND ((state = 'quiescing' AND source_node_id = ?1)
OR (state IN ('copying', 'switching', 'draining') AND target_node_id = ?1))
ORDER BY started_at ASC, id ASC
LIMIT 8`,
)
.bind(principal.nodeId)
.all<MigrationRecord>();
return rows.results.map((row) => {
const assignment: RelayMigrationAssignment = {
migration_id: row.id,
tenant_id: row.tenant_id,
role: row.state === "quiescing" ? "source" : "target",
source_node_id: row.source_node_id,
target_node_id: row.target_node_id,
source_generation: row.source_generation,
target_generation: row.target_generation,
state: row.state as RelayMigrationAssignment["state"],
};
if (row.backup_ref) assignment.backup_ref = row.backup_ref;
if (row.manifest_sha256) assignment.manifest_sha256 = row.manifest_sha256;
if (row.state === "draining" && row.switched_at) {
assignment.finalize_after = new Date(new Date(row.switched_at).getTime() + 5 * 60_000).toISOString();
}
return assignment;
});
}
export async function advanceRelayMigration(input: {
principal: RelayNodePrincipal;
migrationId: string;
action: "quiesced" | "copied" | "switched" | "finalized" | "failed";
backupRef?: string;
manifestSha256?: string;
errorCode?: string;
}): Promise<MigrationRecord> {
await ensureDatabase();
const migrationId = input.migrationId.trim();
if (!validMigrationId(migrationId)) {
throw new DomainError("invalid_relay_migration", "迁移 ID 无效");
}
const db = getD1();
const record = await db
.prepare("SELECT * FROM relay_migrations WHERE id = ?")
.bind(migrationId)
.first<MigrationRecord>();
if (!record) throw new DomainError("relay_migration_not_found", "迁移不存在", 404);
const sourceAction = input.action === "quiesced";
const expectedNode = sourceAction ? record.source_node_id : record.target_node_id;
if (input.action === "failed") {
if (![record.source_node_id, record.target_node_id].includes(input.principal.nodeId)) {
throw new DomainError("relay_migration_forbidden", "节点不属于该迁移", 403);
}
} else if (input.principal.nodeId !== expectedNode) {
throw new DomainError("relay_migration_forbidden", "节点不能推进该迁移阶段", 403);
}
const transitions = {
quiesced: ["quiescing", "copying"],
copied: ["copying", "switching"],
switched: ["switching", "draining"],
finalized: ["draining", "completed"],
} as const;
if (input.action !== "failed" && record.state === transitions[input.action][1]) return record;
if (input.action !== "failed" && record.state !== transitions[input.action][0]) {
throw new DomainError("relay_migration_fence_conflict", "迁移阶段已经变化", 409);
}
const now = new Date().toISOString();
if (input.action === "quiesced") {
const backupRef = input.backupRef?.trim() ?? "";
const manifestSha256 = input.manifestSha256?.trim().toLowerCase() ?? "";
if (!validBackupRef(backupRef) || !validHash(manifestSha256)) {
throw new DomainError("invalid_relay_backup", "迁移备份引用或摘要无效");
}
const results = await db.batch([
db.prepare(
`UPDATE relay_migrations SET state = 'copying', backup_ref = ?1,
manifest_sha256 = ?2, updated_at = ?3
WHERE id = ?4 AND state = 'quiescing' AND source_node_id = ?5`,
).bind(backupRef, manifestSha256, now, migrationId, input.principal.nodeId),
db.prepare(
`UPDATE tenant_placements SET state = 'copying', updated_at = ?1
WHERE tenant_id = ?2 AND relay_node_id = ?3 AND generation = ?4 AND state = 'quiescing'`,
).bind(now, record.tenant_id, record.source_node_id, record.source_generation),
transitionAudit(db, record, input.principal.nodeId, "relay.migration.backup_ready", "copying", now),
]);
if (!changed(results[0]) || !changed(results[1])) throw new DomainError("relay_migration_fence_conflict", "迁移 quiesce 栅栏冲突", 409);
} else if (input.action === "copied") {
if (input.backupRef !== record.backup_ref || input.manifestSha256?.toLowerCase() !== record.manifest_sha256) {
throw new DomainError("relay_backup_mismatch", "目标节点恢复的备份与控制面不一致", 409);
}
const results = await db.batch([
db.prepare(
`UPDATE relay_migrations SET state = 'switching', updated_at = ?1
WHERE id = ?2 AND state = 'copying' AND target_node_id = ?3`,
).bind(now, migrationId, input.principal.nodeId),
db.prepare(
`UPDATE tenant_placements SET state = 'switching', updated_at = ?1
WHERE tenant_id = ?2 AND relay_node_id = ?3 AND generation = ?4 AND state = 'copying'`,
).bind(now, record.tenant_id, record.source_node_id, record.source_generation),
transitionAudit(db, record, input.principal.nodeId, "relay.migration.copy_verified", "switching", now),
]);
if (!changed(results[0]) || !changed(results[1])) throw new DomainError("relay_migration_fence_conflict", "迁移 copy 栅栏冲突", 409);
} else if (input.action === "switched") {
const results = await db.batch([
db.prepare(
`UPDATE relay_migrations SET state = 'draining', switched_at = ?1, updated_at = ?1
WHERE id = ?2 AND state = 'switching' AND target_node_id = ?3`,
).bind(now, migrationId, input.principal.nodeId),
db.prepare(
`UPDATE tenant_placements
SET relay_node_id = ?1, generation = ?2, state = 'draining', updated_at = ?3
WHERE tenant_id = ?4 AND relay_node_id = ?5 AND generation = ?6 AND state = 'switching'`,
).bind(record.target_node_id, record.target_generation, now, record.tenant_id, record.source_node_id, record.source_generation),
transitionAudit(db, record, input.principal.nodeId, "relay.migration.switched", "draining", now),
]);
if (!changed(results[0]) || !changed(results[1])) throw new DomainError("relay_migration_fence_conflict", "迁移 switch 栅栏冲突", 409);
} else if (input.action === "finalized") {
if (!record.switched_at || Date.now() < new Date(record.switched_at).getTime() + 5 * 60_000) {
throw new DomainError("relay_migration_drain_pending", "旧 generation 尚在排空窗口", 409, true, 5);
}
const results = await db.batch([
db.prepare(
`UPDATE relay_migrations SET state = 'completed', completed_at = ?1, updated_at = ?1
WHERE id = ?2 AND state = 'draining' AND target_node_id = ?3`,
).bind(now, migrationId, input.principal.nodeId),
db.prepare(
`UPDATE tenant_placements SET state = 'active', updated_at = ?1
WHERE tenant_id = ?2 AND relay_node_id = ?3 AND generation = ?4 AND state = 'draining'`,
).bind(now, record.tenant_id, record.target_node_id, record.target_generation),
transitionAudit(db, record, input.principal.nodeId, "relay.migration.completed", "completed", now),
]);
if (!changed(results[0]) || !changed(results[1])) throw new DomainError("relay_migration_fence_conflict", "迁移 finalize 栅栏冲突", 409);
} else {
const errorCode = input.errorCode?.trim().toLowerCase() ?? "relay_migration_failed";
if (!/^[a-z][a-z0-9_]{2,63}$/u.test(errorCode) || ["completed", "failed"].includes(record.state)) {
throw new DomainError("invalid_relay_migration_failure", "迁移失败码或阶段无效");
}
const authority = authorityAfterMigrationFailure(record);
const results = await db.batch([
db.prepare(
`UPDATE relay_migrations SET state = 'failed', last_error_code = ?1,
completed_at = ?2, updated_at = ?2
WHERE id = ?3 AND state = ?4`,
).bind(errorCode, now, migrationId, record.state),
db.prepare(
`UPDATE tenant_placements
SET relay_node_id = ?1, generation = ?2, state = 'active',
last_error_code = ?3, updated_at = ?4
WHERE tenant_id = ?5 AND relay_node_id = ?6 AND generation = ?7 AND state = ?8`,
).bind(authority.nodeId, authority.generation, errorCode, now, record.tenant_id, authority.nodeId, authority.generation, record.state),
transitionAudit(db, record, input.principal.nodeId, "relay.migration.failed", "failed", now),
]);
if (!changed(results[0]) || !changed(results[1])) throw new DomainError("relay_migration_fence_conflict", "迁移回滚栅栏冲突", 409);
}
const updated = await db
.prepare("SELECT * FROM relay_migrations WHERE id = ?")
.bind(migrationId)
.first<MigrationRecord>();
if (!updated) throw new DomainError("relay_migration_indeterminate", "迁移状态不确定", 503, true, 5);
return updated;
}
+162
View File
@@ -0,0 +1,162 @@
const ASSERTION_DOMAIN = "nekonest-cloud/relay-mtls-identity/v1";
const MAX_ASSERTION_SKEW_SECONDS = 30;
export type TrustedRelayMtlsIdentity = {
nodeId: string;
spiffeId: string;
certificateFingerprintSha256: string;
};
export const AUTHENTICATE_RELAY_NODE_IDENTITY_SQL = `
UPDATE relay_node_credentials
SET last_used_at = ?1
WHERE node_id = ?2
AND mtls_spiffe_id = ?3
AND certificate_fingerprint_sha256 = ?4
AND status = 'active'
AND issued_at <= ?1
AND revoked_at IS NULL
AND (expires_at IS NULL OR expires_at > ?1)
AND EXISTS (
SELECT 1 FROM relay_nodes
WHERE id = ?2 AND status IN ('active', 'draining')
)`;
export class RelayMtlsIdentityError extends Error {
readonly code: "relay_mtls_unavailable" | "relay_mtls_identity_invalid";
readonly status: 401 | 503;
constructor(
code: "relay_mtls_unavailable" | "relay_mtls_identity_invalid",
message: string,
status: 401 | 503,
) {
super(message);
this.code = code;
this.status = status;
}
}
function assertionTranscript(input: {
method: string;
pathname: string;
nodeId: string;
spiffeId: string;
certificateFingerprintSha256: string;
timestampSeconds: number;
}): Uint8Array {
return new TextEncoder().encode([
ASSERTION_DOMAIN,
input.method.toUpperCase(),
input.pathname,
input.nodeId,
input.spiffeId,
input.certificateFingerprintSha256,
String(input.timestampSeconds),
].join("\0"));
}
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
}
function equalHex(left: string, right: string): boolean {
let difference = left.length ^ right.length;
const length = Math.max(left.length, right.length);
for (let index = 0; index < length; index += 1) {
difference |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
}
return difference === 0;
}
export async function createTrustedRelayMtlsAssertion(input: {
assertionSecret: string;
method: string;
pathname: string;
nodeId: string;
spiffeId: string;
certificateFingerprintSha256: string;
timestampSeconds: number;
}): Promise<string> {
if (input.assertionSecret.length < 32) throw new TypeError("relay_mtls_assertion_secret_too_short");
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(input.assertionSecret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const signature = await crypto.subtle.sign(
"HMAC",
key,
Uint8Array.from(assertionTranscript(input)).buffer,
);
return bytesToHex(new Uint8Array(signature));
}
/**
* Workers cannot inspect a client certificate directly. The production mTLS
* terminator must strip all x-neko-mtls-* headers from untrusted traffic,
* verify the certificate, and inject this short-lived HMAC assertion.
*/
export async function verifyTrustedRelayMtlsIdentity(input: {
request: Request;
assertionSecret: string;
nowMs?: number;
}): Promise<TrustedRelayMtlsIdentity> {
if (input.assertionSecret.length < 32) {
throw new RelayMtlsIdentityError(
"relay_mtls_unavailable",
"Relay mTLS ingress assertion is not configured",
503,
);
}
const headers = input.request.headers;
const nodeId = headers.get("x-neko-relay-node-id")?.trim() ?? "";
const spiffeId = headers.get("x-neko-mtls-spiffe-id")?.trim() ?? "";
const certificateFingerprintSha256 =
headers.get("x-neko-mtls-cert-sha256")?.trim().toLowerCase() ?? "";
const verified = headers.get("x-neko-mtls-verified")?.trim() ?? "";
const timestampRaw = headers.get("x-neko-mtls-timestamp")?.trim() ?? "";
const assertion = headers.get("x-neko-mtls-assertion")?.trim().toLowerCase() ?? "";
const timestampSeconds = Number(timestampRaw);
if (
verified !== "SUCCESS" ||
!/^node_[A-Za-z0-9._:-]{1,96}$/u.test(nodeId) ||
!/^spiffe:\/\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{3,240}$/u.test(spiffeId) ||
!/^[0-9a-f]{64}$/u.test(certificateFingerprintSha256) ||
!/^[0-9a-f]{64}$/u.test(assertion) ||
!Number.isSafeInteger(timestampSeconds)
) {
throw new RelayMtlsIdentityError(
"relay_mtls_identity_invalid",
"Relay mTLS identity is invalid",
401,
);
}
const nowSeconds = Math.floor((input.nowMs ?? Date.now()) / 1_000);
if (Math.abs(nowSeconds - timestampSeconds) > MAX_ASSERTION_SKEW_SECONDS) {
throw new RelayMtlsIdentityError(
"relay_mtls_identity_invalid",
"Relay mTLS assertion has expired",
401,
);
}
const expected = await createTrustedRelayMtlsAssertion({
assertionSecret: input.assertionSecret,
method: input.request.method,
pathname: new URL(input.request.url).pathname,
nodeId,
spiffeId,
certificateFingerprintSha256,
timestampSeconds,
});
if (!equalHex(assertion, expected)) {
throw new RelayMtlsIdentityError(
"relay_mtls_identity_invalid",
"Relay mTLS assertion is invalid",
401,
);
}
return { nodeId, spiffeId, certificateFingerprintSha256 };
}
+68
View File
@@ -0,0 +1,68 @@
export function relayPurgeCompletionAuditId(purgeId: string): string {
return `audit_${purgeId.slice("purge_".length)}`;
}
export function relayPurgeRecordCanReturn(
state: string,
completedProofValid: boolean,
): boolean {
return state !== "completed" || completedProofValid;
}
export const COMPLETED_RELAY_PURGE_PROOF_SQL = `
SELECT EXISTS (
SELECT 1 FROM relay_purge_jobs AS jobs
INNER JOIN audit_events AS audits
ON audits.id = ?2
AND audits.actor_id = jobs.relay_node_id
AND audits.action = 'relay.purge.completed'
AND audits.target_type = 'relay_purge'
AND audits.target_id = jobs.id
AND json_extract(audits.after_json, '$.evidence_sha256') = jobs.evidence_sha256
WHERE jobs.id = ?1 AND jobs.state = 'completed'
AND jobs.evidence_sha256 = ?3
AND EXISTS (
SELECT 1 FROM tenant_authorization_state
WHERE tenant_id = jobs.tenant_id AND status = 'deleted'
)
AND EXISTS (
SELECT 1 FROM tenant_placements
WHERE tenant_id = jobs.tenant_id AND relay_node_id IS NULL
AND generation = jobs.placement_generation + 1 AND state = 'deleted'
)
AND EXISTS (
SELECT 1 FROM tenant_instances
WHERE id = jobs.tenant_id AND lifecycle = 'deleted'
AND desired_state = 'deleted' AND observed_state = 'deleted'
)
AND EXISTS (
SELECT 1 FROM account_deletion_requests
WHERE id = jobs.deletion_request_id AND status = 'relay_purged'
)
AND EXISTS (
SELECT 1 FROM accounts
WHERE id = (
SELECT account_id FROM account_deletion_requests
WHERE id = jobs.deletion_request_id
) AND status = 'relay_purged'
)
AND NOT EXISTS (
SELECT 1 FROM device_credentials
WHERE host_id IN (
SELECT hosts.id FROM hosts
INNER JOIN tenant_instances AS tenants ON tenants.account_id = hosts.account_id
WHERE tenants.id = jobs.tenant_id
)
)
AND NOT EXISTS (SELECT 1 FROM phone_route_handles WHERE tenant_id = jobs.tenant_id)
AND NOT EXISTS (SELECT 1 FROM relay_phone_principals WHERE tenant_id = jobs.tenant_id)
AND NOT EXISTS (SELECT 1 FROM phone_handoff_tickets WHERE tenant_id = jobs.tenant_id)
AND NOT EXISTS (
SELECT 1 FROM hosts
WHERE account_id = (
SELECT account_id FROM tenant_instances WHERE id = jobs.tenant_id
)
AND (COALESCE(lifecycle, '') != 'deactivated'
OR COALESCE(slot_state, '') != 'released')
)
) AS proof_valid`;
+443
View File
@@ -0,0 +1,443 @@
import { ensureDatabase, getD1 } from "./bootstrap";
import { type RelayNodePrincipal } from "./relay-control-plane";
import {
COMPLETED_RELAY_PURGE_PROOF_SQL,
relayPurgeCompletionAuditId,
relayPurgeRecordCanReturn,
} from "./relay-purge-proof";
import { DomainError } from "./repository";
export type RelayPurgeRecord = {
id: string;
deletion_request_id: string;
tenant_id: string;
relay_node_id: string;
placement_generation: number;
state: "quiescing" | "completed" | "failed";
requested_by: string;
reason: string;
started_at: string;
updated_at: string;
completed_at: string | null;
evidence_sha256: string | null;
last_error_code: string | null;
};
export type RelayPurgeAssignment = {
purge_id: string;
tenant_id: string;
placement_generation: number;
};
function validDeletionId(value: string): boolean {
return /^deletion_[0-9a-f]{32}$/u.test(value);
}
function validPurgeId(value: string): boolean {
return /^purge_[0-9a-f]{32}$/u.test(value);
}
function validErrorCode(value: string): boolean {
return /^[a-z][a-z0-9_]{2,63}$/u.test(value);
}
function changed(result: D1Result<unknown>): boolean {
return Number(result.meta.changes ?? 0) === 1;
}
async function reloadPurge(id: string): Promise<RelayPurgeRecord> {
const record = await getD1()
.prepare("SELECT * FROM relay_purge_jobs WHERE id = ?")
.bind(id)
.first<RelayPurgeRecord>();
if (!record) {
throw new DomainError("relay_purge_indeterminate", "租户删除状态不确定", 503, true, 5);
}
return validatePurgeRecordForReturn(record);
}
async function completedPurgeProofIsValid(record: RelayPurgeRecord): Promise<boolean> {
if (record.state !== "completed" || !/^[0-9a-f]{64}$/u.test(record.evidence_sha256 ?? "")) {
return false;
}
const completionAuditId = relayPurgeCompletionAuditId(record.id);
const proof = await getD1()
.prepare(COMPLETED_RELAY_PURGE_PROOF_SQL)
.bind(record.id, completionAuditId, record.evidence_sha256)
.first<{ proof_valid: number }>();
return proof?.proof_valid === 1;
}
async function validatePurgeRecordForReturn(
record: RelayPurgeRecord,
): Promise<RelayPurgeRecord> {
const completedProofValid = record.state === "completed"
? await completedPurgeProofIsValid(record)
: false;
if (!relayPurgeRecordCanReturn(record.state, completedProofValid)) {
throw new DomainError(
"relay_purge_indeterminate",
"租户删除记录缺少完整完成证明",
503,
true,
5,
);
}
return record;
}
export async function beginRelayPurge(input: {
deletionRequestId: string;
actorId: string;
reason: string;
confirmation: string;
}): Promise<RelayPurgeRecord> {
await ensureDatabase();
const deletionRequestId = input.deletionRequestId.trim();
const actorId = input.actorId.trim();
const reason = input.reason.trim();
if (!validDeletionId(deletionRequestId) || !actorId || reason.length < 8 || reason.length > 500) {
throw new DomainError("invalid_relay_purge", "删除申请、操作者或原因无效");
}
if (input.confirmation !== "DELETE TENANT DATA") {
throw new DomainError("relay_purge_confirmation_required", "必须明确确认永久删除租户数据");
}
const db = getD1();
const existing = await db
.prepare("SELECT * FROM relay_purge_jobs WHERE deletion_request_id = ?")
.bind(deletionRequestId)
.first<RelayPurgeRecord>();
const now = new Date().toISOString();
if (existing) {
if (existing.state !== "failed") return validatePurgeRecordForReturn(existing);
const results = await db.batch([
db.prepare(
`UPDATE relay_purge_jobs
SET state = 'quiescing', requested_by = ?1, reason = ?2,
updated_at = ?3, completed_at = NULL, last_error_code = NULL
WHERE id = ?4 AND state = 'failed'`,
).bind(actorId, reason, now, existing.id),
db.prepare(
`UPDATE tenant_placements SET state = 'deleting', last_error_code = NULL, updated_at = ?1
WHERE tenant_id = ?2 AND relay_node_id = ?3 AND generation = ?4
AND state IN ('deleting', 'deletion_failed')`,
).bind(now, existing.tenant_id, existing.relay_node_id, existing.placement_generation),
db.prepare(
`INSERT INTO audit_events
(id, actor_id, action, target_type, target_id, reason, created_at)
SELECT ?1, ?2, 'relay.purge.retried', 'relay_purge', ?3, ?4, ?5
WHERE EXISTS (SELECT 1 FROM relay_purge_jobs WHERE id = ?3 AND state = 'quiescing')`,
).bind(`audit_${crypto.randomUUID().replaceAll("-", "")}`, actorId, existing.id, reason, now),
]);
if (!changed(results[0])) {
throw new DomainError("relay_purge_conflict", "租户删除重试发生冲突", 409);
}
return reloadPurge(existing.id);
}
const purgeId = `purge_${crypto.randomUUID().replaceAll("-", "")}`;
try {
const results = await db.batch([
db.prepare(
`INSERT INTO relay_purge_jobs
(id, deletion_request_id, tenant_id, relay_node_id, placement_generation,
state, requested_by, reason, started_at, updated_at)
SELECT ?1, deletions.id, tenants.id, placements.relay_node_id,
placements.generation, 'quiescing', ?3, ?4, ?5, ?5
FROM account_deletion_requests AS deletions
INNER JOIN tenant_instances AS tenants ON tenants.account_id = deletions.account_id
INNER JOIN tenant_placements AS placements ON placements.tenant_id = tenants.id
INNER JOIN tenant_authorization_state AS authorizations ON authorizations.tenant_id = tenants.id
INNER JOIN relay_nodes AS nodes ON nodes.id = placements.relay_node_id
WHERE deletions.id = ?2 AND deletions.status = 'requested'
AND placements.state = 'active' AND authorizations.status = 'active'
AND nodes.status IN ('active', 'draining')
AND NOT EXISTS (
SELECT 1 FROM relay_migrations AS migrations
WHERE migrations.tenant_id = tenants.id
AND migrations.state IN ('quiescing', 'copying', 'switching', 'draining')
)
AND NOT EXISTS (SELECT 1 FROM relay_purge_jobs AS purges WHERE purges.tenant_id = tenants.id)`,
).bind(purgeId, deletionRequestId, actorId, reason, now),
db.prepare(
`UPDATE tenant_authorization_state
SET status = 'suspended', revision = revision + 1, updated_at = ?1
WHERE tenant_id = (SELECT tenant_id FROM relay_purge_jobs WHERE id = ?2)
AND status = 'active'`,
).bind(now, purgeId),
db.prepare(
`UPDATE tenant_placements
SET state = 'deleting', last_error_code = NULL, updated_at = ?1
WHERE tenant_id = (SELECT tenant_id FROM relay_purge_jobs WHERE id = ?2)
AND state = 'active'`,
).bind(now, purgeId),
db.prepare(
`UPDATE tenant_instances
SET lifecycle = 'deleting', desired_state = 'deleted', relay_ready = 0, updated_at = ?1
WHERE id = (SELECT tenant_id FROM relay_purge_jobs WHERE id = ?2)`,
).bind(now, purgeId),
db.prepare(
`UPDATE account_deletion_requests
SET status = 'processing', updated_at = ?1
WHERE id = ?2 AND status = 'requested'
AND EXISTS (SELECT 1 FROM relay_purge_jobs WHERE id = ?3)`,
).bind(now, deletionRequestId, purgeId),
db.prepare(
`UPDATE accounts SET status = 'deleting', updated_at = ?1
WHERE id = (SELECT account_id FROM account_deletion_requests WHERE id = ?2)
AND EXISTS (SELECT 1 FROM relay_purge_jobs WHERE id = ?3)`,
).bind(now, deletionRequestId, purgeId),
db.prepare(
`INSERT INTO audit_events
(id, actor_id, action, target_type, target_id, reason, created_at)
SELECT ?1, ?2, 'relay.purge.started', 'relay_purge', ?3, ?4, ?5
WHERE EXISTS (SELECT 1 FROM relay_purge_jobs WHERE id = ?3)`,
).bind(`audit_${crypto.randomUUID().replaceAll("-", "")}`, actorId, purgeId, reason, now),
]);
if (!changed(results[0])) {
throw new DomainError("relay_purge_conflict", "租户不可删除、正在迁移或已经删除", 409);
}
} catch (error) {
const raced = await db
.prepare("SELECT * FROM relay_purge_jobs WHERE deletion_request_id = ?")
.bind(deletionRequestId)
.first<RelayPurgeRecord>();
if (raced) return validatePurgeRecordForReturn(raced);
throw error;
}
return reloadPurge(purgeId);
}
export async function relayPurgeAssignments(
principal: RelayNodePrincipal,
): Promise<RelayPurgeAssignment[]> {
await ensureDatabase();
const rows = await getD1()
.prepare(
`SELECT id AS purge_id, tenant_id, placement_generation
FROM relay_purge_jobs
WHERE relay_node_id = ?1 AND state = 'quiescing'
ORDER BY started_at ASC, id ASC
LIMIT 4`,
)
.bind(principal.nodeId)
.all<RelayPurgeAssignment>();
return rows.results;
}
export async function advanceRelayPurge(input: {
principal: RelayNodePrincipal;
purgeId: string;
action: "completed" | "failed";
evidenceSha256?: string;
errorCode?: string;
}): Promise<RelayPurgeRecord> {
await ensureDatabase();
const purgeId = input.purgeId.trim();
if (!validPurgeId(purgeId)) {
throw new DomainError("invalid_relay_purge", "租户删除任务无效");
}
const db = getD1();
const record = await db
.prepare("SELECT * FROM relay_purge_jobs WHERE id = ?")
.bind(purgeId)
.first<RelayPurgeRecord>();
if (!record) throw new DomainError("relay_purge_not_found", "租户删除任务不存在", 404);
if (record.relay_node_id !== input.principal.nodeId) {
throw new DomainError("relay_purge_forbidden", "节点不属于该删除任务", 403);
}
if (record.state === input.action) {
return validatePurgeRecordForReturn(record);
}
if (record.state !== "quiescing") {
throw new DomainError("relay_purge_fence_conflict", "租户删除阶段已经变化", 409);
}
const now = new Date().toISOString();
if (input.action === "failed") {
const errorCode = input.errorCode?.trim().toLowerCase() ?? "relay_purge_failed";
if (!validErrorCode(errorCode)) {
throw new DomainError("invalid_relay_purge_failure", "租户删除失败码无效");
}
const results = await db.batch([
db.prepare(
`UPDATE relay_purge_jobs
SET state = 'failed', last_error_code = ?1, completed_at = ?2, updated_at = ?2
WHERE id = ?3 AND state = 'quiescing' AND relay_node_id = ?4`,
).bind(errorCode, now, purgeId, input.principal.nodeId),
db.prepare(
`UPDATE tenant_placements SET state = 'deletion_failed', last_error_code = ?1, updated_at = ?2
WHERE tenant_id = ?3 AND relay_node_id = ?4 AND generation = ?5 AND state = 'deleting'`,
).bind(errorCode, now, record.tenant_id, record.relay_node_id, record.placement_generation),
db.prepare(
`INSERT INTO audit_events
(id, actor_id, action, target_type, target_id, reason, created_at)
SELECT ?1, ?2, 'relay.purge.failed', 'relay_purge', ?3, ?4, ?5
WHERE EXISTS (SELECT 1 FROM relay_purge_jobs WHERE id = ?3 AND state = 'failed')`,
).bind(`audit_${crypto.randomUUID().replaceAll("-", "")}`, input.principal.nodeId, purgeId, errorCode, now),
]);
if (!changed(results[0])) {
throw new DomainError("relay_purge_fence_conflict", "租户删除失败栅栏冲突", 409);
}
return reloadPurge(purgeId);
}
const evidence = input.evidenceSha256?.trim().toLowerCase() ?? "";
if (!/^[0-9a-f]{64}$/u.test(evidence)) {
throw new DomainError("invalid_relay_purge_evidence", "租户删除证据摘要无效");
}
const completionAuditId = relayPurgeCompletionAuditId(purgeId);
const results = await db.batch([
db.prepare(
`DELETE FROM device_registration_replays
WHERE pairing_id IN (
SELECT pairings.id FROM pairing_requests AS pairings
INNER JOIN tenant_instances AS tenants ON tenants.account_id = pairings.account_id
WHERE tenants.id = ?1
)`,
).bind(record.tenant_id),
db.prepare(
`DELETE FROM pairing_claim_attempts
WHERE pairing_request_id IN (
SELECT pairings.id FROM pairing_requests AS pairings
INNER JOIN tenant_instances AS tenants ON tenants.account_id = pairings.account_id
WHERE tenants.id = ?1
)`,
).bind(record.tenant_id),
db.prepare(
`DELETE FROM device_credentials
WHERE host_id IN (
SELECT hosts.id FROM hosts
INNER JOIN tenant_instances AS tenants ON tenants.account_id = hosts.account_id
WHERE tenants.id = ?1
)`,
).bind(record.tenant_id),
db.prepare("DELETE FROM phone_route_handles WHERE tenant_id = ?").bind(record.tenant_id),
db.prepare("DELETE FROM relay_phone_principals WHERE tenant_id = ?").bind(record.tenant_id),
db.prepare("DELETE FROM phone_handoff_tickets WHERE tenant_id = ?").bind(record.tenant_id),
db.prepare(
`DELETE FROM pairing_requests
WHERE account_id = (SELECT account_id FROM tenant_instances WHERE id = ?1)`,
).bind(record.tenant_id),
db.prepare(
`UPDATE hosts SET name = 'Deleted host', lifecycle = 'deactivated', slot_state = 'released',
connection_state = 'unknown', daemon_version = NULL, recovery_fingerprint = NULL,
ed25519_public = NULL, x25519_public = NULL, identity_fingerprint = NULL,
claim_request_id = NULL, last_seen_at = NULL, deactivated_at = ?1
WHERE account_id = (SELECT account_id FROM tenant_instances WHERE id = ?2)`,
).bind(now, record.tenant_id),
db.prepare(
`UPDATE tenant_authorization_state
SET status = 'deleted',
revision = revision + CASE WHEN status = 'suspended' THEN 1 ELSE 0 END,
updated_at = ?1
WHERE tenant_id = ?2 AND status IN ('suspended', 'deleted')`,
).bind(now, record.tenant_id),
db.prepare(
`UPDATE tenant_placements
SET relay_node_id = NULL,
generation = CASE WHEN state = 'deleting' THEN generation + 1 ELSE generation END,
state = 'deleted',
last_error_code = NULL, updated_at = ?1
WHERE tenant_id = ?2 AND (
(relay_node_id = ?3 AND generation = ?4 AND state = 'deleting')
OR (relay_node_id IS NULL AND generation = ?4 + 1 AND state = 'deleted')
)`,
).bind(now, record.tenant_id, record.relay_node_id, record.placement_generation),
db.prepare(
`UPDATE tenant_instances
SET lifecycle = 'deleted', desired_state = 'deleted', observed_state = 'deleted',
relay_ready = 0, runtime_ref = NULL, relay_origin = NULL,
secret_bundle_ref = NULL, tombstoned_at = ?1, updated_at = ?1
WHERE id = ?2 AND lifecycle IN ('deleting', 'deleted') AND desired_state = 'deleted'`,
).bind(now, record.tenant_id),
db.prepare(
`UPDATE account_deletion_requests
SET status = 'relay_purged', updated_at = ?1
WHERE id = ?2 AND status IN ('processing', 'relay_purged')`,
).bind(now, record.deletion_request_id),
db.prepare(
`UPDATE accounts SET status = 'relay_purged', updated_at = ?1
WHERE id = (SELECT account_id FROM account_deletion_requests WHERE id = ?2)
AND status IN ('deleting', 'relay_purged')`,
).bind(now, record.deletion_request_id),
db.prepare(
`INSERT OR IGNORE INTO audit_events
(id, actor_id, action, target_type, target_id, reason, after_json, created_at)
SELECT ?1, ?2, 'relay.purge.completed', 'relay_purge', ?3,
'Relay 实时数据与备份已完成应用层逻辑删除',
json_object('evidence_sha256', ?4), ?5
WHERE EXISTS (
SELECT 1 FROM relay_purge_jobs
WHERE id = ?3 AND state = 'quiescing' AND relay_node_id = ?2
)
AND EXISTS (
SELECT 1 FROM tenant_authorization_state
WHERE tenant_id = ?6 AND status = 'deleted'
)
AND EXISTS (
SELECT 1 FROM tenant_placements
WHERE tenant_id = ?6 AND relay_node_id IS NULL
AND generation = ?7 + 1 AND state = 'deleted'
)
AND EXISTS (
SELECT 1 FROM tenant_instances
WHERE id = ?6 AND lifecycle = 'deleted'
AND desired_state = 'deleted' AND observed_state = 'deleted'
)
AND EXISTS (
SELECT 1 FROM account_deletion_requests
WHERE id = ?8 AND status = 'relay_purged'
)
AND EXISTS (
SELECT 1 FROM accounts
WHERE id = (SELECT account_id FROM account_deletion_requests WHERE id = ?8)
AND status = 'relay_purged'
)
AND NOT EXISTS (
SELECT 1 FROM device_credentials
WHERE host_id IN (
SELECT hosts.id FROM hosts
INNER JOIN tenant_instances AS tenants ON tenants.account_id = hosts.account_id
WHERE tenants.id = ?6
)
)
AND NOT EXISTS (SELECT 1 FROM phone_route_handles WHERE tenant_id = ?6)
AND NOT EXISTS (SELECT 1 FROM relay_phone_principals WHERE tenant_id = ?6)
AND NOT EXISTS (SELECT 1 FROM phone_handoff_tickets WHERE tenant_id = ?6)
AND NOT EXISTS (
SELECT 1 FROM hosts
WHERE account_id = (SELECT account_id FROM tenant_instances WHERE id = ?6)
AND (COALESCE(lifecycle, '') != 'deactivated'
OR COALESCE(slot_state, '') != 'released')
)`,
).bind(
completionAuditId,
input.principal.nodeId,
purgeId,
evidence,
now,
record.tenant_id,
record.placement_generation,
record.deletion_request_id,
),
db.prepare(
`UPDATE relay_purge_jobs
SET state = 'completed', evidence_sha256 = ?1, completed_at = ?2,
updated_at = ?2, last_error_code = NULL
WHERE id = ?3 AND state = 'quiescing' AND relay_node_id = ?4
AND EXISTS (
SELECT 1 FROM audit_events
WHERE id = ?5 AND actor_id = ?4
AND action = 'relay.purge.completed'
AND target_type = 'relay_purge' AND target_id = ?3
AND json_extract(after_json, '$.evidence_sha256') = ?1
)`,
).bind(evidence, now, purgeId, input.principal.nodeId, completionAuditId),
]);
if (!changed(results.at(-1)!)) {
const current = await reloadPurge(purgeId);
if (current.state === "completed") return validatePurgeRecordForReturn(current);
throw new DomainError("relay_purge_indeterminate", "租户删除关键状态未完整收口", 503, true, 5);
}
return reloadPurge(purgeId);
}
+52
View File
@@ -0,0 +1,52 @@
import { DomainError } from "./domain-error.ts";
export type RelayPlacementRouteRecord = {
tenant_id: string;
tenant_status: string;
home_region: string;
relay_node_id: string | null;
generation: number;
placement_state: string;
authorization_revision: number;
internal_endpoint_ref?: string | null;
relay_node_status?: string | null;
};
export type RelayRouteResolution = {
relay_node_id: string;
placement_generation: number;
home_region: string;
local: boolean;
endpoint_ref?: string;
};
export function isWritableRelayPlacement(
placementState: string | null | undefined,
relayNodeStatus: string | null | undefined,
): boolean {
return ["active", "draining"].includes(placementState ?? "") && relayNodeStatus === "active";
}
export function resolveRelayPlacementRoute(
placement: RelayPlacementRouteRecord,
currentNodeId: string,
): RelayRouteResolution {
if (!placement.relay_node_id || !["active", "draining"].includes(placement.placement_state)) {
throw new DomainError("service_provisioning", "租户 Relay 正在准备", 503, true, 5);
}
if (placement.relay_node_status !== "active") {
throw new DomainError("region_unavailable", "目标 Relay 节点当前不可用", 503, true, 5);
}
const local = placement.relay_node_id === currentNodeId;
const endpointRef = placement.internal_endpoint_ref?.trim() ?? "";
if (!local && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(endpointRef)) {
throw new DomainError("route_unavailable", "目标 Relay 内部路由不可用", 503, true, 5);
}
return {
relay_node_id: placement.relay_node_id,
placement_generation: placement.generation,
home_region: placement.home_region,
local,
...(local ? {} : { endpoint_ref: endpointRef }),
};
}
+4005
View File
File diff suppressed because it is too large Load Diff
+103
View File
@@ -0,0 +1,103 @@
import { ensureDatabase } from "./bootstrap";
import { DomainError, runRetentionMaintenance, type RetentionMaintenanceResult } from "./repository";
import {
ACQUIRE_RETENTION_JOB_SQL,
COMPLETE_RETENTION_JOB_SQL,
FAIL_RETENTION_JOB_SQL,
RETENTION_JOB_KEY,
RETENTION_RUNNING_STALE_MS,
} from "./retention";
export type ScheduledRetentionOutcome =
| { status: "skipped"; reason: "already_running" }
| { status: "succeeded"; result: RetentionMaintenanceResult };
function scheduledTimestamp(value: number): string {
if (!Number.isFinite(value)) {
throw new TypeError("scheduled retention requires a finite timestamp");
}
const timestamp = new Date(value);
if (!Number.isFinite(timestamp.getTime())) {
throw new TypeError("scheduled retention requires a valid timestamp");
}
return timestamp.toISOString();
}
function safeErrorCode(error: unknown): string {
if (error instanceof DomainError && /^[a-z0-9_]{3,64}$/.test(error.code)) {
return error.code;
}
return "retention_maintenance_failed";
}
export async function runScheduledRetentionMaintenance(
db: D1Database,
scheduledTime: number,
): Promise<ScheduledRetentionOutcome> {
await ensureDatabase();
const scheduledAt = scheduledTimestamp(scheduledTime);
const startedAt = new Date().toISOString();
const staleBefore = new Date(
new Date(startedAt).getTime() - RETENTION_RUNNING_STALE_MS,
).toISOString();
const runId = `maintenance_${crypto.randomUUID().replaceAll("-", "")}`;
const acquired = await db
.prepare(ACQUIRE_RETENTION_JOB_SQL)
.bind(
RETENTION_JOB_KEY,
runId,
"scheduled",
scheduledAt,
startedAt,
startedAt,
startedAt,
staleBefore,
)
.run();
if (Number(acquired.meta.changes ?? 0) !== 1) {
return { status: "skipped", reason: "already_running" };
}
try {
const result = await runRetentionMaintenance({
actorId: "system:scheduled-retention",
confirmed: true,
reason: "每日自动清理已经到期的技术记录",
now: startedAt,
});
const completedAt = new Date().toISOString();
const completion = await db
.prepare(COMPLETE_RETENTION_JOB_SQL)
.bind(
completedAt,
completedAt,
JSON.stringify(result),
completedAt,
RETENTION_JOB_KEY,
runId,
)
.run();
if (Number(completion.meta.changes ?? 0) !== 1) {
throw new DomainError(
"retention_run_fenced",
"到期数据清理完成,但运行状态已经被更新的任务接管",
409,
);
}
return { status: "succeeded", result };
} catch (error) {
const failedAt = new Date().toISOString();
await db
.prepare(FAIL_RETENTION_JOB_SQL)
.bind(
failedAt,
safeErrorCode(error),
failedAt,
RETENTION_JOB_KEY,
runId,
)
.run();
throw error;
}
}
+271
View File
@@ -0,0 +1,271 @@
export const CLAIM_RATE_RETENTION_MS = 24 * 60 * 60_000;
export const CLAIM_ATTEMPT_RETENTION_MS = 30 * 24 * 60 * 60_000;
export const HANDOFF_TICKET_RETENTION_MS = 24 * 60 * 60_000;
export const RETENTION_JOB_KEY = "retention_cleanup";
export const RETENTION_CRON = "17 18 * * *";
export const RETENTION_RUNNING_STALE_MS = 30 * 60_000;
export const RETENTION_SUCCESS_OVERDUE_MS = 36 * 60 * 60_000;
const RETENTION_FUTURE_SKEW_MS = 60_000;
export type RetentionCutoffs = {
now: string;
claimRateBefore: string;
claimAttemptBefore: string;
handoffTicketBefore: string;
};
export type RetentionJobRecord = {
key: string;
state: string;
run_id: string | null;
trigger: string | null;
scheduled_at: string | null;
started_at: string | null;
completed_at: string | null;
last_success_at: string | null;
result_json: string | null;
error_code: string | null;
consecutive_failures: number;
run_count: number;
created_at: string;
updated_at: string;
};
export type RetentionJobHealth = {
state: "never_run" | "running" | "healthy" | "overdue" | "failed" | "stalled" | "invalid";
trigger: "scheduled" | "manual" | null;
startedAt: string | null;
completedAt: string | null;
lastSuccessAt: string | null;
errorCode: string | null;
consecutiveFailures: number;
runCount: number;
ageSeconds: number | null;
};
function parseTimestamp(value: string | null): number | null {
if (!value) return null;
const timestamp = new Date(value).getTime();
return Number.isFinite(timestamp) ? timestamp : null;
}
export function deriveRetentionJobHealth(
record: RetentionJobRecord | null,
now = new Date().toISOString(),
): RetentionJobHealth {
if (!record) {
return {
state: "never_run",
trigger: null,
startedAt: null,
completedAt: null,
lastSuccessAt: null,
errorCode: null,
consecutiveFailures: 0,
runCount: 0,
ageSeconds: null,
};
}
const nowMilliseconds = new Date(now).getTime();
const scheduledMilliseconds = parseTimestamp(record.scheduled_at);
const startedMilliseconds = parseTimestamp(record.started_at);
const completedMilliseconds = parseTimestamp(record.completed_at);
const successMilliseconds = parseTimestamp(record.last_success_at);
const countsValid = Number.isSafeInteger(record.run_count) && record.run_count >= 1
&& Number.isSafeInteger(record.consecutive_failures) && record.consecutive_failures >= 0;
const errorCodeValid = record.error_code === null
|| /^[a-z0-9_]{3,64}$/.test(record.error_code);
const trigger: RetentionJobHealth["trigger"] = record.trigger === "scheduled" || record.trigger === "manual"
? record.trigger
: null;
const base = {
trigger,
startedAt: record.started_at,
completedAt: record.completed_at,
lastSuccessAt: record.last_success_at,
errorCode: record.error_code,
consecutiveFailures: countsValid ? record.consecutive_failures : 0,
runCount: countsValid ? record.run_count : 0,
};
const invalid = !Number.isFinite(nowMilliseconds)
|| record.key !== RETENTION_JOB_KEY
|| !countsValid
|| !errorCodeValid
|| !trigger
|| !record.run_id
|| scheduledMilliseconds === null
|| !startedMilliseconds
|| scheduledMilliseconds > nowMilliseconds + RETENTION_FUTURE_SKEW_MS
|| startedMilliseconds > nowMilliseconds + RETENTION_FUTURE_SKEW_MS
|| (record.completed_at !== null && completedMilliseconds === null)
|| (record.last_success_at !== null && successMilliseconds === null)
|| (completedMilliseconds !== null && completedMilliseconds > nowMilliseconds + RETENTION_FUTURE_SKEW_MS)
|| (successMilliseconds !== null && successMilliseconds > nowMilliseconds + RETENTION_FUTURE_SKEW_MS)
|| (completedMilliseconds !== null && completedMilliseconds < startedMilliseconds)
|| (successMilliseconds !== null && completedMilliseconds !== null && successMilliseconds > completedMilliseconds);
if (invalid) {
return { state: "invalid", ...base, ageSeconds: null };
}
if (record.state === "running") {
if (record.error_code !== null) {
return { state: "invalid", ...base, ageSeconds: null };
}
const ageMilliseconds = Math.max(0, nowMilliseconds - startedMilliseconds);
return {
state: ageMilliseconds > RETENTION_RUNNING_STALE_MS ? "stalled" : "running",
...base,
ageSeconds: Math.floor(ageMilliseconds / 1_000),
};
}
if (record.state === "failed") {
if (record.error_code === null) {
return { state: "invalid", ...base, ageSeconds: null };
}
const failureAgeMilliseconds = Math.max(
0,
nowMilliseconds - (completedMilliseconds ?? startedMilliseconds),
);
return {
state: "failed",
...base,
ageSeconds: Math.floor(failureAgeMilliseconds / 1_000),
};
}
if (record.state !== "succeeded"
|| record.error_code !== null
|| successMilliseconds === null
|| completedMilliseconds === null) {
return { state: "invalid", ...base, ageSeconds: null };
}
const ageMilliseconds = Math.max(0, nowMilliseconds - successMilliseconds);
return {
state: ageMilliseconds > RETENTION_SUCCESS_OVERDUE_MS ? "overdue" : "healthy",
...base,
ageSeconds: Math.floor(ageMilliseconds / 1_000),
};
}
export function getRetentionCutoffs(now: string): RetentionCutoffs {
const nowMilliseconds = new Date(now).getTime();
if (!Number.isFinite(nowMilliseconds)) {
throw new TypeError("retention maintenance requires an ISO timestamp");
}
return {
now: new Date(nowMilliseconds).toISOString(),
claimRateBefore: new Date(nowMilliseconds - CLAIM_RATE_RETENTION_MS).toISOString(),
claimAttemptBefore: new Date(nowMilliseconds - CLAIM_ATTEMPT_RETENTION_MS).toISOString(),
handoffTicketBefore: new Date(nowMilliseconds - HANDOFF_TICKET_RETENTION_MS).toISOString(),
};
}
/**
* Pairing history remains available, but its credential-derived hash does not.
* The deterministic tombstone is unique because pairing request IDs are unique.
*/
export const RETIRE_EXPIRED_PAIRING_CODES_SQL = `
UPDATE pairing_requests
SET code_hash = 'expired:' || id
WHERE expires_at <= ?
AND code_hash != ('expired:' || id)`;
export const DELETE_EXPIRED_CLAIM_RATE_WINDOWS_SQL = `
DELETE FROM pairing_claim_rate_limits
WHERE window_start < ?`;
export const DELETE_EXPIRED_CLAIM_ATTEMPTS_SQL = `
DELETE FROM pairing_claim_attempts
WHERE created_at < ?`;
export const DELETE_EXPIRED_IDEMPOTENCY_SQL = `
DELETE FROM idempotency_records
WHERE expires_at <= ?`;
/**
* Handoff tickets contain only a one-way ticket digest and public phone identity,
* never the raw ticket or phone token. Retain consumed/expired rows briefly for
* replay diagnostics, then remove them on a bounded schedule.
*/
export const DELETE_RETIRED_PHONE_HANDOFF_TICKETS_SQL = `
DELETE FROM phone_handoff_tickets
WHERE expires_at <= ?
OR (consumed_at IS NOT NULL AND consumed_at <= ?)`;
export const DELETE_RETIRED_PENDING_PHONE_ROUTES_SQL = `
DELETE FROM phone_route_handles
WHERE status = 'pending'
AND EXISTS (
SELECT 1 FROM phone_handoff_tickets AS tickets
WHERE tickets.tenant_id = phone_route_handles.tenant_id
AND tickets.completed_phone_id = phone_route_handles.phone_id
AND tickets.completed_route_handle_hash = phone_route_handles.handle_hash
AND (
tickets.expires_at <= ?
OR (tickets.consumed_at IS NOT NULL AND tickets.consumed_at <= ?)
)
)`;
export const DELETE_RETIRED_PENDING_PHONE_PRINCIPALS_SQL = `
DELETE FROM relay_phone_principals
WHERE status = 'pending'
AND EXISTS (
SELECT 1 FROM phone_handoff_tickets AS tickets
WHERE tickets.tenant_id = relay_phone_principals.tenant_id
AND tickets.completed_phone_id = relay_phone_principals.phone_id
AND tickets.completed_phone_token_hash = relay_phone_principals.token_hash
AND (
tickets.expires_at <= ?
OR (tickets.consumed_at IS NOT NULL AND tickets.consumed_at <= ?)
)
)`;
export const DELETE_EXPIRED_DEVICE_REGISTRATION_REPLAYS_SQL = `
DELETE FROM device_registration_replays
WHERE expires_at <= ?`;
export const ACQUIRE_RETENTION_JOB_SQL = `
INSERT INTO maintenance_jobs
(key, state, run_id, trigger, scheduled_at, started_at, completed_at,
last_success_at, result_json, error_code, consecutive_failures, run_count,
created_at, updated_at)
VALUES (?, 'running', ?, ?, ?, ?, NULL, NULL, NULL, NULL, 0, 1, ?, ?)
ON CONFLICT(key) DO UPDATE SET
state = 'running',
run_id = excluded.run_id,
trigger = excluded.trigger,
scheduled_at = excluded.scheduled_at,
started_at = excluded.started_at,
completed_at = NULL,
result_json = NULL,
error_code = NULL,
run_count = maintenance_jobs.run_count + 1,
updated_at = excluded.updated_at
WHERE maintenance_jobs.state != 'running'
OR maintenance_jobs.started_at < ?`;
export const COMPLETE_RETENTION_JOB_SQL = `
UPDATE maintenance_jobs
SET state = 'succeeded',
completed_at = ?,
last_success_at = ?,
result_json = ?,
error_code = NULL,
consecutive_failures = 0,
updated_at = ?
WHERE key = ?
AND run_id = ?
AND state = 'running'`;
export const FAIL_RETENTION_JOB_SQL = `
UPDATE maintenance_jobs
SET state = 'failed',
completed_at = ?,
result_json = NULL,
error_code = ?,
consecutive_failures = consecutive_failures + 1,
updated_at = ?
WHERE key = ?
AND run_id = ?
AND state = 'running'`;
+826
View File
@@ -0,0 +1,826 @@
import { sql } from "drizzle-orm";
import {
index,
integer,
primaryKey,
sqliteTable,
text,
uniqueIndex,
} from "drizzle-orm/sqlite-core";
const createdAt = () =>
text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`);
export const accounts = sqliteTable(
"accounts",
{
id: text("id").primaryKey(),
authSubject: text("auth_subject").notNull(),
email: text("email").notNull(),
displayName: text("display_name").notNull(),
status: text("status").notNull().default("active"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_accounts_auth_subject").on(table.authSubject),
index("idx_accounts_email").on(table.email),
],
);
export const hosts = sqliteTable(
"hosts",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
name: text("name").notNull(),
os: text("os").notNull(),
lifecycle: text("lifecycle").notNull().default("active"),
slotState: text("slot_state").notNull().default("active"),
connectionState: text("connection_state").notNull().default("unknown"),
daemonVersion: text("daemon_version"),
recoveryFingerprint: text("recovery_fingerprint"),
ed25519Public: text("ed25519_public"),
x25519Public: text("x25519_public"),
identityFingerprint: text("identity_fingerprint"),
claimRequestId: text("claim_request_id"),
claimedAt: text("claimed_at"),
lastSeenAt: text("last_seen_at"),
createdAt: createdAt(),
deactivatedAt: text("deactivated_at"),
},
(table) => [
index("idx_hosts_account_lifecycle").on(
table.accountId,
table.lifecycle,
),
uniqueIndex("idx_hosts_recovery_fingerprint").on(
table.recoveryFingerprint,
),
uniqueIndex("idx_hosts_identity_fingerprint").on(
table.identityFingerprint,
),
],
);
export const pairingRequests = sqliteTable(
"pairing_requests",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
requestedName: text("requested_name").notNull(),
os: text("os").notNull(),
codeHash: text("code_hash").notNull(),
status: text("status").notNull().default("waiting"),
expiresAt: text("expires_at").notNull(),
claimedHostId: text("claimed_host_id").references(() => hosts.id),
failedAttempts: integer("failed_attempts").notNull().default(0),
lockedAt: text("locked_at"),
lastAttemptAt: text("last_attempt_at"),
createdAt: createdAt(),
claimedAt: text("claimed_at"),
},
(table) => [
uniqueIndex("idx_pairing_code_hash").on(table.codeHash),
index("idx_pairing_account_status").on(table.accountId, table.status),
],
);
export const deviceCredentials = sqliteTable(
"device_credentials",
{
id: text("id").primaryKey(),
hostId: text("host_id")
.notNull()
.references(() => hosts.id),
tokenHash: text("token_hash").notNull(),
status: text("status").notNull().default("active"),
issuedAt: text("issued_at").notNull(),
expiresAt: text("expires_at"),
lastUsedAt: text("last_used_at"),
revokedAt: text("revoked_at"),
createdAt: createdAt(),
},
(table) => [
uniqueIndex("idx_device_credentials_token_hash").on(table.tokenHash),
index("idx_device_credentials_host_status").on(table.hostId, table.status),
],
);
export const pairingClaimRateLimits = sqliteTable(
"pairing_claim_rate_limits",
{
sourceHash: text("source_hash").notNull(),
windowStart: text("window_start").notNull(),
attempts: integer("attempts").notNull().default(1),
updatedAt: text("updated_at").notNull(),
},
(table) => [primaryKey({ columns: [table.sourceHash, table.windowStart] })],
);
export const pairingClaimAttempts = sqliteTable(
"pairing_claim_attempts",
{
id: text("id").primaryKey(),
pairingRequestId: text("pairing_request_id").notNull(),
sourceHash: text("source_hash").notNull(),
outcome: text("outcome").notNull(),
createdAt: createdAt(),
},
(table) => [
index("idx_pairing_claim_attempts_target_time").on(
table.pairingRequestId,
table.createdAt,
),
index("idx_pairing_claim_attempts_source_time").on(
table.sourceHash,
table.createdAt,
),
],
);
export const cloudSchemaMigrations = sqliteTable("cloud_schema_migrations", {
id: text("id").primaryKey(),
appliedAt: text("applied_at").notNull(),
});
export const priceVersions = sqliteTable(
"price_versions",
{
id: text("id").primaryKey(),
productCode: text("product_code").notNull().default("host_slot"),
billingPeriod: text("billing_period").notNull(),
unitSlots: integer("unit_slots").notNull().default(1),
amountMinor: integer("amount_minor").notNull(),
currency: text("currency").notNull().default("CNY"),
taxMode: text("tax_mode").notNull().default("undecided"),
quoteTtlSeconds: integer("quote_ttl_seconds").notNull().default(900),
status: text("status").notNull().default("draft"),
effectiveFrom: text("effective_from").notNull(),
effectiveTo: text("effective_to"),
createdBy: text("created_by").notNull(),
createdAt: createdAt(),
},
(table) => [
index("idx_prices_catalog").on(
table.productCode,
table.billingPeriod,
table.status,
),
],
);
export const orders = sqliteTable(
"orders",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
clientOrderId: text("client_order_id").notNull(),
kind: text("kind").notNull().default("purchase"),
priceVersionId: text("price_version_id")
.notNull()
.references(() => priceVersions.id),
slotQuantity: integer("slot_quantity").notNull(),
termStart: text("term_start").notNull(),
termEnd: text("term_end").notNull(),
amountMinor: integer("amount_minor").notNull(),
currency: text("currency").notNull(),
status: text("status").notNull().default("draft"),
quoteExpiresAt: text("quote_expires_at").notNull(),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_orders_account_client").on(
table.accountId,
table.clientOrderId,
),
index("idx_orders_account_status").on(table.accountId, table.status),
],
);
export const paymentAttempts = sqliteTable(
"payment_attempts",
{
id: text("id").primaryKey(),
orderId: text("order_id")
.notNull()
.references(() => orders.id),
provider: text("provider").notNull(),
providerPaymentRef: text("provider_payment_ref"),
providerEventId: text("provider_event_id"),
amountMinor: integer("amount_minor").notNull(),
currency: text("currency").notNull(),
status: text("status").notNull().default("created"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_payments_provider_event").on(
table.provider,
table.providerEventId,
),
uniqueIndex("idx_payments_provider_ref").on(
table.provider,
table.providerPaymentRef,
),
],
);
export const invoices = sqliteTable(
"invoices",
{
id: text("id").primaryKey(),
orderId: text("order_id")
.notNull()
.references(() => orders.id),
invoiceType: text("invoice_type").notNull().default("commercial"),
invoiceNumber: text("invoice_number"),
amountMinor: integer("amount_minor").notNull(),
taxMinor: integer("tax_minor"),
currency: text("currency").notNull(),
status: text("status").notNull().default("draft"),
documentRef: text("document_ref"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [uniqueIndex("idx_invoices_order").on(table.orderId)],
);
export const refunds = sqliteTable(
"refunds",
{
id: text("id").primaryKey(),
paymentAttemptId: text("payment_attempt_id")
.notNull()
.references(() => paymentAttempts.id),
invoiceId: text("invoice_id").references(() => invoices.id),
amountMinor: integer("amount_minor").notNull(),
currency: text("currency").notNull(),
reason: text("reason").notNull(),
status: text("status").notNull().default("requested"),
entitlementEffect: text("entitlement_effect").notNull().default("pending"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [index("idx_refunds_payment").on(table.paymentAttemptId)],
);
export const betaPrograms = sqliteTable(
"beta_programs",
{
id: text("id").primaryKey(),
state: text("state").notNull(),
capacitySlots: integer("capacity_slots"),
startsAt: text("starts_at").notNull(),
endsAt: text("ends_at"),
graceDays: integer("grace_days").notNull().default(7),
createdBy: text("created_by").notNull(),
createdAt: createdAt(),
retiredAt: text("retired_at"),
},
(table) => [index("idx_beta_state_time").on(table.state, table.startsAt)],
);
export const entitlementGrants = sqliteTable(
"entitlement_grants",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
hostId: text("host_id").references(() => hosts.id),
source: text("source").notNull(),
sourceRef: text("source_ref"),
capacitySlots: integer("capacity_slots"),
startsAt: text("starts_at").notNull(),
endsAt: text("ends_at"),
state: text("state").notNull().default("scheduled"),
reason: text("reason").notNull(),
createdBy: text("created_by").notNull(),
createdAt: createdAt(),
revokedAt: text("revoked_at"),
},
(table) => [
index("idx_grants_account_state_time").on(
table.accountId,
table.state,
table.startsAt,
),
uniqueIndex("idx_grants_source_ref").on(table.source, table.sourceRef),
],
);
export const betaAccessRequests = sqliteTable(
"beta_access_requests",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
status: text("status").notNull().default("requested"),
preferredOs: text("preferred_os").notNull(),
requestedSlots: integer("requested_slots").notNull().default(1),
useCase: text("use_case").notNull(),
adminResponse: text("admin_response"),
resolvedBy: text("resolved_by"),
invitationGrantId: text("invitation_grant_id").references(
() => entitlementGrants.id,
),
requestedAt: text("requested_at").notNull(),
resolvedAt: text("resolved_at"),
cancelledAt: text("cancelled_at"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_beta_access_requests_one_pending")
.on(table.accountId)
.where(sql`${table.status} = 'requested'`),
index("idx_beta_access_requests_status_time").on(
table.status,
table.requestedAt,
),
index("idx_beta_access_requests_account_time").on(
table.accountId,
table.requestedAt,
),
index("idx_beta_access_requests_requested_time").on(table.requestedAt),
],
);
export const tenantInstances = sqliteTable(
"tenant_instances",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
slug: text("slug").notNull(),
lifecycle: text("lifecycle").notNull().default("requested"),
desiredState: text("desired_state").notNull().default("requested"),
observedState: text("observed_state").notNull().default("absent"),
desiredGeneration: integer("desired_generation").notNull().default(1),
activeGeneration: integer("active_generation"),
credentialRevision: integer("credential_revision").notNull().default(0),
runtimeVersion: text("runtime_version"),
runtimeRef: text("runtime_ref"),
relayOrigin: text("relay_origin"),
secretBundleRef: text("secret_bundle_ref"),
relayReady: integer("relay_ready", { mode: "boolean" }).notNull().default(false),
lastHealthAt: text("last_health_at"),
tombstonedAt: text("tombstoned_at"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_tenants_account").on(table.accountId),
uniqueIndex("idx_tenants_slug").on(table.slug),
],
);
export const relayRegions = sqliteTable(
"relay_regions",
{
id: text("id").primaryKey(),
code: text("code").notNull(),
displayName: text("display_name").notNull(),
status: text("status").notNull().default("active"),
publicOrigin: text("public_origin").notNull(),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_relay_regions_code").on(table.code),
uniqueIndex("idx_relay_regions_public_origin").on(table.publicOrigin),
],
);
export const relayNodes = sqliteTable(
"relay_nodes",
{
id: text("id").primaryKey(),
regionId: text("region_id")
.notNull()
.references(() => relayRegions.id),
status: text("status").notNull().default("provisioning"),
internalEndpointRef: text("internal_endpoint_ref").notNull(),
capacityTenants: integer("capacity_tenants").notNull().default(0),
lastHeartbeatAt: text("last_heartbeat_at"),
heartbeatGeneration: integer("heartbeat_generation").notNull().default(0),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
index("idx_relay_nodes_region_status").on(table.regionId, table.status),
],
);
export const tenantPlacements = sqliteTable(
"tenant_placements",
{
tenantId: text("tenant_id")
.primaryKey()
.references(() => tenantInstances.id),
homeRegionId: text("home_region_id")
.notNull()
.references(() => relayRegions.id),
relayNodeId: text("relay_node_id").references(() => relayNodes.id),
generation: integer("generation").notNull().default(1),
state: text("state").notNull().default("provisioning"),
lastErrorCode: text("last_error_code"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
index("idx_tenant_placements_region_state").on(
table.homeRegionId,
table.state,
),
index("idx_tenant_placements_node_state").on(
table.relayNodeId,
table.state,
),
],
);
export const tenantAuthorizationState = sqliteTable(
"tenant_authorization_state",
{
tenantId: text("tenant_id")
.primaryKey()
.references(() => tenantInstances.id),
revision: integer("revision").notNull().default(0),
status: text("status").notNull().default("active"),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [index("idx_tenant_authorization_revision").on(table.revision)],
);
export const relayMigrations = sqliteTable(
"relay_migrations",
{
id: text("id").primaryKey(),
tenantId: text("tenant_id")
.notNull()
.references(() => tenantInstances.id),
sourceNodeId: text("source_node_id")
.notNull()
.references(() => relayNodes.id),
targetNodeId: text("target_node_id")
.notNull()
.references(() => relayNodes.id),
sourceGeneration: integer("source_generation").notNull(),
targetGeneration: integer("target_generation").notNull(),
state: text("state").notNull().default("quiescing"),
backupRef: text("backup_ref"),
manifestSha256: text("manifest_sha256"),
requestedBy: text("requested_by").notNull(),
reason: text("reason").notNull(),
startedAt: text("started_at").notNull(),
updatedAt: text("updated_at").notNull(),
switchedAt: text("switched_at"),
completedAt: text("completed_at"),
lastErrorCode: text("last_error_code"),
},
(table) => [
index("idx_relay_migrations_source_state").on(
table.sourceNodeId,
table.state,
table.updatedAt,
),
index("idx_relay_migrations_target_state").on(
table.targetNodeId,
table.state,
table.updatedAt,
),
],
);
export const relaySigningKeys = sqliteTable("relay_signing_keys", {
kid: text("kid").primaryKey(),
algorithm: text("algorithm").notNull().default("Ed25519"),
publicKeyJwk: text("public_key_jwk").notNull(),
privateKeyRef: text("private_key_ref").notNull(),
status: text("status").notNull().default("active"),
notBefore: text("not_before").notNull(),
notAfter: text("not_after").notNull(),
createdAt: createdAt(),
retiredAt: text("retired_at"),
});
export const relayNodeCredentials = sqliteTable(
"relay_node_credentials",
{
id: text("id").primaryKey(),
nodeId: text("node_id")
.notNull()
.references(() => relayNodes.id),
mtlsSpiffeId: text("mtls_spiffe_id").notNull(),
certificateFingerprintSha256: text("certificate_fingerprint_sha256").notNull(),
status: text("status").notNull().default("active"),
bootstrapReason: text("bootstrap_reason").notNull(),
issuedBy: text("issued_by").notNull(),
issuedAt: text("issued_at").notNull(),
expiresAt: text("expires_at"),
lastUsedAt: text("last_used_at"),
revokedAt: text("revoked_at"),
createdAt: createdAt(),
},
(table) => [
uniqueIndex("idx_relay_node_credentials_mtls").on(
table.mtlsSpiffeId,
table.certificateFingerprintSha256,
),
index("idx_relay_node_credentials_node_status").on(
table.nodeId,
table.status,
),
],
);
export const phoneHandoffTickets = sqliteTable(
"phone_handoff_tickets",
{
id: text("id").primaryKey(),
ticketHash: text("ticket_hash").notNull(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
tenantId: text("tenant_id")
.notNull()
.references(() => tenantInstances.id),
expectedOrigin: text("expected_origin").notNull(),
expiresAt: text("expires_at").notNull(),
consumedAt: text("consumed_at"),
consumedByNodeId: text("consumed_by_node_id").references(
() => relayNodes.id,
),
pendingPhoneName: text("pending_phone_name"),
pendingEd25519Public: text("pending_ed25519_public"),
pendingX25519Public: text("pending_x25519_public"),
pendingIdentityFingerprint: text("pending_identity_fingerprint"),
completedAt: text("completed_at"),
completedPhoneId: text("completed_phone_id"),
completedPhoneTokenHash: text("completed_phone_token_hash"),
completedRouteHandleHash: text("completed_route_handle_hash"),
activationNonce: text("activation_nonce"),
activatedAt: text("activated_at"),
createdAt: createdAt(),
},
(table) => [
uniqueIndex("idx_phone_handoff_ticket_hash").on(table.ticketHash),
uniqueIndex("idx_phone_handoff_activation_nonce").on(table.activationNonce),
index("idx_phone_handoff_account_expiry").on(
table.accountId,
table.expiresAt,
),
],
);
export const phoneRouteHandles = sqliteTable(
"phone_route_handles",
{
id: text("id").primaryKey(),
handleHash: text("handle_hash").notNull(),
tenantId: text("tenant_id")
.notNull()
.references(() => tenantInstances.id),
phoneId: text("phone_id").notNull(),
status: text("status").notNull().default("active"),
createdAt: createdAt(),
lastUsedAt: text("last_used_at"),
revokedAt: text("revoked_at"),
},
(table) => [
uniqueIndex("idx_phone_route_handle_hash").on(table.handleHash),
index("idx_phone_route_tenant_status").on(table.tenantId, table.status),
],
);
export const relayPhonePrincipals = sqliteTable(
"relay_phone_principals",
{
phoneId: text("phone_id").primaryKey(),
tenantId: text("tenant_id")
.notNull()
.references(() => tenantInstances.id),
tokenHash: text("token_hash").notNull(),
name: text("name").notNull(),
ed25519Public: text("ed25519_public").notNull(),
x25519Public: text("x25519_public").notNull(),
identityFingerprint: text("identity_fingerprint").notNull(),
status: text("status").notNull().default("active"),
createdAt: createdAt(),
lastUsedAt: text("last_used_at"),
revokedAt: text("revoked_at"),
},
(table) => [
uniqueIndex("idx_relay_phone_token_hash").on(table.tokenHash),
uniqueIndex("idx_relay_phone_tenant_identity").on(
table.tenantId,
table.identityFingerprint,
),
index("idx_relay_phone_tenant_status").on(table.tenantId, table.status),
],
);
export const deviceRegistrationReplays = sqliteTable(
"device_registration_replays",
{
pairingId: text("pairing_id")
.primaryKey()
.references(() => pairingRequests.id),
requestHash: text("request_hash").notNull(),
responseCiphertext: text("response_ciphertext").notNull(),
responseNonce: text("response_nonce").notNull(),
expiresAt: text("expires_at").notNull(),
createdAt: createdAt(),
},
(table) => [index("idx_device_registration_replay_expiry").on(table.expiresAt)],
);
export const launchGates = sqliteTable(
"launch_gates",
{
key: text("key").primaryKey(),
priority: text("priority").notNull(),
category: text("category").notNull(),
title: text("title").notNull(),
status: text("status").notNull().default("blocked"),
owner: text("owner"),
evidenceUrl: text("evidence_url"),
notes: text("notes").notNull().default(""),
reviewedAt: text("reviewed_at"),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [index("idx_launch_gates_status").on(table.priority, table.status)],
);
export const betaFeedback = sqliteTable(
"beta_feedback",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
category: text("category").notNull(),
message: text("message").notNull(),
status: text("status").notNull().default("open"),
adminResponse: text("admin_response"),
handledBy: text("handled_by"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
resolvedAt: text("resolved_at"),
},
(table) => [
index("idx_beta_feedback_account_time").on(
table.accountId,
table.createdAt,
),
index("idx_beta_feedback_status_time").on(table.status, table.createdAt),
],
);
export const accountDeletionRequests = sqliteTable(
"account_deletion_requests",
{
id: text("id").primaryKey(),
accountId: text("account_id")
.notNull()
.references(() => accounts.id),
status: text("status").notNull().default("requested"),
reason: text("reason"),
requestedAt: text("requested_at").notNull(),
cancelledAt: text("cancelled_at"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
uniqueIndex("idx_account_deletion_one_requested")
.on(table.accountId)
.where(sql`${table.status} = 'requested'`),
index("idx_account_deletion_status_time").on(
table.status,
table.requestedAt,
),
],
);
export const relayPurgeJobs = sqliteTable(
"relay_purge_jobs",
{
id: text("id").primaryKey(),
deletionRequestId: text("deletion_request_id")
.notNull()
.references(() => accountDeletionRequests.id),
tenantId: text("tenant_id")
.notNull()
.references(() => tenantInstances.id),
relayNodeId: text("relay_node_id")
.notNull()
.references(() => relayNodes.id),
placementGeneration: integer("placement_generation").notNull(),
state: text("state").notNull().default("quiescing"),
requestedBy: text("requested_by").notNull(),
reason: text("reason").notNull(),
startedAt: text("started_at").notNull(),
updatedAt: text("updated_at").notNull(),
completedAt: text("completed_at"),
evidenceSha256: text("evidence_sha256"),
lastErrorCode: text("last_error_code"),
},
(table) => [
uniqueIndex("idx_relay_purge_deletion_request").on(table.deletionRequestId),
uniqueIndex("idx_relay_purge_tenant").on(table.tenantId),
index("idx_relay_purge_node_state").on(table.relayNodeId, table.state, table.updatedAt),
],
);
export const serviceIncidents = sqliteTable(
"service_incidents",
{
id: text("id").primaryKey(),
severity: text("severity").notNull(),
title: text("title").notNull(),
message: text("message").notNull(),
status: text("status").notNull().default("active"),
createdBy: text("created_by").notNull(),
resolvedBy: text("resolved_by"),
resolution: text("resolution"),
startedAt: text("started_at").notNull(),
resolvedAt: text("resolved_at"),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
},
(table) => [
index("idx_service_incidents_status_started").on(
table.status,
table.startedAt,
),
],
);
export const auditEvents = sqliteTable(
"audit_events",
{
id: text("id").primaryKey(),
actorId: text("actor_id").notNull(),
action: text("action").notNull(),
targetType: text("target_type").notNull(),
targetId: text("target_id").notNull(),
reason: text("reason").notNull(),
beforeJson: text("before_json"),
afterJson: text("after_json"),
correlationId: text("correlation_id").notNull(),
createdAt: createdAt(),
},
(table) => [
index("idx_audit_target_time").on(
table.targetType,
table.targetId,
table.createdAt,
),
index("idx_audit_actor_time").on(table.actorId, table.createdAt),
],
);
export const idempotencyRecords = sqliteTable(
"idempotency_records",
{
scope: text("scope").notNull(),
key: text("key").notNull(),
requestHash: text("request_hash").notNull(),
responseJson: text("response_json").notNull(),
statusCode: integer("status_code").notNull(),
expiresAt: text("expires_at").notNull(),
createdAt: createdAt(),
},
(table) => [
primaryKey({ columns: [table.scope, table.key] }),
index("idx_idempotency_expires").on(table.expiresAt),
],
);
export const maintenanceJobs = sqliteTable("maintenance_jobs", {
key: text("key").primaryKey(),
state: text("state").notNull(),
runId: text("run_id"),
trigger: text("trigger"),
scheduledAt: text("scheduled_at"),
startedAt: text("started_at"),
completedAt: text("completed_at"),
lastSuccessAt: text("last_success_at"),
resultJson: text("result_json"),
errorCode: text("error_code"),
consecutiveFailures: integer("consecutive_failures").notNull().default(0),
runCount: integer("run_count").notNull().default(0),
createdAt: createdAt(),
updatedAt: text("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
});
+26
View File
@@ -0,0 +1,26 @@
export type IncidentSeverity = "maintenance" | "degraded" | "outage";
export type ServiceStatusLevel =
| "operational"
| "maintenance"
| "degraded"
| "outage";
const severityRank: Record<IncidentSeverity, number> = {
maintenance: 1,
degraded: 2,
outage: 3,
};
export function deriveServiceStatus(
incidents: ReadonlyArray<{ severity: IncidentSeverity; status: string }>,
): ServiceStatusLevel {
let selected: IncidentSeverity | null = null;
for (const incident of incidents) {
if (incident.status !== "active") continue;
if (!selected || severityRank[incident.severity] > severityRank[selected]) {
selected = incident.severity;
}
}
return selected ?? "operational";
}