feat(device-core): add device ownership lifecycle

This commit is contained in:
Codex
2026-08-10 18:03:56 +03:00
parent 72db23c0e9
commit fceaca9546
17 changed files with 2218 additions and 54 deletions
@@ -0,0 +1,326 @@
begin;
create unique index if not exists device_projects_id_owner_scope_idx
on device_projects (id, owner_scope_id);
alter table device_instances
alter column contour_id drop not null,
add column if not exists owner_scope_id uuid references device_owner_scopes(id),
add column if not exists device_key text
check (
device_key is null
or device_key ~ '^[a-z][a-z0-9-]{1,62}$'
);
create unique index if not exists device_instances_project_key_idx
on device_instances (project_id, device_key)
where device_key is not null;
create unique index if not exists device_instances_id_project_owner_idx
on device_instances (id, project_id, owner_scope_id);
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'device_instances_project_owner_fk'
and conrelid = 'device_instances'::regclass
) then
alter table device_instances
add constraint device_instances_project_owner_fk
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id)
not valid;
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_instances_ownership_mode_check'
and conrelid = 'device_instances'::regclass
) then
alter table device_instances
add constraint device_instances_ownership_mode_check
check (
(
owner_scope_id is not null
and project_id is not null
)
or
(
owner_scope_id is null
and project_id is null
and contour_id is not null
)
) not valid;
end if;
end
$$;
alter table device_discoveries
add column if not exists session_ref text
check (
session_ref is null
or length(btrim(session_ref)) between 3 and 256
),
add column if not exists project_id uuid references device_projects(id),
add column if not exists route_id uuid,
add column if not exists enrollment_intent_id uuid,
add column if not exists resolution_code text
check (
resolution_code is null
or resolution_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
add column if not exists resolved_at timestamptz,
add column if not exists resolved_by_ref text
check (
resolved_by_ref is null
or length(btrim(resolved_by_ref)) between 3 and 256
);
create unique index if not exists device_discoveries_id_project_idx
on device_discoveries (id, project_id);
create index if not exists device_discoveries_route_state_seen_idx
on device_discoveries (route_id, lifecycle_state, last_observed_at desc)
where route_id is not null;
create unique index if not exists device_enrollment_intents_context_idx
on device_enrollment_intents (
id,
project_id,
route_id,
model_profile_ref
);
create unique index if not exists device_enrollment_intents_active_identity_idx
on device_enrollment_intents (
expected_identifier_kind,
expected_identifier_digest,
model_profile_ref
)
where lifecycle_state in ('pending', 'observed', 'claimed');
alter table device_enrollment_intents
add column if not exists observed_discovery_id uuid,
add column if not exists observed_at timestamptz,
add column if not exists claimed_at timestamptz,
add column if not exists resolution_code text
check (
resolution_code is null
or resolution_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
add column if not exists resolved_at timestamptz,
add column if not exists resolved_by_ref text
check (
resolved_by_ref is null
or length(btrim(resolved_by_ref)) between 3 and 256
);
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'device_discoveries_route_context_fk'
and conrelid = 'device_discoveries'::regclass
) then
alter table device_discoveries
add constraint device_discoveries_route_context_fk
foreign key (route_id, project_id, model_profile_ref)
references device_routes(id, project_id, model_profile_ref)
not valid;
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_discoveries_enrollment_context_fk'
and conrelid = 'device_discoveries'::regclass
) then
alter table device_discoveries
add constraint device_discoveries_enrollment_context_fk
foreign key (
enrollment_intent_id,
project_id,
route_id,
model_profile_ref
) references device_enrollment_intents (
id,
project_id,
route_id,
model_profile_ref
) not valid;
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_discoveries_route_context_check'
and conrelid = 'device_discoveries'::regclass
) then
alter table device_discoveries
add constraint device_discoveries_route_context_check
check (
(project_id is null and route_id is null)
or
(project_id is not null and route_id is not null)
) not valid;
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_discoveries_enrollment_context_check'
and conrelid = 'device_discoveries'::regclass
) then
alter table device_discoveries
add constraint device_discoveries_enrollment_context_check
check (
enrollment_intent_id is null
or (project_id is not null and route_id is not null)
) not valid;
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_enrollment_observed_discovery_fk'
and conrelid = 'device_enrollment_intents'::regclass
) then
alter table device_enrollment_intents
add constraint device_enrollment_observed_discovery_fk
foreign key (observed_discovery_id, project_id)
references device_discoveries(id, project_id)
not valid;
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_enrollment_lifecycle_evidence_check'
and conrelid = 'device_enrollment_intents'::regclass
) then
alter table device_enrollment_intents
add constraint device_enrollment_lifecycle_evidence_check
check (
lifecycle_state not in ('observed', 'claimed')
or (observed_discovery_id is not null and observed_at is not null)
) not valid;
end if;
end
$$;
alter table device_sessions
drop constraint if exists device_sessions_device_id_project_id_fkey;
alter table device_enrollment_intents
drop constraint if exists device_enrollment_intents_claimed_device_id_project_id_fkey;
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'device_sessions_device_id_fk'
and conrelid = 'device_sessions'::regclass
) then
alter table device_sessions
add constraint device_sessions_device_id_fk
foreign key (device_id) references device_instances(id)
not valid;
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_enrollment_claimed_device_id_fk'
and conrelid = 'device_enrollment_intents'::regclass
) then
alter table device_enrollment_intents
add constraint device_enrollment_claimed_device_id_fk
foreign key (claimed_device_id) references device_instances(id)
not valid;
end if;
end
$$;
create or replace function device_assert_session_current_project()
returns trigger
language plpgsql
as $$
begin
if new.device_id is not null and not exists (
select 1 from device_instances di
where di.id = new.device_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using
message = 'device_session_project_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_sessions_current_project_guard
on device_sessions;
create trigger device_sessions_current_project_guard
before insert or update of device_id, project_id
on device_sessions
for each row
execute function device_assert_session_current_project();
create or replace function device_assert_enrollment_current_project()
returns trigger
language plpgsql
as $$
begin
if new.claimed_device_id is not null and not exists (
select 1 from device_instances di
where di.id = new.claimed_device_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using
message = 'device_enrollment_project_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_enrollment_current_project_guard
on device_enrollment_intents;
create trigger device_enrollment_current_project_guard
before insert or update of claimed_device_id, project_id
on device_enrollment_intents
for each row
execute function device_assert_enrollment_current_project();
create table if not exists device_ownership_transitions (
id uuid primary key,
device_id uuid not null references device_instances(id),
transition_kind text not null
check (transition_kind in ('claim', 'transfer')),
source_owner_scope_id uuid,
source_project_id uuid,
target_owner_scope_id uuid not null,
target_project_id uuid not null,
actor_ref text not null
check (length(btrim(actor_ref)) between 3 and 256),
occurred_at timestamptz not null default now(),
foreign key (source_project_id, source_owner_scope_id)
references device_projects(id, owner_scope_id),
foreign key (target_project_id, target_owner_scope_id)
references device_projects(id, owner_scope_id),
check (
(
transition_kind = 'claim'
and source_owner_scope_id is null
and source_project_id is null
)
or
(
transition_kind = 'transfer'
and source_owner_scope_id is not null
and source_project_id is not null
and (
source_owner_scope_id <> target_owner_scope_id
or source_project_id <> target_project_id
)
)
)
);
create unique index if not exists device_ownership_single_claim_idx
on device_ownership_transitions (device_id)
where transition_kind = 'claim';
create index if not exists device_ownership_device_time_idx
on device_ownership_transitions (device_id, occurred_at desc);
commit;
@@ -0,0 +1,25 @@
begin;
alter table device_management_command_receipts
drop constraint if exists device_management_command_receipts_command_kind_check;
alter table device_management_command_receipts
add constraint device_management_command_receipts_command_kind_check
check (command_kind in (
'owner_scope.ensure',
'project.ensure',
'collection.ensure',
'project_grant.upsert',
'adapter_package.ensure',
'adapter_version.register',
'model_profile.register',
'edge.ensure',
'route.ensure',
'enrollment_intent.ensure',
'device.claim',
'device.transfer',
'discovery.reject',
'discovery.expire'
));
commit;