Skip to content

Commit c6df866

Browse files
committed
feat(services): sync domain verification loop from constructive-db
Upstreams the services-module tables and function added in constructive-db#2283 (the verify->DNS->issue loop) plus their managed_domains foundation: - managed_domains: cert-bearing host/wildcard registry with independent verification_status / tls_status / cert_status - domain_verifications: entity-owned (owner_id) ownership-challenge table - domain_events: append-only lifecycle audit trail - services_private.domain_issue_challenge: domain:issue_challenge RLS/permission/FK wiring lives in the constructive-db introspection / metaschema-generators packages (no upstream counterpart) and is out of scope for this module sync.
1 parent 7d5acfd commit c6df866

14 files changed

Lines changed: 667 additions & 1 deletion

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
-- Deploy schemas/services_private/procedures/domain_issue_challenge to pg
2+
3+
-- requires: schemas/services_private/schema
4+
-- requires: schemas/services_public/tables/managed_domains/table
5+
-- requires: schemas/services_public/tables/domain_verifications/table
6+
-- requires: schemas/services_public/tables/domain_events/table
7+
-- requires: metaschema-schema:schemas/metaschema_public/tables/database/table
8+
9+
BEGIN;
10+
11+
-- domain:issue_challenge — first node of the verify->DNS->issue loop.
12+
--
13+
-- Mints a fresh PUBLIC verification challenge for a managed_domain, records the
14+
-- record the user must publish in services_public.domain_verifications, moves
15+
-- the domain's verification_status back to 'pending', and emits a
16+
-- 'challenge_issued' audit event. The record_value is a verification token that
17+
-- ends up in a public DNS record — it is NOT a secret and lives in a table, not
18+
-- the secrets module.
19+
--
20+
-- Any still-outstanding challenge for the same (managed_domain, method) is
21+
-- expired first so exactly one challenge is active at a time. This is a static
22+
-- function (no dynamic SQL); it is invoked directly, by a job trigger, or by
23+
-- the runtime='sql' worker dispatch registered under the domain:issue_challenge
24+
-- task identifier.
25+
CREATE FUNCTION services_private.domain_issue_challenge(
26+
v_managed_domain_id uuid,
27+
v_method text DEFAULT 'dns_txt_ownership',
28+
v_actor_id uuid DEFAULT NULL
29+
) RETURNS services_public.domain_verifications AS $$
30+
DECLARE
31+
v_owner_id uuid;
32+
v_domain text;
33+
v_token text;
34+
v_record_name text;
35+
v_record_type text;
36+
v_record_value text;
37+
v_row services_public.domain_verifications;
38+
BEGIN
39+
-- The verification/audit rows are entity-owned (owner_id), so resolve the
40+
-- owning entity from the managed_domain's database owner. managed_domains
41+
-- itself stays database-scoped; only this loop's tables key on the entity.
42+
SELECT db.owner_id, md.domain
43+
INTO v_owner_id, v_domain
44+
FROM services_public.managed_domains AS md
45+
JOIN metaschema_public.database AS db ON db.id = md.database_id
46+
WHERE md.id = v_managed_domain_id;
47+
48+
IF NOT FOUND THEN
49+
RAISE EXCEPTION 'DOMAIN_ISSUE_CHALLENGE_UNKNOWN_DOMAIN: no managed_domain with id %', v_managed_domain_id
50+
USING ERRCODE = 'foreign_key_violation';
51+
END IF;
52+
53+
IF v_owner_id IS NULL THEN
54+
RAISE EXCEPTION 'DOMAIN_ISSUE_CHALLENGE_NO_OWNER: managed_domain % has no owning entity', v_managed_domain_id
55+
USING ERRCODE = 'not_null_violation';
56+
END IF;
57+
58+
IF v_method NOT IN ('dns_txt_ownership', 'http_01', 'dns_01_acme') THEN
59+
RAISE EXCEPTION 'DOMAIN_ISSUE_CHALLENGE_BAD_METHOD: unsupported verification method %', v_method
60+
USING ERRCODE = 'check_violation';
61+
END IF;
62+
63+
-- 256 bits of entropy as a 64-char hex token; gen_random_uuid is core in
64+
-- PostgreSQL (no pgcrypto dependency needed for the services module).
65+
v_token := replace(gen_random_uuid()::text || gen_random_uuid()::text, '-', '');
66+
67+
IF v_method = 'dns_txt_ownership' THEN
68+
v_record_name := '_constructive-challenge.' || v_domain;
69+
v_record_type := 'TXT';
70+
v_record_value := 'constructive-domain-verification=' || v_token;
71+
ELSIF v_method = 'dns_01_acme' THEN
72+
v_record_name := '_acme-challenge.' || v_domain;
73+
v_record_type := 'TXT';
74+
v_record_value := v_token;
75+
ELSE
76+
-- http_01: served at /.well-known/acme-challenge/<token>, no DNS record.
77+
v_record_name := NULL;
78+
v_record_type := NULL;
79+
v_record_value := v_token;
80+
END IF;
81+
82+
-- Supersede any outstanding challenge for this (domain, method) so only one
83+
-- is ever active; historical rows stay for the audit trail.
84+
UPDATE services_public.domain_verifications
85+
SET status = 'expired',
86+
updated_at = now()
87+
WHERE managed_domain_id = v_managed_domain_id
88+
AND method = v_method
89+
AND status IN ('pending', 'checking');
90+
91+
INSERT INTO services_public.domain_verifications
92+
(owner_id, managed_domain_id, method, record_name, record_type, record_value,
93+
status, attempts, expires_at)
94+
VALUES
95+
(v_owner_id, v_managed_domain_id, v_method, v_record_name, v_record_type, v_record_value,
96+
'pending', 0, now() + interval '7 days')
97+
RETURNING * INTO v_row;
98+
99+
UPDATE services_public.managed_domains
100+
SET verification_status = 'pending'
101+
WHERE id = v_managed_domain_id;
102+
103+
INSERT INTO services_public.domain_events
104+
(owner_id, managed_domain_id, domain_verification_id, event_type, actor_id, message, metadata)
105+
VALUES
106+
(v_owner_id, v_managed_domain_id, v_row.id, 'challenge_issued', v_actor_id,
107+
'Issued ' || v_method || ' verification challenge for ' || v_domain,
108+
jsonb_build_object(
109+
'method', v_method,
110+
'record_name', v_record_name,
111+
'record_type', v_record_type,
112+
'record_value', v_record_value
113+
));
114+
115+
RETURN v_row;
116+
END;
117+
$$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER;
118+
119+
COMMENT ON FUNCTION services_private.domain_issue_challenge(uuid, text, uuid) IS 'domain:issue_challenge — mints a public verification challenge, writes the domain_verifications row + challenge_issued event, and resets managed_domains.verification_status to pending.';
120+
121+
GRANT EXECUTE ON FUNCTION services_private.domain_issue_challenge(uuid, text, uuid) TO authenticated;
122+
GRANT EXECUTE ON FUNCTION services_private.domain_issue_challenge(uuid, text, uuid) TO administrator;
123+
124+
COMMIT;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
-- Deploy schemas/services_public/tables/domain_events/table to pg
2+
3+
-- requires: schemas/services_public/schema
4+
-- requires: schemas/services_public/tables/managed_domains/table
5+
-- requires: schemas/services_public/tables/domain_verifications/table
6+
7+
BEGIN;
8+
9+
-- Append-only audit trail for the domain verify->DNS->issue loop, mirroring the
10+
-- shape of compute_public.resource_events / cluster_events (id, created_at,
11+
-- <subject>_id, event_type, actor_id, message, metadata). resource_events is
12+
-- partitioned+retained by its module generator's partition-lifecycle wiring;
13+
-- these hand-authored services tables have no such maintenance wiring, so
14+
-- domain_events is a plain append-only table (matching managed_domains, which is
15+
-- also unpartitioned). Partitioning/retention can be layered on later alongside
16+
-- the module-generator lifecycle if event volume warrants it.
17+
CREATE TABLE services_public.domain_events (
18+
id uuid PRIMARY KEY DEFAULT uuidv7(),
19+
owner_id uuid NOT NULL,
20+
managed_domain_id uuid NOT NULL,
21+
domain_verification_id uuid,
22+
23+
event_type text NOT NULL,
24+
actor_id uuid,
25+
message text,
26+
metadata jsonb NOT NULL DEFAULT '{}',
27+
28+
created_at timestamptz NOT NULL DEFAULT now(),
29+
30+
--
31+
CONSTRAINT managed_domain_fkey FOREIGN KEY (managed_domain_id) REFERENCES services_public.managed_domains (id) ON DELETE CASCADE,
32+
CONSTRAINT domain_verification_fkey FOREIGN KEY (domain_verification_id) REFERENCES services_public.domain_verifications (id) ON DELETE SET NULL,
33+
CONSTRAINT event_type_chk CHECK (event_type IN (
34+
'challenge_issued',
35+
'verification_started',
36+
'verified',
37+
'verification_failed',
38+
'verification_expired',
39+
'cert_issuing',
40+
'cert_active',
41+
'cert_error',
42+
'cert_renewed',
43+
'cert_revoked'
44+
))
45+
);
46+
47+
COMMENT ON TABLE services_public.domain_events IS 'Append-only audit trail of the domain verify->DNS->issue lifecycle, mirroring resource_events / cluster_events. One row per state transition emitted by the domain:* functions.';
48+
COMMENT ON COLUMN services_public.domain_events.id IS 'Unique event identifier';
49+
COMMENT ON COLUMN services_public.domain_events.owner_id IS 'Entity (e.g. org) that owns the managed_domain; scope key for AuthzEntityMembership RLS';
50+
COMMENT ON COLUMN services_public.domain_events.managed_domain_id IS 'The managed_domain this event belongs to';
51+
COMMENT ON COLUMN services_public.domain_events.domain_verification_id IS 'The verification challenge this event relates to, when applicable';
52+
COMMENT ON COLUMN services_public.domain_events.event_type IS 'Lifecycle event: challenge_issued | verification_started | verified | verification_failed | verification_expired | cert_issuing | cert_active | cert_error | cert_renewed | cert_revoked';
53+
COMMENT ON COLUMN services_public.domain_events.actor_id IS 'User who triggered this event (NULL for system/automated transitions)';
54+
COMMENT ON COLUMN services_public.domain_events.message IS 'Human-readable description of the event';
55+
COMMENT ON COLUMN services_public.domain_events.metadata IS 'Structured context (challenge record, cert-manager detail, error details, ...)';
56+
COMMENT ON COLUMN services_public.domain_events.created_at IS 'Event timestamp';
57+
58+
CREATE INDEX domain_events_owner_id_idx ON services_public.domain_events ( owner_id );
59+
60+
CREATE INDEX domain_events_managed_domain_id_created_at_idx ON services_public.domain_events ( managed_domain_id, created_at );
61+
62+
CREATE INDEX domain_events_domain_verification_id_idx ON services_public.domain_events ( domain_verification_id );
63+
64+
COMMIT;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
-- Deploy schemas/services_public/tables/domain_verifications/table to pg
2+
3+
-- requires: schemas/services_public/schema
4+
-- requires: schemas/services_public/tables/managed_domains/table
5+
6+
BEGIN;
7+
8+
CREATE TABLE services_public.domain_verifications (
9+
id uuid PRIMARY KEY DEFAULT uuidv7(),
10+
owner_id uuid NOT NULL,
11+
managed_domain_id uuid NOT NULL,
12+
13+
method text NOT NULL DEFAULT 'dns_txt_ownership',
14+
15+
record_name text,
16+
record_type text,
17+
record_value text,
18+
19+
status text NOT NULL DEFAULT 'pending',
20+
attempts integer NOT NULL DEFAULT 0,
21+
22+
last_checked_at timestamptz,
23+
verified_at timestamptz,
24+
expires_at timestamptz,
25+
error text,
26+
27+
created_at timestamptz NOT NULL DEFAULT now(),
28+
updated_at timestamptz NOT NULL DEFAULT now(),
29+
30+
--
31+
CONSTRAINT managed_domain_fkey FOREIGN KEY (managed_domain_id) REFERENCES services_public.managed_domains (id) ON DELETE CASCADE,
32+
CONSTRAINT method_chk CHECK (method IN ('dns_txt_ownership', 'http_01', 'dns_01_acme')),
33+
CONSTRAINT record_type_chk CHECK (record_type IS NULL OR record_type IN ('TXT', 'CNAME', 'A')),
34+
CONSTRAINT status_chk CHECK (status IN ('pending', 'checking', 'verified', 'failed', 'expired'))
35+
);
36+
37+
COMMENT ON TABLE services_public.domain_verifications IS 'One row per outstanding/completed ownership-verification challenge for a managed_domain. Holds the PUBLIC challenge the user must publish (e.g. a DNS TXT record value) — this is a verification token, NOT a secret. Entity-owned via owner_id and read/written through the AuthzEntityMembership scoped-module security path gated on manage_domains.';
38+
COMMENT ON COLUMN services_public.domain_verifications.id IS 'Unique identifier for this verification challenge';
39+
COMMENT ON COLUMN services_public.domain_verifications.owner_id IS 'Entity (e.g. org) that owns this verification; scope key for AuthzEntityMembership RLS. Domain control is proven once per owning entity.';
40+
COMMENT ON COLUMN services_public.domain_verifications.managed_domain_id IS 'The managed_domain this challenge proves ownership of';
41+
COMMENT ON COLUMN services_public.domain_verifications.method IS 'Verification method: dns_txt_ownership (root-domain ownership TXT) | http_01 (ACME HTTP challenge) | dns_01_acme (ACME DNS challenge)';
42+
COMMENT ON COLUMN services_public.domain_verifications.record_name IS 'DNS record name the user must create (e.g. _constructive-challenge.example.com); NULL for http_01';
43+
COMMENT ON COLUMN services_public.domain_verifications.record_type IS 'DNS record type to create: TXT | CNAME | A; NULL for http_01';
44+
COMMENT ON COLUMN services_public.domain_verifications.record_value IS 'The public challenge token the user must publish (ends up in a public DNS record). NOT a secret.';
45+
COMMENT ON COLUMN services_public.domain_verifications.status IS 'Challenge lifecycle: pending | checking | verified | failed | expired';
46+
COMMENT ON COLUMN services_public.domain_verifications.attempts IS 'Number of times domain:verify has polled for this challenge (drives backoff / max_attempts)';
47+
COMMENT ON COLUMN services_public.domain_verifications.last_checked_at IS 'When domain:verify last polled DNS/HTTP for this challenge';
48+
COMMENT ON COLUMN services_public.domain_verifications.verified_at IS 'When status last became verified';
49+
COMMENT ON COLUMN services_public.domain_verifications.expires_at IS 'When this challenge expires and must be reissued';
50+
COMMENT ON COLUMN services_public.domain_verifications.error IS 'Last verification error (mismatch, NXDOMAIN, timeout, ...)';
51+
COMMENT ON COLUMN services_public.domain_verifications.created_at IS 'When this challenge was minted (domain:issue_challenge)';
52+
COMMENT ON COLUMN services_public.domain_verifications.updated_at IS 'When this row was last updated';
53+
54+
CREATE INDEX domain_verifications_owner_id_idx ON services_public.domain_verifications ( owner_id );
55+
56+
CREATE INDEX domain_verifications_managed_domain_id_idx ON services_public.domain_verifications ( managed_domain_id );
57+
58+
COMMIT;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
-- Deploy schemas/services_public/tables/managed_domains/table to pg
2+
3+
-- requires: schemas/services_public/schema
4+
-- requires: schemas/metaschema_public/tables/database/table
5+
6+
BEGIN;
7+
8+
CREATE TABLE services_public.managed_domains (
9+
id uuid PRIMARY KEY DEFAULT uuidv7(),
10+
database_id uuid NOT NULL,
11+
12+
domain hostname NOT NULL,
13+
is_wildcard boolean NOT NULL DEFAULT false,
14+
15+
allow_public_usage boolean NOT NULL DEFAULT false,
16+
17+
verification_status text NOT NULL DEFAULT 'pending',
18+
verified_at timestamptz,
19+
20+
tls_status text NOT NULL DEFAULT 'none',
21+
tls_ready_at timestamptz,
22+
23+
cert_status text NOT NULL DEFAULT 'none',
24+
25+
annotations jsonb NOT NULL DEFAULT '{}',
26+
27+
--
28+
CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE,
29+
CONSTRAINT verification_status_chk CHECK (verification_status IN ('pending', 'checking', 'verified', 'failed', 'expired')),
30+
CONSTRAINT tls_status_chk CHECK (tls_status IN ('none', 'provisioning', 'active', 'failed')),
31+
CONSTRAINT cert_status_chk CHECK (cert_status IN ('none', 'issuing', 'active', 'error')),
32+
UNIQUE ( domain )
33+
);
34+
35+
COMMENT ON TABLE services_public.managed_domains IS 'One row per cert-bearing host or wildcard; tracks domain verification and TLS provisioning independently of services_public.domains. Reconcilers match a route''s root domain to a row here by string (no FK/coupling in v1)';
36+
COMMENT ON COLUMN services_public.managed_domains.id IS 'Unique identifier for this managed domain record';
37+
COMMENT ON COLUMN services_public.managed_domains.database_id IS 'Database that owns this cert-bearing host; platform wildcards are owned by the platform database';
38+
COMMENT ON COLUMN services_public.managed_domains.domain IS 'Root hostname this row governs certs/verification for (e.g. launchql.dev, shop.acme.com)';
39+
COMMENT ON COLUMN services_public.managed_domains.is_wildcard IS 'Whether the cert covers the wildcard *.domain (one wildcard cert covers every subdomain row sharing this root)';
40+
COMMENT ON COLUMN services_public.managed_domains.allow_public_usage IS 'Whether this domain is deliberately published so routes in other scopes may match and ride this row''s cert. Only settable by app/platform authority via a generated AuthzColumnSecurity write-guard; backed by a generated permissive cross-scope SELECT policy.';
41+
COMMENT ON COLUMN services_public.managed_domains.verification_status IS 'Domain ownership verification state driven by the domain:issue_challenge/domain:verify loop: pending | checking | verified | failed | expired';
42+
COMMENT ON COLUMN services_public.managed_domains.verified_at IS 'When verification_status last became verified';
43+
COMMENT ON COLUMN services_public.managed_domains.tls_status IS 'TLS/SSL serving/reconcile state (ingress): none | provisioning | active | failed';
44+
COMMENT ON COLUMN services_public.managed_domains.tls_ready_at IS 'When tls_status last became active';
45+
COMMENT ON COLUMN services_public.managed_domains.cert_status IS 'cert-manager resource lifecycle driven by the domain:issue_cert/domain:check_cert loop, tracked independently of tls_status: none | issuing | active | error';
46+
COMMENT ON COLUMN services_public.managed_domains.annotations IS 'Freeform cert-manager detail (secret name, challenge, last error) and tooling metadata';
47+
48+
CREATE INDEX managed_domains_database_id_idx ON services_public.managed_domains ( database_id );
49+
50+
COMMIT;

packages/services/pgpm.plan

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ schemas/services_public/tables/cors_settings/table [schemas/services_public/sche
2222
schemas/services_public/tables/pubkey_settings/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table metaschema-schema:schemas/metaschema_public/tables/schema/table metaschema-schema:schemas/metaschema_public/tables/function/table] 2026-05-09T00:00:00Z devin <devin@cognition.ai> # add typed pubkey crypto auth settings with UUID FKs
2323
schemas/services_public/tables/webauthn_settings/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table metaschema-schema:schemas/metaschema_public/tables/schema/table metaschema-schema:schemas/metaschema_public/tables/table/table metaschema-schema:schemas/metaschema_public/tables/field/table] 2026-05-09T00:00:00Z devin <devin@cognition.ai> # add typed WebAuthn/passkey settings with UUID FKs
2424
schemas/services_private/triggers/enforce_api_exposure [schemas/services_private/schema schemas/services_public/tables/api_schemas/table metaschema-schema:schemas/metaschema_public/tables/schema/table] 2026-06-18T00:00:00Z devin <devin@cognition.ai> # block never_expose and internal_only schemas from being added to APIs
25+
schemas/services_public/tables/managed_domains/table [schemas/services_public/schema metaschema-schema:schemas/metaschema_public/tables/database/table] 2026-07-16T00:00:00Z devin <devin@cognition.ai> # add cert-bearing managed_domains registry for TLS/verification state
26+
schemas/services_public/tables/domain_verifications/table [schemas/services_public/schema schemas/services_public/tables/managed_domains/table] 2026-07-18T00:00:00Z devin <devin@cognition.ai> # add domain_verifications challenge table for the verify->DNS->issue loop
27+
schemas/services_public/tables/domain_events/table [schemas/services_public/schema schemas/services_public/tables/managed_domains/table schemas/services_public/tables/domain_verifications/table] 2026-07-18T00:00:00Z devin <devin@cognition.ai> # add domain_events audit trail for the domain verification/cert lifecycle
28+
schemas/services_private/procedures/domain_issue_challenge [schemas/services_private/schema schemas/services_public/tables/managed_domains/table schemas/services_public/tables/domain_verifications/table schemas/services_public/tables/domain_events/table metaschema-schema:schemas/metaschema_public/tables/database/table] 2026-07-18T00:00:00Z devin <devin@cognition.ai> # domain:issue_challenge - mint verification challenge + emit event
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Revert schemas/services_private/procedures/domain_issue_challenge
2+
3+
BEGIN;
4+
5+
DROP FUNCTION IF EXISTS services_private.domain_issue_challenge(uuid, text, uuid);
6+
7+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Revert schemas/services_public/tables/domain_events/table
2+
3+
BEGIN;
4+
5+
DROP TABLE IF EXISTS services_public.domain_events;
6+
7+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Revert schemas/services_public/tables/domain_verifications/table
2+
3+
BEGIN;
4+
5+
DROP TABLE IF EXISTS services_public.domain_verifications;
6+
7+
COMMIT;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Revert schemas/services_public/tables/managed_domains/table
2+
3+
BEGIN;
4+
5+
DROP TABLE IF EXISTS services_public.managed_domains;
6+
7+
COMMIT;

0 commit comments

Comments
 (0)