feat: establish standalone Device Core repository

This commit is contained in:
DCCONSTRUCTIONS
2026-08-21 11:51:21 +03:00
commit e0bac205d0
244 changed files with 51962 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
FROM node:22-alpine
WORKDIR /app/services/device-control-core
COPY services/device-control-core/package.json services/device-control-core/package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts
WORKDIR /app
COPY packages/device-protocol-contract ./packages/device-protocol-contract
COPY packages/device-edge-channel-contract ./packages/device-edge-channel-contract
COPY services/device-control-core ./services/device-control-core
USER node
CMD ["node", "services/device-control-core/src/server.mjs"]
@@ -0,0 +1,99 @@
begin;
create table if not exists device_model_profiles (
profile_ref text primary key,
schema_version text not null,
vendor text not null,
model text not null,
device_type text not null,
protocol text not null,
profile jsonb not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists device_contours (
id uuid primary key,
owner_scope text not null,
name text not null,
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'suspended', 'retired')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (owner_scope, name)
);
create table if not exists device_discoveries (
id uuid primary key,
identifier_kind text not null,
identifier_digest text not null,
identifier_masked text not null,
model_profile_ref text not null references device_model_profiles(profile_ref),
protocol text not null,
lifecycle_state text not null default 'quarantine'
check (lifecycle_state in ('quarantine', 'claimed', 'rejected', 'expired')),
first_observed_at timestamptz not null,
last_observed_at timestamptz not null,
evidence jsonb not null,
claimed_device_id uuid,
claimed_at timestamptz,
claimed_by text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (identifier_kind, identifier_digest, model_profile_ref)
);
create index if not exists device_discoveries_state_last_seen_idx
on device_discoveries (lifecycle_state, last_observed_at desc);
create table if not exists device_instances (
id uuid primary key,
contour_id uuid not null references device_contours(id),
model_profile_ref text not null references device_model_profiles(profile_ref),
display_name text not null,
identifier_kind text not null,
identifier_digest text not null,
identifier_masked text not null,
credential_ref text,
lifecycle_state text not null default 'claimed'
check (lifecycle_state in ('claimed', 'online', 'offline', 'suspended', 'retired')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (identifier_kind, identifier_digest, model_profile_ref)
);
alter table device_discoveries
drop constraint if exists device_discoveries_claimed_device_fk;
alter table device_discoveries
add constraint device_discoveries_claimed_device_fk
foreign key (claimed_device_id) references device_instances(id);
create table if not exists device_bindings (
id uuid primary key,
contour_id uuid not null references device_contours(id),
target_kind text not null,
target_ref text not null,
capabilities text[] not null,
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'revoked')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (contour_id, target_kind, target_ref)
);
create table if not exists device_audit_events (
id uuid primary key,
event_type text not null,
actor_ref text not null,
contour_id uuid,
device_id uuid,
discovery_id uuid,
payload jsonb not null,
occurred_at timestamptz not null default now()
);
create index if not exists device_audit_events_device_time_idx
on device_audit_events (device_id, occurred_at desc);
commit;
@@ -0,0 +1,113 @@
begin;
create table if not exists device_owner_scopes (
id uuid primary key,
scope_kind text not null
check (scope_kind in ('company', 'personal')),
owner_ref text not null
check (length(btrim(owner_ref)) between 3 and 256),
display_name text not null
check (length(btrim(display_name)) between 1 and 160),
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'suspended', 'retired')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (scope_kind, owner_ref)
);
create table if not exists device_projects (
id uuid primary key,
owner_scope_id uuid not null references device_owner_scopes(id),
project_key text not null
check (project_key ~ '^[a-z][a-z0-9-]{1,62}$'),
name text not null
check (length(btrim(name)) between 1 and 160),
description text,
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'suspended', 'archived')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (owner_scope_id, project_key)
);
create index if not exists device_projects_owner_scope_idx
on device_projects (owner_scope_id, lifecycle_state, updated_at desc);
alter table device_instances
add column if not exists project_id uuid references device_projects(id);
create unique index if not exists device_instances_id_project_idx
on device_instances (id, project_id);
create table if not exists device_collections (
id uuid primary key,
project_id uuid not null references device_projects(id),
collection_key text not null
check (collection_key ~ '^[a-z][a-z0-9-]{1,62}$'),
name text not null
check (length(btrim(name)) between 1 and 160),
description text,
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'archived')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, collection_key),
unique (id, project_id)
);
create index if not exists device_collections_project_idx
on device_collections (project_id, lifecycle_state, updated_at desc);
create table if not exists device_collection_members (
collection_id uuid not null,
device_id uuid not null,
project_id uuid not null references device_projects(id),
added_by_ref text not null
check (length(btrim(added_by_ref)) between 3 and 256),
added_at timestamptz not null default now(),
primary key (collection_id, device_id),
foreign key (collection_id, project_id)
references device_collections(id, project_id),
foreign key (device_id, project_id)
references device_instances(id, project_id)
);
create index if not exists device_collection_members_device_idx
on device_collection_members (device_id, collection_id);
create table if not exists device_project_grants (
id uuid primary key,
project_id uuid not null references device_projects(id),
principal_kind text not null
check (principal_kind in ('user', 'group')),
principal_ref text not null
check (length(btrim(principal_ref)) between 3 and 256),
project_role text not null
check (project_role in ('viewer', 'operator', 'engineer', 'admin', 'owner')),
capability_allow text[] not null default '{}',
capability_deny text[] not null default '{}',
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'revoked')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, principal_kind, principal_ref),
check (not (capability_allow && capability_deny))
);
create index if not exists device_project_grants_principal_idx
on device_project_grants (
principal_kind,
principal_ref,
lifecycle_state,
project_id
);
commit;
@@ -0,0 +1,68 @@
begin;
do $$
begin
if not exists (
select 1
from pg_constraint
where conname = 'device_project_grants_owner_user_only'
and conrelid = 'device_project_grants'::regclass
) then
alter table device_project_grants
add constraint device_project_grants_owner_user_only
check (project_role <> 'owner' or principal_kind = 'user');
end if;
end
$$;
alter table device_audit_events
add column if not exists project_id uuid references device_projects(id);
create index if not exists device_audit_events_project_time_idx
on device_audit_events (project_id, occurred_at desc);
create table if not exists device_management_command_receipts (
id uuid primary key,
actor_ref text not null
check (length(btrim(actor_ref)) between 3 and 256),
command_kind text not null
check (command_kind in (
'owner_scope.ensure',
'project.ensure',
'collection.ensure',
'project_grant.upsert'
)),
idempotency_key text not null
check (length(idempotency_key) between 8 and 256),
request_digest text not null
check (request_digest ~ '^sha256:[a-f0-9]{64}$'),
lifecycle_state text not null default 'pending'
check (lifecycle_state in ('pending', 'completed')),
response_status integer
check (response_status between 200 and 599),
response_body jsonb,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
completed_at timestamptz,
unique (actor_ref, command_kind, idempotency_key),
check (
(
lifecycle_state = 'pending'
and response_status is null
and response_body is null
and completed_at is null
)
or
(
lifecycle_state = 'completed'
and response_status is not null
and response_body is not null
and completed_at is not null
)
)
);
create index if not exists device_management_receipts_created_idx
on device_management_command_receipts (created_at desc);
commit;
@@ -0,0 +1,208 @@
begin;
create table if not exists device_adapter_packages (
id uuid primary key,
package_key text not null
check (package_key ~ '^[a-z][a-z0-9-]{1,62}$'),
display_name text not null
check (length(btrim(display_name)) between 1 and 160),
publisher_ref text not null
check (length(btrim(publisher_ref)) between 3 and 256),
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'retired')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (package_key)
);
create table if not exists device_adapter_versions (
id uuid primary key,
adapter_package_id uuid not null references device_adapter_packages(id),
version text not null
check (version ~ '^[0-9]+\.[0-9]+\.[0-9]+([+-][A-Za-z0-9.-]+)?$'),
runtime_package_ref text not null
check (length(btrim(runtime_package_ref)) between 3 and 256),
content_digest text not null
check (content_digest ~ '^sha256:[a-f0-9]{64}$'),
contract_version text not null
check (contract_version ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$'),
capabilities text[] not null default '{}'
check (
cardinality(capabilities) <= 64
and array_position(capabilities, null) is null
),
lifecycle_state text not null default 'draft'
check (lifecycle_state in ('draft', 'active', 'retired')),
registered_by_ref text not null
check (length(btrim(registered_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (adapter_package_id, version),
unique (runtime_package_ref, content_digest),
unique (id, adapter_package_id)
);
alter table device_model_profiles
add column if not exists adapter_version_id uuid
references device_adapter_versions(id),
add column if not exists schema_artifact_ref text
check (
schema_artifact_ref is null
or length(btrim(schema_artifact_ref)) between 3 and 256
),
add column if not exists profile_digest text
check (
profile_digest is null
or profile_digest ~ '^sha256:[a-f0-9]{64}$'
),
add column if not exists capabilities text[] not null default '{}'
check (
cardinality(capabilities) <= 64
and array_position(capabilities, null) is null
),
add column if not exists lifecycle_state text not null default 'active'
check (lifecycle_state in ('draft', 'active', 'retired'));
create index if not exists device_model_profiles_adapter_version_idx
on device_model_profiles (adapter_version_id, lifecycle_state, updated_at desc);
create table if not exists device_edges (
id uuid primary key,
edge_key text not null
check (edge_key ~ '^[a-z][a-z0-9-]{1,62}$'),
display_name text not null
check (length(btrim(display_name)) between 1 and 160),
deployment_ref text
check (
deployment_ref is null
or length(btrim(deployment_ref)) between 3 and 256
),
lifecycle_state text not null default 'provisioning'
check (lifecycle_state in ('provisioning', 'active', 'suspended', 'retired')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (edge_key)
);
create table if not exists device_routes (
id uuid primary key,
project_id uuid not null references device_projects(id),
route_key text not null
check (route_key ~ '^[a-z][a-z0-9-]{1,62}$'),
display_name text not null
check (length(btrim(display_name)) between 1 and 160),
edge_id uuid not null references device_edges(id),
model_profile_ref text not null references device_model_profiles(profile_ref),
listener_ref text not null
check (length(btrim(listener_ref)) between 3 and 256),
protocol text not null
check (protocol ~ '^[A-Z][A-Z0-9_]{0,31}$'),
direction text not null default 'telemetry'
check (direction in ('telemetry', 'bidirectional')),
lifecycle_state text not null default 'draft'
check (lifecycle_state in ('draft', 'active', 'suspended', 'retired')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, route_key),
unique (id, project_id),
unique (id, edge_id, project_id),
unique (id, project_id, model_profile_ref)
);
create index if not exists device_routes_edge_state_idx
on device_routes (edge_id, lifecycle_state, updated_at desc);
create index if not exists device_routes_project_state_idx
on device_routes (project_id, lifecycle_state, updated_at desc);
create table if not exists device_sessions (
id uuid primary key,
session_ref text not null
check (length(btrim(session_ref)) between 3 and 256),
edge_id uuid not null,
project_id uuid not null,
route_id uuid not null,
device_id uuid,
protocol text not null
check (protocol ~ '^[A-Z][A-Z0-9_]{0,31}$'),
lifecycle_state text not null default 'connecting'
check (lifecycle_state in ('connecting', 'online', 'closing', 'closed', 'rejected')),
connected_at timestamptz not null,
last_seen_at timestamptz not null,
disconnected_at timestamptz,
close_reason_code text
check (
close_reason_code is null
or close_reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
frame_count bigint not null default 0 check (frame_count >= 0),
byte_count bigint not null default 0 check (byte_count >= 0),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (edge_id, session_ref),
foreign key (route_id, edge_id, project_id)
references device_routes(id, edge_id, project_id),
foreign key (device_id, project_id)
references device_instances(id, project_id),
check (last_seen_at >= connected_at),
check (
(lifecycle_state in ('connecting', 'online') and disconnected_at is null)
or
(lifecycle_state in ('closing', 'closed', 'rejected'))
)
);
create index if not exists device_sessions_route_state_seen_idx
on device_sessions (route_id, lifecycle_state, last_seen_at desc);
create index if not exists device_sessions_device_seen_idx
on device_sessions (device_id, last_seen_at desc)
where device_id is not null;
create table if not exists device_enrollment_intents (
id uuid primary key,
project_id uuid not null references device_projects(id),
enrollment_key text not null
check (enrollment_key ~ '^[a-z][a-z0-9-]{1,62}$'),
route_id uuid not null,
model_profile_ref text not null references device_model_profiles(profile_ref),
display_name text not null
check (length(btrim(display_name)) between 1 and 160),
expected_identifier_kind text not null
check (expected_identifier_kind ~ '^[a-z][a-z0-9._-]{1,31}$'),
expected_identifier_digest text not null
check (expected_identifier_digest ~ '^hmac-sha256:[a-f0-9]{64}$'),
expected_identifier_masked text not null
check (length(btrim(expected_identifier_masked)) between 4 and 64),
lifecycle_state text not null default 'pending'
check (lifecycle_state in ('pending', 'observed', 'claimed', 'cancelled', 'expired')),
expires_at timestamptz,
claimed_device_id uuid,
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, enrollment_key),
unique (
project_id,
expected_identifier_kind,
expected_identifier_digest,
model_profile_ref
),
foreign key (route_id, project_id, model_profile_ref)
references device_routes(id, project_id, model_profile_ref),
foreign key (claimed_device_id, project_id)
references device_instances(id, project_id),
check (expires_at is null or expires_at > created_at)
);
create index if not exists device_enrollment_intents_project_state_idx
on device_enrollment_intents (project_id, lifecycle_state, updated_at desc);
commit;
@@ -0,0 +1,21 @@
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'
));
commit;
@@ -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;
@@ -0,0 +1,233 @@
begin;
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'device_instances_direct_legacy_credential_check'
and conrelid = 'device_instances'::regclass
) then
alter table device_instances
add constraint device_instances_direct_legacy_credential_check
check (owner_scope_id is null or credential_ref is null)
not valid;
end if;
end
$$;
create table if not exists device_restricted_identifiers (
id uuid primary key,
device_id uuid not null references device_instances(id),
owner_scope_id uuid not null,
project_id uuid not null,
identifier_kind text not null
check (identifier_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
identifier_digest text not null
check (identifier_digest ~ '^hmac-sha256:[a-f0-9]{64}$'),
identifier_masked text not null
check (
length(identifier_masked) between 5 and 128
and position('*' in identifier_masked) > 0
and identifier_masked !~ '[[:cntrl:]]'
and identifier_masked !~ '(^|[^0-9])[0-9]{15}([^0-9]|$)'
),
provenance_kind text not null
check (provenance_kind in ('claim', 'adapter_observation')),
is_primary boolean not null default false,
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'revoked')),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
revoked_at timestamptz,
revoked_by_ref text
check (
revoked_by_ref is null
or length(btrim(revoked_by_ref)) between 3 and 256
),
revocation_code text
check (
revocation_code is null
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id),
check (
(lifecycle_state = 'active' and revoked_at is null and revoked_by_ref is null and revocation_code is null)
or
(lifecycle_state = 'revoked' and revoked_at is not null and revoked_by_ref is not null and revocation_code is not null)
)
);
create unique index if not exists device_restricted_identifiers_active_identity_idx
on device_restricted_identifiers (identifier_kind, identifier_digest)
where lifecycle_state = 'active';
create unique index if not exists device_restricted_identifiers_primary_idx
on device_restricted_identifiers (device_id)
where lifecycle_state = 'active' and is_primary;
create index if not exists device_restricted_identifiers_device_idx
on device_restricted_identifiers (device_id, lifecycle_state, created_at);
create or replace function device_assert_identifier_current_owner()
returns trigger
language plpgsql
as $$
begin
if new.lifecycle_state = 'active' and not exists (
select 1 from device_instances di
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using
message = 'device_identifier_ownership_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_restricted_identifiers_owner_guard
on device_restricted_identifiers;
create trigger device_restricted_identifiers_owner_guard
before insert or update of device_id, owner_scope_id, project_id, lifecycle_state
on device_restricted_identifiers
for each row
execute function device_assert_identifier_current_owner();
create or replace function device_assert_active_identifiers_follow_owner()
returns trigger
language plpgsql
as $$
begin
if exists (
select 1 from device_restricted_identifiers dri
where dri.device_id = new.id
and dri.lifecycle_state = 'active'
and (
dri.owner_scope_id is distinct from new.owner_scope_id
or dri.project_id is distinct from new.project_id
)
) then
raise foreign_key_violation using
message = 'device_active_identifier_ownership_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_instances_identifier_owner_guard
on device_instances;
create constraint trigger device_instances_identifier_owner_guard
after update
on device_instances
deferrable initially deferred
for each row
execute function device_assert_active_identifiers_follow_owner();
create table if not exists device_credential_bindings (
id uuid primary key,
device_id uuid not null references device_instances(id),
owner_scope_id uuid not null,
project_id uuid not null,
purpose text not null
check (purpose ~ '^[a-z][a-z0-9._-]{1,63}$'),
credential_owner text not null
check (credential_owner = 'ndc_l2_credentials'),
credential_ref text not null
check (credential_ref ~ '^ndc-credref:[A-Za-z0-9][A-Za-z0-9._:-]{7,240}$'),
lifecycle_state text not null default 'active'
check (lifecycle_state in ('active', 'revoked')),
bound_by_ref text not null
check (length(btrim(bound_by_ref)) between 3 and 256),
revoked_at timestamptz,
revoked_by_ref text
check (
revoked_by_ref is null
or length(btrim(revoked_by_ref)) between 3 and 256
),
revocation_code text
check (
revocation_code is null
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id),
check (
(lifecycle_state = 'active' and revoked_at is null and revoked_by_ref is null and revocation_code is null)
or
(lifecycle_state = 'revoked' and revoked_at is not null and revoked_by_ref is not null and revocation_code is not null)
)
);
create unique index if not exists device_credential_bindings_active_purpose_idx
on device_credential_bindings (device_id, purpose)
where lifecycle_state = 'active';
create index if not exists device_credential_bindings_project_state_idx
on device_credential_bindings (project_id, lifecycle_state, updated_at desc);
create or replace function device_assert_credential_binding_current_owner()
returns trigger
language plpgsql
as $$
begin
if new.lifecycle_state = 'active' and not exists (
select 1 from device_instances di
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using
message = 'device_credential_binding_ownership_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_credential_bindings_owner_guard
on device_credential_bindings;
create trigger device_credential_bindings_owner_guard
before insert or update of device_id, owner_scope_id, project_id, lifecycle_state
on device_credential_bindings
for each row
execute function device_assert_credential_binding_current_owner();
create or replace function device_require_credential_revoke_before_transfer()
returns trigger
language plpgsql
as $$
begin
if exists (
select 1 from device_credential_bindings dcb
where dcb.device_id = old.id
and dcb.lifecycle_state = 'active'
) then
raise check_violation using
message = 'device_transfer_active_credential_binding';
end if;
return new;
end
$$;
drop trigger if exists device_instances_credential_transfer_guard
on device_instances;
create trigger device_instances_credential_transfer_guard
before update of owner_scope_id, project_id
on device_instances
for each row
when (
old.owner_scope_id is distinct from new.owner_scope_id
or old.project_id is distinct from new.project_id
)
execute function device_require_credential_revoke_before_transfer();
commit;
@@ -0,0 +1,27 @@
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',
'device_credential_binding.upsert',
'device_credential_binding.revoke'
));
commit;
@@ -0,0 +1,610 @@
begin;
create table if not exists device_resource_bindings (
id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
binding_key text not null
check (binding_key ~ '^[a-z][a-z0-9-]{1,62}$'),
display_name text not null
check (length(btrim(display_name)) between 1 and 160),
source_kind text not null
check (source_kind in ('device', 'collection')),
device_id uuid references device_instances(id),
collection_id uuid references device_collections(id),
target_kind text not null
check (target_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
target_ref text not null
check (length(btrim(target_ref)) between 3 and 256),
capabilities text[] not null default '{}'
check (
cardinality(capabilities) between 1 and 16
and array_position(capabilities, null) is null
),
lifecycle_state text not null default 'pending_external_approval'
check (lifecycle_state in ('pending_external_approval', 'active', 'revoked')),
source_approved_by_ref text not null
check (length(btrim(source_approved_by_ref)) between 3 and 256),
source_approved_at timestamptz not null default now(),
external_approval_ref text
check (
external_approval_ref is null
or length(btrim(external_approval_ref)) between 3 and 256
),
external_approval_digest text
check (
external_approval_digest is null
or external_approval_digest ~ '^sha256:[a-f0-9]{64}$'
),
external_approved_at timestamptz,
revoked_at timestamptz,
revoked_by_ref text
check (
revoked_by_ref is null
or length(btrim(revoked_by_ref)) between 3 and 256
),
revocation_code text
check (
revocation_code is null
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, binding_key),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id),
foreign key (collection_id, project_id)
references device_collections(id, project_id),
check (
(source_kind = 'device' and device_id is not null and collection_id is null)
or
(source_kind = 'collection' and device_id is null and collection_id is not null)
),
check (
(
lifecycle_state = 'pending_external_approval'
and external_approval_ref is null
and external_approval_digest is null
and external_approved_at is null
and revoked_at is null
and revoked_by_ref is null
and revocation_code is null
)
or
(
lifecycle_state = 'active'
and external_approval_ref is not null
and external_approval_digest is not null
and external_approved_at is not null
and revoked_at is null
and revoked_by_ref is null
and revocation_code is null
)
or
(
lifecycle_state = 'revoked'
and revoked_at is not null
and revoked_by_ref is not null
and revocation_code is not null
)
)
);
create index if not exists device_resource_bindings_project_state_idx
on device_resource_bindings (project_id, lifecycle_state, updated_at desc);
create index if not exists device_resource_bindings_device_state_idx
on device_resource_bindings (device_id, lifecycle_state, updated_at desc)
where device_id is not null;
create or replace function device_assert_binding_source_scope()
returns trigger
language plpgsql
as $$
begin
if new.source_kind = 'device' and not exists (
select 1 from device_instances di
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using
message = 'device_binding_source_scope_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_resource_bindings_source_guard
on device_resource_bindings;
create trigger device_resource_bindings_source_guard
before insert or update of owner_scope_id, project_id, source_kind, device_id, collection_id
on device_resource_bindings
for each row
execute function device_assert_binding_source_scope();
create table if not exists device_configuration_revisions (
id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
device_id uuid not null references device_instances(id),
revision_number bigint not null check (revision_number > 0),
model_profile_ref text not null references device_model_profiles(profile_ref),
schema_artifact_ref text not null
check (length(btrim(schema_artifact_ref)) between 3 and 256),
configuration_digest text not null
check (configuration_digest ~ '^sha256:[a-f0-9]{64}$'),
configuration jsonb not null
check (
jsonb_typeof(configuration) = 'object'
and octet_length(configuration::text) <= 65536
),
change_summary text
check (change_summary is null or length(change_summary) <= 1000),
created_by_ref text not null
check (length(btrim(created_by_ref)) between 3 and 256),
created_at timestamptz not null default now(),
unique (device_id, revision_number),
unique (id, device_id, project_id),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id)
);
create index if not exists device_configuration_revisions_project_idx
on device_configuration_revisions (project_id, device_id, revision_number desc);
create or replace function device_assert_configuration_revision_scope()
returns trigger
language plpgsql
as $$
begin
if not exists (
select 1
from device_instances di
join device_model_profiles dmp
on dmp.profile_ref = di.model_profile_ref
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
and di.model_profile_ref = new.model_profile_ref
and dmp.schema_artifact_ref = new.schema_artifact_ref
and dmp.lifecycle_state = 'active'
) then
raise foreign_key_violation using
message = 'device_configuration_revision_scope_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_configuration_revisions_scope_guard
on device_configuration_revisions;
create trigger device_configuration_revisions_scope_guard
before insert
on device_configuration_revisions
for each row
execute function device_assert_configuration_revision_scope();
create table if not exists device_configuration_state (
device_id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
desired_revision_id uuid,
applied_revision_id uuid,
applied_at timestamptz,
applied_by_ref text,
updated_at timestamptz not null default now(),
foreign key (device_id, project_id, owner_scope_id)
references device_instances(id, project_id, owner_scope_id),
foreign key (desired_revision_id, device_id, project_id)
references device_configuration_revisions(id, device_id, project_id),
foreign key (applied_revision_id, device_id, project_id)
references device_configuration_revisions(id, device_id, project_id),
check (desired_revision_id is not null or applied_revision_id is not null),
check (
(applied_revision_id is null and applied_at is null and applied_by_ref is null)
or
(applied_revision_id is not null and applied_at is not null and applied_by_ref is not null)
)
);
create table if not exists device_commands (
id uuid primary key,
owner_scope_id uuid not null,
project_id uuid not null,
device_id uuid not null references device_instances(id),
command_key text not null
check (command_key ~ '^[a-z][a-z0-9-]{1,62}$'),
command_catalog_ref text not null
check (length(btrim(command_catalog_ref)) between 3 and 256),
command_type text not null
check (command_type ~ '^[a-z][a-z0-9._:-]{1,63}$'),
risk_class text not null
check (risk_class in ('low', 'moderate', 'high', 'critical')),
parameters_digest text not null
check (parameters_digest ~ '^sha256:[a-f0-9]{64}$'),
parameters_projection jsonb not null
check (
jsonb_typeof(parameters_projection) = 'object'
and octet_length(parameters_projection::text) <= 16384
),
lifecycle_state text not null default 'draft'
check (lifecycle_state in (
'draft',
'planned',
'awaiting_confirmation',
'queued',
'dispatched',
'acknowledged',
'verified',
'failed',
'expired',
'unknown'
)),
planned_by_ref text not null
check (length(btrim(planned_by_ref)) between 3 and 256),
planned_at timestamptz not null default now(),
expires_at timestamptz not null,
confirmed_by_ref text,
confirmed_at timestamptz,
dispatched_at timestamptz,
transport_message_ref text
check (
transport_message_ref is null
or length(btrim(transport_message_ref)) between 3 and 256
),
acknowledged_at timestamptz,
terminal_at timestamptz,
terminal_reason_code text
check (
terminal_reason_code is null
or terminal_reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'
),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (project_id, command_key),
unique (id, device_id, project_id),
foreign key (project_id, owner_scope_id)
references device_projects(id, owner_scope_id),
check (expires_at > planned_at),
check (
(confirmed_at is null and confirmed_by_ref is null)
or
(confirmed_at is not null and confirmed_by_ref is not null)
),
check (
(
lifecycle_state in ('dispatched', 'acknowledged', 'verified', 'unknown')
and dispatched_at is not null
and transport_message_ref is not null
)
or lifecycle_state not in ('dispatched', 'acknowledged', 'verified', 'unknown')
),
check (
(lifecycle_state in ('acknowledged', 'verified') and acknowledged_at is not null)
or lifecycle_state not in ('acknowledged', 'verified')
),
check (
(
lifecycle_state in ('verified', 'failed', 'expired', 'unknown')
and terminal_at is not null
and terminal_reason_code is not null
)
or
(
lifecycle_state not in ('verified', 'failed', 'expired', 'unknown')
and terminal_at is null
and terminal_reason_code is null
)
)
);
create index if not exists device_commands_project_state_idx
on device_commands (project_id, lifecycle_state, updated_at desc);
create index if not exists device_commands_device_state_idx
on device_commands (device_id, lifecycle_state, updated_at desc);
create table if not exists device_command_events (
id uuid primary key,
command_id uuid not null,
device_id uuid not null,
project_id uuid not null,
sequence_number bigint not null check (sequence_number > 0),
from_state text,
to_state text not null
check (to_state in (
'draft',
'planned',
'awaiting_confirmation',
'queued',
'dispatched',
'acknowledged',
'verified',
'failed',
'expired',
'unknown'
)),
actor_ref text not null
check (length(btrim(actor_ref)) between 3 and 256),
reason_code text not null
check (reason_code ~ '^[a-z][a-z0-9._-]{1,63}$'),
evidence_ref text
check (
evidence_ref is null
or length(btrim(evidence_ref)) between 3 and 256
),
occurred_at timestamptz not null default now(),
unique (command_id, sequence_number),
foreign key (command_id, device_id, project_id)
references device_commands(id, device_id, project_id),
check (sequence_number = 1 or from_state is not null),
check (sequence_number <> 1 or from_state is null),
check (
from_state is null
or from_state in (
'draft',
'planned',
'awaiting_confirmation',
'queued',
'dispatched',
'acknowledged',
'verified',
'failed',
'expired',
'unknown'
)
)
);
create index if not exists device_command_events_command_idx
on device_command_events (command_id, sequence_number);
create or replace function device_assert_command_scope()
returns trigger
language plpgsql
as $$
begin
if not exists (
select 1 from device_instances di
where di.id = new.device_id
and di.owner_scope_id = new.owner_scope_id
and di.project_id = new.project_id
) then
raise foreign_key_violation using message = 'device_command_scope_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_commands_scope_guard on device_commands;
create trigger device_commands_scope_guard
before insert
on device_commands
for each row
execute function device_assert_command_scope();
create or replace function device_assert_command_event_sequence()
returns trigger
language plpgsql
as $$
declare
previous_state text;
begin
if new.sequence_number = 1 then
if new.from_state is not null or new.to_state <> 'draft' then
raise check_violation using message = 'device_command_initial_event_invalid';
end if;
return new;
end if;
select dce.to_state into previous_state
from device_command_events dce
where dce.command_id = new.command_id
and dce.sequence_number = new.sequence_number - 1;
if previous_state is null or previous_state <> new.from_state then
raise check_violation using message = 'device_command_event_sequence_invalid';
end if;
if not (
(new.from_state = 'draft' and new.to_state in ('planned', 'expired'))
or (new.from_state = 'planned' and new.to_state in ('awaiting_confirmation', 'queued', 'expired'))
or (new.from_state = 'awaiting_confirmation' and new.to_state in ('queued', 'expired'))
or (new.from_state = 'queued' and new.to_state in ('dispatched', 'failed', 'expired'))
or (new.from_state = 'dispatched' and new.to_state in ('acknowledged', 'failed', 'unknown'))
or (new.from_state = 'acknowledged' and new.to_state in ('verified', 'failed', 'unknown'))
) then
raise check_violation using message = 'device_command_event_transition_invalid';
end if;
return new;
end
$$;
drop trigger if exists device_command_events_sequence_guard
on device_command_events;
create trigger device_command_events_sequence_guard
before insert
on device_command_events
for each row
execute function device_assert_command_event_sequence();
create or replace function device_assert_command_transition()
returns trigger
language plpgsql
as $$
begin
if old.lifecycle_state = new.lifecycle_state then
return new;
end if;
if not (
(old.lifecycle_state = 'draft' and new.lifecycle_state in ('planned', 'expired'))
or (old.lifecycle_state = 'planned' and new.lifecycle_state in ('awaiting_confirmation', 'queued', 'expired'))
or (old.lifecycle_state = 'awaiting_confirmation' and new.lifecycle_state in ('queued', 'expired'))
or (old.lifecycle_state = 'queued' and new.lifecycle_state in ('dispatched', 'failed', 'expired'))
or (old.lifecycle_state = 'dispatched' and new.lifecycle_state in ('acknowledged', 'failed', 'unknown'))
or (old.lifecycle_state = 'acknowledged' and new.lifecycle_state in ('verified', 'failed', 'unknown'))
) then
raise check_violation using message = 'device_command_transition_invalid';
end if;
return new;
end
$$;
drop trigger if exists device_commands_transition_guard on device_commands;
create trigger device_commands_transition_guard
before update of lifecycle_state
on device_commands
for each row
execute function device_assert_command_transition();
create or replace function device_assert_command_event_projection()
returns trigger
language plpgsql
as $$
declare
current_state text;
latest_event_state text;
begin
select dc.lifecycle_state into current_state
from device_commands dc
where dc.id = new.id;
select dce.to_state into latest_event_state
from device_command_events dce
where dce.command_id = new.id
order by dce.sequence_number desc
limit 1;
if current_state is null or latest_event_state is distinct from current_state then
raise check_violation using message = 'device_command_event_projection_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_commands_event_projection_guard on device_commands;
create constraint trigger device_commands_event_projection_guard
after insert or update of lifecycle_state
on device_commands
deferrable initially deferred
for each row
execute function device_assert_command_event_projection();
create or replace function device_assert_command_current_projection()
returns trigger
language plpgsql
as $$
declare
current_state text;
latest_event_state text;
begin
select dc.lifecycle_state into current_state
from device_commands dc
where dc.id = new.command_id;
select dce.to_state into latest_event_state
from device_command_events dce
where dce.command_id = new.command_id
order by dce.sequence_number desc
limit 1;
if current_state is null or latest_event_state is distinct from current_state then
raise check_violation using message = 'device_command_current_projection_mismatch';
end if;
return new;
end
$$;
drop trigger if exists device_command_events_current_projection_guard
on device_command_events;
create constraint trigger device_command_events_current_projection_guard
after insert
on device_command_events
deferrable initially deferred
for each row
execute function device_assert_command_current_projection();
create or replace function device_reject_immutable_mutation()
returns trigger
language plpgsql
as $$
begin
raise check_violation using message = 'device_immutable_record_mutation_forbidden';
end
$$;
drop trigger if exists device_configuration_revisions_immutable_guard
on device_configuration_revisions;
create trigger device_configuration_revisions_immutable_guard
before update or delete or truncate
on device_configuration_revisions
for each statement
execute function device_reject_immutable_mutation();
drop trigger if exists device_command_events_immutable_guard
on device_command_events;
create trigger device_command_events_immutable_guard
before update or delete or truncate
on device_command_events
for each statement
execute function device_reject_immutable_mutation();
drop trigger if exists device_audit_events_immutable_guard
on device_audit_events;
create trigger device_audit_events_immutable_guard
before update or delete or truncate
on device_audit_events
for each statement
execute function device_reject_immutable_mutation();
create or replace function device_require_control_resources_clear_before_transfer()
returns trigger
language plpgsql
as $$
begin
if exists (
select 1 from device_resource_bindings drb
where drb.device_id = old.id
and drb.lifecycle_state in ('pending_external_approval', 'active')
) then
raise check_violation using message = 'device_transfer_active_resource_binding';
end if;
if exists (
select 1 from device_configuration_state dcs
where dcs.device_id = old.id
and dcs.applied_revision_id is not null
) then
raise check_violation using message = 'device_transfer_applied_configuration';
end if;
if exists (
select 1 from device_commands dc
where dc.device_id = old.id
and dc.lifecycle_state not in ('verified', 'failed', 'expired', 'unknown')
) then
raise check_violation using message = 'device_transfer_nonterminal_command';
end if;
return new;
end
$$;
drop trigger if exists device_instances_control_resource_transfer_guard
on device_instances;
create trigger device_instances_control_resource_transfer_guard
before update of owner_scope_id, project_id
on device_instances
for each row
when (
old.owner_scope_id is distinct from new.owner_scope_id
or old.project_id is distinct from new.project_id
)
execute function device_require_control_resources_clear_before_transfer();
commit;
@@ -0,0 +1,31 @@
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',
'device_credential_binding.upsert',
'device_credential_binding.revoke',
'device_binding.ensure',
'device_binding.revoke',
'device_configuration_revision.create',
'device_configuration_desired.set'
));
commit;
@@ -0,0 +1,66 @@
begin;
create table if not exists device_gateway_message_receipts (
id uuid primary key,
idempotency_key text not null
check (idempotency_key ~ '^sha256:[a-f0-9]{64}$'),
request_digest text not null
check (request_digest ~ '^sha256:[a-f0-9]{64}$'),
edge_ref text not null
check (length(btrim(edge_ref)) between 3 and 128),
adapter_ref text not null
check (adapter_ref ~ '^[a-z][a-z0-9-]{1,62}$'),
protocol_profile_ref text not null
references device_model_profiles(profile_ref),
protocol text not null
check (protocol ~ '^[A-Z][A-Z0-9_]{0,31}$'),
route_id uuid references device_routes(id),
project_id uuid references device_projects(id),
session_ref text not null
check (length(btrim(session_ref)) between 3 and 128),
message_ref text not null
check (length(btrim(message_ref)) between 3 and 128),
message_type text not null
check (message_type ~ '^[a-z][a-z0-9._-]{1,127}$'),
sequence bigint not null check (sequence > 0),
identifier_kind text not null
check (identifier_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
identifier_digest text not null
check (identifier_digest ~ '^hmac-sha256:[a-f0-9]{64}$'),
identifier_masked text not null
check (length(identifier_masked) between 5 and 128),
payload_schema_ref text not null
check (length(btrim(payload_schema_ref)) between 3 and 128),
payload jsonb not null,
observed_at timestamptz not null,
accepted_at timestamptz not null default now(),
unique (idempotency_key),
unique (edge_ref, session_ref, message_ref),
foreign key (route_id, project_id)
references device_routes(id, project_id),
check (
(route_id is null and project_id is null)
or (route_id is not null and project_id is not null)
)
);
create index if not exists device_gateway_message_receipts_route_time_idx
on device_gateway_message_receipts (route_id, accepted_at desc)
where route_id is not null;
create index if not exists device_gateway_message_receipts_identity_time_idx
on device_gateway_message_receipts (
identifier_kind,
identifier_digest,
accepted_at desc
);
drop trigger if exists device_gateway_message_receipts_immutable_guard
on device_gateway_message_receipts;
create trigger device_gateway_message_receipts_immutable_guard
before update or delete or truncate
on device_gateway_message_receipts
for each statement
execute function device_reject_immutable_mutation();
commit;
@@ -0,0 +1,54 @@
begin;
alter table device_edges
add column if not exists channel_endpoint text,
add column if not exists channel_servername text,
add column if not exists channel_generation_ref text,
add column if not exists channel_trust_bundle_ref text,
add column if not exists channel_certificate_identities jsonb not null
default '[]'::jsonb,
add column if not exists channel_lifecycle_state text not null
default 'disabled';
do $$
begin
if not exists (
select 1 from pg_constraint
where conname = 'device_edges_channel_lifecycle_state_check'
) then
alter table device_edges add constraint device_edges_channel_lifecycle_state_check
check (channel_lifecycle_state in ('disabled', 'active', 'revoked'));
end if;
if not exists (
select 1 from pg_constraint
where conname = 'device_edges_channel_configuration_check'
) then
alter table device_edges add constraint device_edges_channel_configuration_check
check (
(
channel_lifecycle_state = 'disabled'
and channel_endpoint is null
and channel_servername is null
and channel_generation_ref is null
and channel_trust_bundle_ref is null
and channel_certificate_identities = '[]'::jsonb
)
or
(
channel_lifecycle_state in ('active', 'revoked')
and length(btrim(channel_endpoint)) between 12 and 256
and channel_servername ~ '^[A-Za-z0-9.-]{1,253}$'
and channel_generation_ref ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
and channel_trust_bundle_ref ~ '^edge-trust:[a-z][a-z0-9-]{1,62}$'
and jsonb_typeof(channel_certificate_identities) = 'array'
and jsonb_array_length(channel_certificate_identities) between 1 and 2
)
);
end if;
end $$;
create index if not exists device_edges_active_channel_idx
on device_edges (channel_lifecycle_state, updated_at desc)
where channel_lifecycle_state = 'active';
commit;
@@ -0,0 +1,32 @@
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.update',
'device.transfer',
'discovery.reject',
'discovery.expire',
'device_credential_binding.upsert',
'device_credential_binding.revoke',
'device_binding.ensure',
'device_binding.revoke',
'device_configuration_revision.create',
'device_configuration_desired.set'
));
commit;
@@ -0,0 +1,20 @@
begin;
alter table device_instances
add column if not exists integration_device_id text;
alter table device_instances
drop constraint if exists device_instances_integration_device_id_check;
alter table device_instances
add constraint device_instances_integration_device_id_check
check (
integration_device_id is null
or (
char_length(integration_device_id) between 1 and 160
and integration_device_id = btrim(integration_device_id)
and integration_device_id !~ '[[:cntrl:]]'
)
);
commit;
+164
View File
@@ -0,0 +1,164 @@
{
"name": "@nodedc/device-control-core",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@nodedc/device-control-core",
"version": "0.1.0",
"dependencies": {
"pg": "8.22.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/pg": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.14.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.15.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz",
"integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@nodedc/device-control-core",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "node src/server.mjs",
"test": "node --test test/*.test.mjs"
},
"dependencies": {
"pg": "8.22.0"
},
"engines": {
"node": ">=20"
}
}
+505
View File
@@ -0,0 +1,505 @@
import { createHash, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import {
hashRestrictedIdentifier,
maskRestrictedIdentifier,
normalizeRestrictedIdentifier,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import { createDeviceGatewayIngest } from "./gateway-ingest.mjs";
import {
normalizeManagementActor,
} from "./project-management.mjs";
import { normalizeDeviceManagementCommand } from "./management-command.mjs";
const managementRoutes = new Map([
["/internal/v1/management/owner-scopes:ensure", "owner_scope.ensure"],
["/internal/v1/management/projects:ensure", "project.ensure"],
["/internal/v1/management/collections:ensure", "collection.ensure"],
["/internal/v1/management/project-grants:upsert", "project_grant.upsert"],
["/internal/v1/management/adapter-packages:ensure", "adapter_package.ensure"],
["/internal/v1/management/adapter-versions:register", "adapter_version.register"],
["/internal/v1/management/model-profiles:register", "model_profile.register"],
["/internal/v1/management/edges:ensure", "edge.ensure"],
["/internal/v1/management/routes:ensure", "route.ensure"],
["/internal/v1/management/enrollment-intents:ensure", "enrollment_intent.ensure"],
["/internal/v1/management/devices:claim", "device.claim"],
["/internal/v1/management/devices:update", "device.update"],
["/internal/v1/management/devices:transfer", "device.transfer"],
["/internal/v1/management/discoveries:reject", "discovery.reject"],
["/internal/v1/management/discoveries:expire", "discovery.expire"],
[
"/internal/v1/management/device-credential-bindings:upsert",
"device_credential_binding.upsert",
],
[
"/internal/v1/management/device-credential-bindings:revoke",
"device_credential_binding.revoke",
],
["/internal/v1/management/device-bindings:ensure", "device_binding.ensure"],
["/internal/v1/management/device-bindings:revoke", "device_binding.revoke"],
[
"/internal/v1/management/device-configuration-revisions:create",
"device_configuration_revision.create",
],
[
"/internal/v1/management/device-configurations:set-desired",
"device_configuration_desired.set",
],
]);
export function createControlCoreApp({
repository,
gatewayToken = "",
identifierPepper = "",
discoveryIngestEnabled = false,
managementApiEnabled = false,
managementToken = "",
gatewayIngest = null,
edgeChannelStatusProvider = null,
typedCommandRuntime = null,
} = {}) {
if (!repository || typeof repository.health !== "function") {
throw new TypeError("device_repository_required");
}
if (discoveryIngestEnabled) {
if (typeof repository.upsertQuarantineDiscovery !== "function") {
throw new TypeError("device_discovery_repository_required");
}
if (typeof repository.acceptAdapterMessage !== "function") {
throw new TypeError("device_gateway_message_repository_required");
}
if (typeof gatewayToken !== "string" || gatewayToken.length < 32) {
throw new TypeError("device_gateway_token_invalid");
}
if (typeof identifierPepper !== "string" || identifierPepper.length < 32) {
throw new TypeError("device_identifier_pepper_invalid");
}
}
if (managementApiEnabled) {
if (typeof repository.executeManagementCommand !== "function") {
throw new TypeError("device_management_repository_required");
}
if (typeof managementToken !== "string" || managementToken.length < 32) {
throw new TypeError("device_management_token_invalid");
}
if (typeof identifierPepper !== "string" || identifierPepper.length < 32) {
throw new TypeError("device_identifier_pepper_invalid");
}
}
const ingest = discoveryIngestEnabled
? gatewayIngest ?? createDeviceGatewayIngest({ repository, identifierPepper })
: gatewayIngest;
if (
ingest
&& (
typeof ingest.observeDiscovery !== "function"
|| typeof ingest.acceptMessage !== "function"
)
) {
throw new TypeError("device_gateway_ingest_invalid");
}
if (
typedCommandRuntime != null
&& (
typeof typedCommandRuntime.planServicePing !== "function"
|| typeof typedCommandRuntime.status !== "function"
)
) {
throw new TypeError("device_typed_command_runtime_invalid");
}
if (
edgeChannelStatusProvider != null
&& typeof edgeChannelStatusProvider !== "function"
) {
throw new TypeError("device_edge_channel_status_provider_invalid");
}
const server = createServer(async (request, response) => {
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.setHeader("X-Content-Type-Options", "nosniff");
try {
const requestUrl = new URL(
request.url || "/",
`http://${request.headers.host || "127.0.0.1"}`,
);
if (request.method === "GET" && requestUrl.pathname === "/healthz") {
const database = await repository.health();
return writeJson(response, 200, {
ok: true,
service: "nodedc-device-control-core",
database,
discoveryIngest: discoveryIngestEnabled ? "enabled" : "disabled",
managementApi: managementApiEnabled ? "enabled" : "disabled",
edgeChannels: edgeChannelStatusProvider
? edgeChannelStatusProvider()
: { enabled: false, configured: 0, accepted: 0, degraded: 0 },
commandTransport: typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
});
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/commands:service-ping"
) {
if (!managementApiEnabled || !typedCommandRuntime) {
return writeJson(response, 404, {
ok: false,
error: "device_command_transport_disabled",
});
}
if (!matchesBearer(request.headers.authorization, managementToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_management_auth_required",
});
}
const idempotencyKey = normalizeIdempotencyKey(
request.headers["idempotency-key"],
);
const actor = managementActorFromHeaders(request.headers);
const input = await readJsonBody(request, 8 * 1024);
const execution = await typedCommandRuntime.planServicePing({
idempotencyKey,
actor,
input,
});
response.setHeader("Idempotency-Key", idempotencyKey);
response.setHeader(
"Idempotency-Replayed",
execution.replayed ? "true" : "false",
);
return writeJson(response, 200, {
ok: true,
replayed: execution.replayed,
result: execution.command,
});
}
const managementCommandKind = managementRoutes.get(requestUrl.pathname);
if (request.method === "POST" && managementCommandKind) {
if (!managementApiEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_management_api_disabled",
});
}
if (!matchesBearer(request.headers.authorization, managementToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_management_auth_required",
});
}
const idempotencyKey = normalizeIdempotencyKey(
request.headers["idempotency-key"],
);
const actor = managementActorFromHeaders(request.headers);
const input = await readJsonBody(request, 64 * 1024);
const protectedInput = managementCommandKind === "enrollment_intent.ensure"
? protectEnrollmentIdentifier(input, identifierPepper)
: input;
const command = normalizeDeviceManagementCommand(
managementCommandKind,
protectedInput,
);
const requestDigest = managementRequestDigest({
actor,
commandKind: managementCommandKind,
command,
});
const execution = await repository.executeManagementCommand({
idempotencyKey,
commandKind: managementCommandKind,
requestDigest,
actor,
command,
});
response.setHeader("Idempotency-Key", idempotencyKey);
response.setHeader(
"Idempotency-Replayed",
execution.replayed ? "true" : "false",
);
return writeJson(response, 200, {
ok: true,
replayed: execution.replayed,
result: execution.result,
});
}
if (
request.method === "GET"
&& requestUrl.pathname === "/internal/v1/query/projects"
) {
if (!managementApiEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_management_api_disabled",
});
}
if (!matchesBearer(request.headers.authorization, managementToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_management_auth_required",
});
}
if (typeof repository.listAccessibleProjects !== "function") {
return writeJson(response, 503, {
ok: false,
error: "device_query_repository_unavailable",
});
}
const actor = managementActorFromHeaders(request.headers);
const projects = await repository.listAccessibleProjects(actor);
return writeJson(response, 200, { ok: true, projects });
}
const workspaceProjectId = projectWorkspaceId(requestUrl.pathname);
if (request.method === "GET" && workspaceProjectId) {
if (!managementApiEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_management_api_disabled",
});
}
if (!matchesBearer(request.headers.authorization, managementToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_management_auth_required",
});
}
if (typeof repository.getProjectWorkspace !== "function") {
return writeJson(response, 503, {
ok: false,
error: "device_query_repository_unavailable",
});
}
const actor = managementActorFromHeaders(request.headers);
const workspace = await repository.getProjectWorkspace(
actor,
workspaceProjectId,
{
commandTransport: typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
},
);
return writeJson(response, 200, { ok: true, workspace });
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/device-discoveries:observe"
) {
if (!discoveryIngestEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_discovery_ingest_disabled",
});
}
if (!matchesBearer(request.headers.authorization, gatewayToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_gateway_auth_required",
});
}
const input = await readJsonBody(request, 32 * 1024);
const discovery = await ingest.observeDiscovery(input);
return writeJson(response, discovery.created ? 201 : 200, {
ok: true,
created: discovery.created,
discovery: discovery.value,
});
}
if (
request.method === "POST"
&& requestUrl.pathname === "/internal/v1/gateway/messages:accept"
) {
if (!discoveryIngestEnabled) {
return writeJson(response, 404, {
ok: false,
error: "device_gateway_message_ingest_disabled",
});
}
if (!matchesBearer(request.headers.authorization, gatewayToken)) {
return writeJson(response, 401, {
ok: false,
error: "device_gateway_auth_required",
});
}
const input = await readJsonBody(request, 1024 * 1024);
const receipt = await ingest.acceptMessage(input);
const acceptance = receipt.value;
return writeJson(response, acceptance.replayed ? 200 : 201, {
ok: true,
acceptance,
});
}
return writeJson(response, 404, {
ok: false,
error: "device_control_core_route_not_found",
});
} catch (error) {
const status = Number(error?.statusCode || 400);
return writeJson(
response,
Number.isInteger(status) && status >= 400 && status < 600
? status
: 500,
{
ok: false,
error: safeErrorCode(error),
},
);
}
});
return server;
}
function protectEnrollmentIdentifier(input, identifierPepper) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new TypeError("device_enrollment_input_invalid");
}
const allowedKeys = new Set([
"projectRef",
"enrollmentKey",
"routeRef",
"modelProfileRef",
"displayName",
"identifier",
"expiresAt",
]);
for (const key of Object.keys(input)) {
if (!allowedKeys.has(key)) {
throw new TypeError("device_enrollment_input_field_unexpected");
}
}
const identifier = normalizeRestrictedIdentifier(input.identifier);
return Object.freeze({
projectRef: input.projectRef,
enrollmentKey: input.enrollmentKey,
routeRef: input.routeRef,
modelProfileRef: input.modelProfileRef,
displayName: input.displayName,
identifierKind: identifier.kind,
identifierDigest: hashRestrictedIdentifier(identifier, identifierPepper),
identifierMasked: maskRestrictedIdentifier(identifier),
expiresAt: input.expiresAt,
});
}
function projectWorkspaceId(pathname) {
const match = pathname.match(
/^\/internal\/v1\/query\/projects\/([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\/workspace$/i,
);
return match?.[1]?.toLowerCase() ?? null;
}
function managementActorFromHeaders(headers) {
return normalizeManagementActor({
userRef: singleHeader(headers["x-nodedc-user-ref"]),
hubRole: singleHeader(headers["x-nodedc-hub-role"]),
groupRefs: commaSeparatedHeader(headers["x-nodedc-group-refs"]),
ownerScopes: ownerScopeHeader(headers["x-nodedc-owner-scopes"]),
});
}
function ownerScopeHeader(value) {
return commaSeparatedHeader(value).map((claim) => {
const separatorIndex = claim.indexOf("=");
if (separatorIndex < 1 || separatorIndex === claim.length - 1) {
throw new TypeError("device_actor_owner_scopes_invalid");
}
return {
scopeKind: claim.slice(0, separatorIndex),
ownerRef: claim.slice(separatorIndex + 1),
};
});
}
function commaSeparatedHeader(value) {
const header = singleHeader(value, true);
if (!header) return [];
return header.split(",").map((item) => item.trim()).filter(Boolean);
}
function singleHeader(value, optional = false) {
if (Array.isArray(value)) throw new TypeError("device_management_header_invalid");
if (value == null || value === "") {
if (optional) return "";
throw new TypeError("device_management_header_required");
}
if (typeof value !== "string" || value.length > 4096) {
throw new TypeError("device_management_header_invalid");
}
return value.trim();
}
function normalizeIdempotencyKey(value) {
const key = singleHeader(value);
if (!/^[\x21-\x7e]{8,256}$/.test(key)) {
const error = new Error("device_idempotency_key_invalid");
error.statusCode = 400;
throw error;
}
return key;
}
function managementRequestDigest(value) {
return `sha256:${createHash("sha256")
.update(JSON.stringify(value), "utf8")
.digest("hex")}`;
}
function matchesBearer(header, expected) {
if (typeof header !== "string" || !header.startsWith("Bearer ")) return false;
const actual = Buffer.from(header.slice("Bearer ".length), "utf8");
const required = Buffer.from(expected, "utf8");
return (
actual.length === required.length
&& required.length > 0
&& timingSafeEqual(actual, required)
);
}
async function readJsonBody(request, maxBytes) {
const chunks = [];
let size = 0;
for await (const chunk of request) {
size += chunk.length;
if (size > maxBytes) {
const error = new Error("device_request_body_too_large");
error.statusCode = 413;
throw error;
}
chunks.push(chunk);
}
if (size === 0) throw new TypeError("device_request_body_required");
try {
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
} catch {
throw new TypeError("device_request_json_invalid");
}
}
function writeJson(response, status, body) {
response.statusCode = status;
return response.end(`${JSON.stringify(body)}\n`);
}
function safeErrorCode(error) {
const value = error instanceof Error ? error.message : "device_control_error";
return /^[a-z0-9_:-]{1,128}$/.test(value)
? value
: "device_control_error";
}
@@ -0,0 +1,284 @@
import { createHash } from "node:crypto";
import {
DEVICE_BINDING_CAPABILITIES,
assertSafeProjection,
} from "../../../packages/device-protocol-contract/src/index.mjs";
export const DEVICE_CONTROL_RESOURCE_COMMAND_KINDS = Object.freeze([
"device_binding.ensure",
"device_binding.revoke",
"device_configuration_revision.create",
"device_configuration_desired.set",
]);
const commandKindSet = new Set(DEVICE_CONTROL_RESOURCE_COMMAND_KINDS);
const bindingCapabilitySet = new Set(DEVICE_BINDING_CAPABILITIES);
const keyPattern = /^[a-z][a-z0-9-]{1,62}$/;
const tokenPattern = /^[a-z][a-z0-9._:-]{1,63}$/;
const resolutionPattern = /^[a-z][a-z0-9._-]{1,63}$/;
const targetRefPattern = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{2,255}$/;
const configurationKeyPattern = /^[a-z][a-z0-9._-]{0,63}$/;
const secretReferencePattern = /^(?:ndc-credref:|(?:bearer|basic)\s)|[?&](?:token|secret|password|api[_-]?key)=/i;
export function isControlResourceManagementCommand(kind) {
return commandKindSet.has(kind);
}
export function normalizeControlResourceManagementCommand(kind, input) {
if (!commandKindSet.has(kind)) {
throw new TypeError("device_control_resource_command_kind_invalid");
}
assertPlainObject(input, "device_control_resource_command_invalid");
if (kind === "device_binding.ensure") {
assertAllowedKeys(input, [
"projectRef",
"bindingKey",
"displayName",
"source",
"targetKind",
"targetRef",
"capabilities",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
bindingKey: normalizePattern(
input.bindingKey,
keyPattern,
"device_binding_key_invalid",
),
displayName: normalizeDisplayText(
input.displayName,
160,
"device_binding_name_invalid",
),
source: normalizeBindingSource(input.source),
targetKind: normalizePattern(
input.targetKind,
tokenPattern,
"device_binding_target_kind_invalid",
),
targetRef: normalizeTargetRef(input.targetRef),
capabilities: Object.freeze(normalizeBindingCapabilities(
input.capabilities,
)),
});
}
if (kind === "device_binding.revoke") {
assertAllowedKeys(input, ["projectRef", "bindingRef", "resolutionCode"]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
bindingId: normalizeEntityRef(input.bindingRef, "binding"),
resolutionCode: normalizePattern(
input.resolutionCode,
resolutionPattern,
"device_binding_resolution_code_invalid",
),
});
}
if (kind === "device_configuration_revision.create") {
assertAllowedKeys(input, [
"projectRef",
"deviceRef",
"configuration",
"changeSummary",
]);
const configuration = normalizeDeviceConfiguration(input.configuration);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
deviceId: normalizeEntityRef(input.deviceRef, "device"),
configuration,
configurationDigest: `sha256:${createHash("sha256")
.update(JSON.stringify(configuration), "utf8")
.digest("hex")}`,
changeSummary: normalizeOptionalText(
input.changeSummary,
1000,
"device_configuration_change_summary_invalid",
),
});
}
assertAllowedKeys(input, [
"projectRef",
"deviceRef",
"configurationRevisionRef",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
deviceId: normalizeEntityRef(input.deviceRef, "device"),
configurationRevisionId: normalizeEntityRef(
input.configurationRevisionRef,
"configuration-revision",
),
});
}
export function normalizeDeviceConfiguration(input) {
const normalized = normalizeConfigurationValue(input, 0, "$configuration");
if (!normalized || typeof normalized !== "object" || Array.isArray(normalized)) {
throw new TypeError("device_configuration_must_be_object");
}
if (Object.keys(normalized).length === 0) {
throw new TypeError("device_configuration_must_not_be_empty");
}
const serialized = JSON.stringify(normalized);
if (Buffer.byteLength(serialized, "utf8") > 32768) {
throw new TypeError("device_configuration_too_large");
}
assertSafeProjection({ configuration: normalized });
return deepFreeze(normalized);
}
function normalizeBindingSource(input) {
assertPlainObject(input, "device_binding_source_invalid");
assertAllowedKeys(input, ["kind", "ref"]);
if (input.kind === "device") {
return Object.freeze({
kind: "device",
id: normalizeEntityRef(input.ref, "device"),
});
}
if (input.kind === "collection") {
return Object.freeze({
kind: "collection",
id: normalizeEntityRef(input.ref, "collection"),
});
}
throw new TypeError("device_binding_source_kind_invalid");
}
function normalizeBindingCapabilities(input) {
if (!Array.isArray(input) || input.length < 1 || input.length > 16) {
throw new TypeError("device_binding_capabilities_invalid");
}
const normalized = input.map((value) => {
if (typeof value !== "string" || !bindingCapabilitySet.has(value)) {
throw new TypeError("device_binding_capability_invalid");
}
return value;
});
if (new Set(normalized).size !== normalized.length) {
throw new TypeError("device_binding_capabilities_duplicate");
}
return normalized.sort();
}
function normalizeTargetRef(value) {
if (
typeof value !== "string"
|| !targetRefPattern.test(value)
|| secretReferencePattern.test(value)
) {
throw new TypeError("device_binding_target_ref_invalid");
}
assertSafeProjection({ targetRef: value });
return value;
}
function normalizeConfigurationValue(value, depth, path) {
if (depth > 5) throw new TypeError("device_configuration_depth_exceeded");
if (value === null || typeof value === "boolean") return value;
if (typeof value === "number") {
if (!Number.isFinite(value)) {
throw new TypeError(`device_configuration_number_invalid:${path}`);
}
return value;
}
if (typeof value === "string") {
if (
value.length > 1000
|| /\u0000|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)
) {
throw new TypeError(`device_configuration_string_invalid:${path}`);
}
return value;
}
if (Array.isArray(value)) {
if (value.length > 64) {
throw new TypeError(`device_configuration_array_invalid:${path}`);
}
return value.map((item, index) =>
normalizeConfigurationValue(item, depth + 1, `${path}[${index}]`)
);
}
assertPlainObject(value, `device_configuration_object_invalid:${path}`);
const keys = Object.keys(value);
if (keys.length > 64) {
throw new TypeError(`device_configuration_object_invalid:${path}`);
}
const normalized = {};
for (const key of keys.sort()) {
if (!configurationKeyPattern.test(key)) {
throw new TypeError(`device_configuration_key_invalid:${path}.${key}`);
}
normalized[key] = normalizeConfigurationValue(
value[key],
depth + 1,
`${path}.${key}`,
);
}
return normalized;
}
function normalizeEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function normalizePattern(value, pattern, code) {
if (typeof value !== "string" || !pattern.test(value)) {
throw new TypeError(code);
}
return value;
}
function normalizeDisplayText(value, maxLength, code) {
if (typeof value !== "string") throw new TypeError(code);
const normalized = value.trim();
if (
normalized.length < 1
|| normalized.length > maxLength
|| /\u0000|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(normalized)
) {
throw new TypeError(code);
}
return normalized;
}
function normalizeOptionalText(value, maxLength, code) {
if (value == null || value === "") return null;
return normalizeDisplayText(value, maxLength, code);
}
function assertPlainObject(value, code) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(code);
}
}
function assertAllowedKeys(input, allowed) {
const allowedSet = new Set(allowed);
for (const key of Object.keys(input)) {
if (!allowedSet.has(key)) {
throw new TypeError(`device_management_command_field_unexpected:${key}`);
}
}
}
function deepFreeze(value) {
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
Object.freeze(value);
for (const child of Object.values(value)) deepFreeze(child);
return value;
}
@@ -0,0 +1,498 @@
import { randomUUID } from "node:crypto";
import {
isControlResourceManagementCommand,
} from "./control-resource-management.mjs";
import { findProjectWithCapability } from "./lifecycle-repository.mjs";
import { toProjectRef } from "./project-management.mjs";
export async function applyControlResourceManagementCommand(
client,
{ commandKind, actor, command },
) {
if (!isControlResourceManagementCommand(commandKind)) {
throw new TypeError("device_control_resource_command_kind_invalid");
}
if (commandKind === "device_binding.ensure") {
return ensureBinding(client, actor, command);
}
if (commandKind === "device_binding.revoke") {
return revokeBinding(client, actor, command);
}
if (commandKind === "device_configuration_revision.create") {
return createConfigurationRevision(client, actor, command);
}
return setDesiredConfiguration(client, actor, command);
}
export async function authorizeControlResourceManagementReplay(
client,
{ commandKind, actor, command },
) {
if (!isControlResourceManagementCommand(commandKind)) {
throw new TypeError("device_control_resource_command_kind_invalid");
}
const capability = commandKind.startsWith("device_binding.")
? "binding.manage"
: "configuration.manage";
await findProjectWithCapability(
client,
actor,
command.projectId,
capability,
);
if (command.deviceId) {
const current = await client.query(
`select project_id from device_instances where id = $1`,
[command.deviceId],
);
const currentProjectId = current.rows[0]?.project_id;
if (!currentProjectId) throw domainError("device_not_found", 404);
if (currentProjectId !== command.projectId) {
await findProjectWithCapability(
client,
actor,
currentProjectId,
capability,
);
}
}
}
async function ensureBinding(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"binding.manage",
);
const source = await findBindingSource(client, command);
const bindingId = randomUUID();
const result = await client.query(
`insert into device_resource_bindings (
id,
owner_scope_id,
project_id,
binding_key,
display_name,
source_kind,
device_id,
collection_id,
target_kind,
target_ref,
capabilities,
source_approved_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
on conflict (project_id, binding_key) do update set
display_name = excluded.display_name,
capabilities = excluded.capabilities,
updated_at = now()
where device_resource_bindings.lifecycle_state = 'pending_external_approval'
and device_resource_bindings.source_kind = excluded.source_kind
and device_resource_bindings.device_id is not distinct from excluded.device_id
and device_resource_bindings.collection_id is not distinct from excluded.collection_id
and device_resource_bindings.target_kind = excluded.target_kind
and device_resource_bindings.target_ref = excluded.target_ref
returning id, owner_scope_id, project_id, binding_key, display_name,
source_kind, device_id, collection_id, target_kind, target_ref,
capabilities, lifecycle_state, source_approved_at, created_at, updated_at,
(xmax = 0) as created`,
[
bindingId,
project.owner_scope_id,
project.id,
command.bindingKey,
command.displayName,
command.source.kind,
command.source.kind === "device" ? source.id : null,
command.source.kind === "collection" ? source.id : null,
command.targetKind,
command.targetRef,
command.capabilities,
actor.userRef,
],
);
const binding = result.rows[0];
if (!binding) throw domainError("device_binding_identity_conflict", 409);
await addAudit(client, {
eventType: binding.created
? "device_binding.created"
: "device_binding.updated",
actorRef: actor.userRef,
projectId: project.id,
deviceId: binding.device_id,
payload: {
bindingRef: `binding:${binding.id}`,
projectRef: toProjectRef(project.id),
bindingKey: binding.binding_key,
sourceKind: binding.source_kind,
sourceRef: bindingSourceRef(binding),
targetKind: binding.target_kind,
targetRef: binding.target_ref,
lifecycleState: binding.lifecycle_state,
},
});
return {
created: binding.created === true,
binding: bindingView(binding),
};
}
async function revokeBinding(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"binding.manage",
);
const result = await client.query(
`update device_resource_bindings
set lifecycle_state = 'revoked',
revoked_at = now(),
revoked_by_ref = $3,
revocation_code = $4,
updated_at = now()
where id = $1
and project_id = $2
and lifecycle_state <> 'revoked'
returning id, owner_scope_id, project_id, binding_key, display_name,
source_kind, device_id, collection_id, target_kind, target_ref,
capabilities, lifecycle_state, source_approved_at, created_at, updated_at`,
[command.bindingId, project.id, actor.userRef, command.resolutionCode],
);
const binding = result.rows[0];
if (!binding) throw domainError("device_binding_not_found", 404);
await addAudit(client, {
eventType: "device_binding.revoked",
actorRef: actor.userRef,
projectId: project.id,
deviceId: binding.device_id,
payload: {
bindingRef: `binding:${binding.id}`,
projectRef: toProjectRef(project.id),
sourceKind: binding.source_kind,
sourceRef: bindingSourceRef(binding),
targetKind: binding.target_kind,
targetRef: binding.target_ref,
lifecycleState: binding.lifecycle_state,
resolutionCode: command.resolutionCode,
},
});
return {
revoked: true,
binding: bindingView(binding),
resolutionCode: command.resolutionCode,
};
}
async function createConfigurationRevision(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"configuration.manage",
);
const device = await findDirectDeviceForUpdate(client, command);
const profileResult = await client.query(
`select profile_ref, schema_artifact_ref, lifecycle_state
from device_model_profiles
where profile_ref = $1
for share`,
[device.model_profile_ref],
);
const profile = profileResult.rows[0];
if (
!profile
|| profile.lifecycle_state !== "active"
|| !profile.schema_artifact_ref
) {
throw domainError("device_configuration_profile_unavailable", 409);
}
const nextResult = await client.query(
`select coalesce(max(revision_number), 0) + 1 as next_revision
from device_configuration_revisions
where device_id = $1`,
[device.id],
);
const revisionNumber = Number(nextResult.rows[0]?.next_revision);
if (!Number.isSafeInteger(revisionNumber) || revisionNumber < 1) {
throw domainError("device_configuration_revision_sequence_invalid", 409);
}
const revisionId = randomUUID();
const inserted = await client.query(
`insert into device_configuration_revisions (
id,
owner_scope_id,
project_id,
device_id,
revision_number,
model_profile_ref,
schema_artifact_ref,
configuration_digest,
configuration,
change_summary,
created_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11)
returning id, owner_scope_id, project_id, device_id, revision_number,
model_profile_ref, schema_artifact_ref, configuration_digest,
configuration, change_summary, created_at`,
[
revisionId,
project.owner_scope_id,
project.id,
device.id,
revisionNumber,
profile.profile_ref,
profile.schema_artifact_ref,
command.configurationDigest,
JSON.stringify(command.configuration),
command.changeSummary,
actor.userRef,
],
);
const revision = inserted.rows[0];
if (!revision) {
throw domainError("device_configuration_revision_insert_failed", 409);
}
await addAudit(client, {
eventType: "device_configuration_revision.created",
actorRef: actor.userRef,
projectId: project.id,
deviceId: device.id,
payload: {
deviceRef: `device:${device.id}`,
projectRef: toProjectRef(project.id),
configurationRevisionRef: `configuration-revision:${revision.id}`,
revisionNumber: Number(revision.revision_number),
modelProfileRef: revision.model_profile_ref,
schemaArtifactRef: revision.schema_artifact_ref,
configurationDigest: revision.configuration_digest,
},
});
return {
created: true,
configurationRevision: configurationRevisionView(revision),
};
}
async function setDesiredConfiguration(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"configuration.manage",
);
const device = await findDirectDeviceForUpdate(client, command);
const revisionResult = await client.query(
`select id, project_id, device_id, revision_number,
model_profile_ref, schema_artifact_ref, configuration_digest,
configuration, change_summary, created_at
from device_configuration_revisions
where id = $1 and device_id = $2 and project_id = $3
for share`,
[command.configurationRevisionId, device.id, project.id],
);
const revision = revisionResult.rows[0];
if (!revision) throw domainError("device_configuration_revision_not_found", 404);
const currentResult = await client.query(
`select desired_revision_id, applied_revision_id
from device_configuration_state
where device_id = $1
for update`,
[device.id],
);
const current = currentResult.rows[0] ?? null;
if (current?.desired_revision_id === revision.id) {
return {
changed: false,
configurationState: configurationStateView({
device_id: device.id,
project_id: project.id,
desired_revision_id: revision.id,
applied_revision_id: current.applied_revision_id,
}),
};
}
const stateResult = await client.query(
`insert into device_configuration_state (
device_id,
owner_scope_id,
project_id,
desired_revision_id
) values ($1, $2, $3, $4)
on conflict (device_id) do update set
owner_scope_id = excluded.owner_scope_id,
project_id = excluded.project_id,
desired_revision_id = excluded.desired_revision_id,
updated_at = now()
returning device_id, project_id, desired_revision_id, applied_revision_id`,
[device.id, project.owner_scope_id, project.id, revision.id],
);
const state = stateResult.rows[0];
if (!state) throw domainError("device_configuration_state_update_failed", 409);
await addAudit(client, {
eventType: "device_configuration.desired_changed",
actorRef: actor.userRef,
projectId: project.id,
deviceId: device.id,
payload: {
deviceRef: `device:${device.id}`,
projectRef: toProjectRef(project.id),
configurationRevisionRef: `configuration-revision:${revision.id}`,
previousConfigurationRevisionRef: current?.desired_revision_id
? `configuration-revision:${current.desired_revision_id}`
: null,
configurationDigest: revision.configuration_digest,
},
});
return {
changed: true,
configurationState: configurationStateView(state),
};
}
async function findBindingSource(client, command) {
if (command.source.kind === "device") {
return findDirectDeviceForUpdate(client, {
projectId: command.projectId,
deviceId: command.source.id,
});
}
const result = await client.query(
`select id, project_id, lifecycle_state
from device_collections
where id = $1 and project_id = $2
for share`,
[command.source.id, command.projectId],
);
const collection = result.rows[0];
if (!collection) throw domainError("device_collection_not_found", 404);
if (collection.lifecycle_state !== "active") {
throw domainError("device_collection_inactive", 409);
}
return collection;
}
async function findDirectDeviceForUpdate(client, command) {
const result = await client.query(
`select id, contour_id, owner_scope_id, project_id,
model_profile_ref, lifecycle_state
from device_instances
where id = $1
for update`,
[command.deviceId],
);
const device = result.rows[0];
if (!device) throw domainError("device_not_found", 404);
if (
device.contour_id
|| !device.owner_scope_id
|| !device.project_id
|| device.project_id !== command.projectId
) {
throw domainError("device_control_resource_project_mismatch", 409);
}
if (device.lifecycle_state === "retired") {
throw domainError("device_control_resource_lifecycle_blocked", 409);
}
return device;
}
async function addAudit(client, {
eventType,
actorRef,
projectId,
deviceId = null,
payload,
}) {
await client.query(
`insert into device_audit_events (
id,
event_type,
actor_ref,
project_id,
device_id,
payload
) values ($1, $2, $3, $4, $5, $6::jsonb)`,
[
randomUUID(),
eventType,
actorRef,
projectId,
deviceId,
JSON.stringify(payload),
],
);
}
function bindingView(row) {
return {
bindingRef: `binding:${row.id}`,
projectRef: toProjectRef(row.project_id),
bindingKey: row.binding_key,
displayName: row.display_name,
source: {
kind: row.source_kind,
ref: bindingSourceRef(row),
},
target: {
kind: row.target_kind,
ref: row.target_ref,
},
capabilities: row.capabilities ?? [],
lifecycleState: row.lifecycle_state,
sourceApprovedAt: toIso(row.source_approved_at),
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function bindingSourceRef(row) {
return row.source_kind === "device"
? `device:${row.device_id}`
: `collection:${row.collection_id}`;
}
function configurationRevisionView(row) {
return {
configurationRevisionRef: `configuration-revision:${row.id}`,
deviceRef: `device:${row.device_id}`,
projectRef: toProjectRef(row.project_id),
revisionNumber: Number(row.revision_number),
modelProfileRef: row.model_profile_ref,
schemaArtifactRef: row.schema_artifact_ref,
configurationDigest: row.configuration_digest,
configuration: row.configuration,
changeSummary: row.change_summary ?? null,
createdAt: toIso(row.created_at),
};
}
function configurationStateView(row) {
return {
deviceRef: `device:${row.device_id}`,
projectRef: toProjectRef(row.project_id),
desiredConfigurationRevisionRef: row.desired_revision_id
? `configuration-revision:${row.desired_revision_id}`
: null,
appliedConfigurationRevisionRef: row.applied_revision_id
? `configuration-revision:${row.applied_revision_id}`
: null,
};
}
function toIso(value) {
return new Date(value).toISOString();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,29 @@
export const NDC_CREDENTIAL_REFERENCE_OWNER = "ndc_l2_credentials";
const CREDENTIAL_REFERENCE_PATTERN =
/^ndc-credref:[A-Za-z0-9][A-Za-z0-9._:-]{7,240}$/;
export function normalizeNdcCredentialReference(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new TypeError("ndc_credential_reference_invalid");
}
for (const key of Object.keys(input)) {
if (!new Set(["owner", "reference"]).has(key)) {
throw new TypeError(`ndc_credential_reference_field_unexpected:${key}`);
}
}
if (input.owner !== NDC_CREDENTIAL_REFERENCE_OWNER) {
throw new TypeError("ndc_credential_reference_owner_invalid");
}
if (!isNdcCredentialReferenceValue(input.reference)) {
throw new TypeError("ndc_credential_reference_value_invalid");
}
return Object.freeze({
owner: NDC_CREDENTIAL_REFERENCE_OWNER,
reference: input.reference,
});
}
export function isNdcCredentialReferenceValue(value) {
return typeof value === "string" && CREDENTIAL_REFERENCE_PATTERN.test(value);
}
@@ -0,0 +1,73 @@
import { readFile } from "node:fs/promises";
export async function resolveDeviceDatabaseUrl(
environment = process.env,
readSecret = readFile,
) {
const explicit = optionalValue(environment.DEVICE_DATABASE_URL);
if (explicit) return explicit;
const host = restrictedValue(
environment.DEVICE_DATABASE_HOST,
/^[A-Za-z0-9.-]{1,253}$/,
"device_database_host_invalid",
);
const port = parsePort(environment.DEVICE_DATABASE_PORT, 5432);
const database = restrictedValue(
environment.DEVICE_DATABASE_NAME,
/^[A-Za-z_][A-Za-z0-9_-]{0,62}$/,
"device_database_name_invalid",
);
const user = restrictedValue(
environment.DEVICE_DATABASE_USER,
/^[A-Za-z_][A-Za-z0-9_-]{0,62}$/,
"device_database_user_invalid",
);
const passwordFile = requiredValue(
environment.DEVICE_DATABASE_PASSWORD_FILE,
"device_database_password_file_required",
);
const password = (await readSecret(passwordFile, "utf8")).trim();
if (password.length < 32 || password.length > 512) {
throw new Error("device_database_password_invalid");
}
return [
"postgresql://",
encodeURIComponent(user),
":",
encodeURIComponent(password),
"@",
host,
":",
String(port),
"/",
encodeURIComponent(database),
"?sslmode=disable",
].join("");
}
function optionalValue(value) {
if (typeof value !== "string") return "";
return value.trim();
}
function requiredValue(value, errorCode) {
const normalized = optionalValue(value);
if (!normalized) throw new Error(errorCode);
return normalized;
}
function restrictedValue(value, pattern, errorCode) {
const normalized = requiredValue(value, errorCode);
if (!pattern.test(normalized)) throw new Error(errorCode);
return normalized;
}
function parsePort(value, fallback) {
const parsed = Number(value || fallback);
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) {
throw new Error("device_database_port_invalid");
}
return parsed;
}
@@ -0,0 +1,741 @@
import { randomUUID } from "node:crypto";
import { connect as connectHttp2 } from "node:http2";
import {
DEVICE_EDGE_CHANNEL_LIMITS,
DEVICE_EDGE_CHANNEL_PATH,
createChannelEnvelope,
createChannelEnvelopeDecoder,
encodeChannelEnvelope,
nextReconnectDelay,
normalizeCertificateIdentities,
normalizeCertificateFingerprint,
} from "../../../packages/device-edge-channel-contract/src/index.mjs";
import {
DEVICE_DISCOVERY_VIEW_SCHEMA,
assertSafeProjection,
normalizeAdapterAcceptance,
normalizeAdapterMessage,
normalizeDiscoverySignal,
} from "../../../packages/device-protocol-contract/src/index.mjs";
// Runtime-owned transport implementation; kept inside the deployable Core context.
const CHANNEL_TRACKER_SESSION_ID = "channel:control";
const CHANNEL_PROFILE_REF = "channel.control.v1";
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
export function createDeviceGatewayCoreChannelClient(options = {}) {
const config = normalizeConfig(options);
const readyWaiters = new Set();
let running = false;
let state = null;
let reconnectTimer = null;
let reconnectAttempt = 0;
let connectionSerial = 0;
let totalConnectionAttempts = 0;
let totalChannelsAccepted = 0;
let totalReconnects = 0;
let totalEventsAccepted = 0;
let totalEventsRejected = 0;
let totalProtocolFailures = 0;
let lastErrorCode = null;
return Object.freeze({
async start() {
if (running) return;
running = true;
void connectNow();
},
async stop() {
running = false;
clearTimeout(reconnectTimer);
reconnectTimer = null;
const current = state;
state = null;
if (current) closeConnection(current, false);
rejectReadyWaiters("device_gateway_core_channel_stopped");
},
waitForReady(timeoutMs = 5_000) {
if (state?.ready && !state.closed) return Promise.resolve(status());
const normalizedTimeout = normalizeInteger(
timeoutMs,
10,
120_000,
5_000,
"ready_timeout",
);
return new Promise((resolve, reject) => {
const waiter = { resolve, reject, timer: null };
waiter.timer = setTimeout(() => {
readyWaiters.delete(waiter);
reject(new Error("device_gateway_core_channel_ready_timeout"));
}, normalizedTimeout);
waiter.timer.unref?.();
readyWaiters.add(waiter);
});
},
status,
disconnect() {
if (state) closeConnection(state, true);
},
});
function status() {
return Object.freeze({
running,
channel: state?.ready ? "accepted" : state ? "connecting" : "absent",
edgeRegistrationId: state?.registration?.edgeRegistrationId ?? null,
channelGeneration: state?.channelGeneration ?? null,
edgeTrustGeneration: state?.observedEdgeIdentity?.generationRef ?? null,
edgeCertificateFingerprint:
state?.observedEdgeIdentity?.fingerprint ?? null,
negotiatedCommandTransport: state?.negotiatedCommandTransport ?? null,
activeTrackerSessionChains: state?.sessionChains.size ?? 0,
connectionAttempts: totalConnectionAttempts,
channelsAccepted: totalChannelsAccepted,
reconnects: totalReconnects,
eventsAccepted: totalEventsAccepted,
eventsRejected: totalEventsRejected,
protocolFailures: totalProtocolFailures,
lastErrorCode,
trackerIngress: "remote-edge-only",
commandTransport: config.commandTransport,
});
}
async function connectNow() {
if (!running || state) return;
totalConnectionAttempts += 1;
const serial = ++connectionSerial;
let registration;
try {
registration = normalizeRegistration(await config.registrationProvider());
if (registration.lifecycleState !== "active") {
throw new Error("device_gateway_core_edge_registration_inactive");
}
} catch (error) {
lastErrorCode = safeErrorCode(error);
scheduleReconnect();
return;
}
const connection = {
serial,
registration,
session: null,
request: null,
decoder: createChannelEnvelopeDecoder({
direction: "edge-to-core",
maxEnvelopeBytes: config.maxEnvelopeBytes,
}),
sessionChains: new Map(),
trackerDevices: new Map(),
channelGeneration: null,
negotiatedCommandTransport: null,
observedEdgeIdentity: null,
edgeSequence: 0,
coreSequence: 0,
lastEdgeActivityAt: config.clock(),
connectTimer: null,
heartbeatTimer: null,
ready: false,
closed: false,
};
state = connection;
const endpoint = new URL(registration.endpoint);
const authority = `${endpoint.protocol}//${endpoint.host}`;
const session = connectHttp2(authority, {
key: config.tls.key,
cert: config.tls.cert,
ca: config.tls.ca,
minVersion: "TLSv1.3",
maxVersion: "TLSv1.3",
rejectUnauthorized: true,
servername: registration.servername,
ALPNProtocols: ["h2"],
settings: {
enablePush: false,
initialWindowSize: 1024 * 1024,
},
});
connection.session = session;
connection.connectTimer = setTimeout(() => {
failConnection(connection, new Error(
"device_gateway_core_channel_connect_timeout",
));
}, config.connectTimeoutMs);
connection.connectTimer.unref?.();
session.once("error", (error) => failConnection(connection, error));
session.once("close", () => closeConnection(connection, true));
session.once("connect", () => {
try {
verifyEdgePeer(connection);
openChannelStream(connection);
} catch (error) {
failConnection(connection, error);
}
});
}
function openChannelStream(connection) {
assertCurrent(connection);
const request = connection.session.request({
":method": "POST",
":path": DEVICE_EDGE_CHANNEL_PATH,
"content-type": "application/x-ndjson",
"cache-control": "no-store",
}, { endStream: false });
connection.request = request;
request.once("response", (headers) => {
if (Number(headers[":status"]) !== 200) {
failConnection(connection, new Error(
`device_gateway_core_channel_http_status_${headers[":status"]}`,
));
}
});
let processing = Promise.resolve();
request.on("data", (chunk) => {
request.pause();
processing = processing
.then(async () => {
const envelopes = connection.decoder.push(chunk);
for (const envelope of envelopes) {
await handleEdgeEnvelope(connection, envelope);
}
})
.catch((error) => failConnection(connection, error))
.finally(() => {
if (!connection.closed) request.resume();
});
});
request.once("aborted", () => closeConnection(connection, true));
request.once("close", () => closeConnection(connection, true));
request.once("error", (error) => failConnection(connection, error));
connection.heartbeatTimer = setInterval(
() => checkChannelHealth(connection),
config.keepaliveMs,
);
connection.heartbeatTimer.unref?.();
}
async function handleEdgeEnvelope(connection, envelope) {
assertCurrent(connection);
if (
envelope.edgeRegistrationId !== connection.registration.edgeRegistrationId
|| envelope.sequence !== connection.edgeSequence + 1
) {
throw new Error("device_gateway_core_edge_envelope_mismatch");
}
if (
connection.channelGeneration
&& envelope.channelGeneration !== connection.channelGeneration
) {
throw new Error("device_gateway_core_channel_generation_mismatch");
}
connection.edgeSequence = envelope.sequence;
connection.lastEdgeActivityAt = config.clock();
if (!connection.ready) {
if (envelope.messageKind !== "channel.hello") {
throw new Error("device_gateway_core_channel_hello_required");
}
if (
envelope.channelGeneration !== connection.registration.channelGeneration
) {
throw new Error("device_gateway_core_channel_generation_mismatch");
}
if (
envelope.payload?.status !== "ready"
|| envelope.payload?.transport !== "http2-mtls"
|| envelope.payload?.trustGeneration
!== connection.observedEdgeIdentity?.generationRef
|| !isCompatibleCommandTransport(
config.commandTransport,
envelope.payload?.commandTransport,
)
) {
throw new Error("device_gateway_core_channel_hello_invalid");
}
connection.negotiatedCommandTransport = envelope.payload.commandTransport;
connection.channelGeneration = connection.registration.channelGeneration;
send(connection, "channel.accepted", {
status: "accepted",
coreIdentity: config.coreIdentity,
commandTransport: connection.negotiatedCommandTransport,
}, {
trackerSessionId: CHANNEL_TRACKER_SESSION_ID,
adapterProfileRef: CHANNEL_PROFILE_REF,
correlationId: envelope.correlationId,
});
connection.ready = true;
clearTimeout(connection.connectTimer);
connection.connectTimer = null;
reconnectAttempt = 0;
totalChannelsAccepted += 1;
lastErrorCode = null;
resolveReadyWaiters();
return;
}
if (envelope.messageKind === "channel.heartbeat") return;
if (["discovery.observed", "adapter.message", "command.status"].includes(envelope.messageKind)) {
scheduleTrackerEvent(connection, envelope);
return;
}
throw new Error("device_gateway_core_edge_message_unhandled");
}
function scheduleTrackerEvent(connection, envelope) {
if (envelope.trackerSessionId === CHANNEL_TRACKER_SESSION_ID) {
throw new Error("device_gateway_core_tracker_session_invalid");
}
const previous = connection.sessionChains.get(envelope.trackerSessionId);
if (!previous && connection.sessionChains.size >= 128) {
throw new Error("device_gateway_core_tracker_session_limit_reached");
}
const work = (previous ?? Promise.resolve())
.then(() => envelope.messageKind === "discovery.observed"
? acceptDiscovery(connection, envelope)
: envelope.messageKind === "adapter.message"
? acceptAdapterMessage(connection, envelope)
: acceptCommandStatus(connection, envelope))
.catch((error) => failConnection(connection, error))
.finally(() => {
if (connection.sessionChains.get(envelope.trackerSessionId) === work) {
connection.sessionChains.delete(envelope.trackerSessionId);
}
});
connection.sessionChains.set(envelope.trackerSessionId, work);
}
async function acceptDiscovery(connection, envelope) {
try {
const signal = normalizeDiscoverySignal(envelope.payload?.signal);
const receipt = normalizeDiscoveryReceipt(
await config.observeDiscovery(signal),
);
if (receipt.claimedDeviceRef) {
connection.trackerDevices.set(
envelope.trackerSessionId,
receipt.claimedDeviceRef,
);
} else {
connection.trackerDevices.delete(envelope.trackerSessionId);
}
const commandOffer = (
connection.negotiatedCommandTransport === "typed-service-ping-v1"
&& receipt.claimedDeviceRef
)
? await config.offerCommand(receipt.claimedDeviceRef)
: null;
sendEventResult(connection, envelope, {
discovery: receipt.discovery,
...(commandOffer ? { commandOffer } : {}),
});
totalEventsAccepted += 1;
} catch (error) {
sendEventRejection(connection, envelope, error);
totalEventsRejected += 1;
}
}
async function acceptAdapterMessage(connection, envelope) {
try {
const message = normalizeAdapterMessage(envelope.payload?.message, {
maxBytes: config.maxEnvelopeBytes,
});
const receipt = normalizeAdapterReceipt(
await config.acceptMessage(message),
);
if (receipt.claimedDeviceRef) {
connection.trackerDevices.set(
envelope.trackerSessionId,
receipt.claimedDeviceRef,
);
}
const claimedDeviceRef = receipt.claimedDeviceRef
?? connection.trackerDevices.get(envelope.trackerSessionId);
const commandOffer = (
connection.negotiatedCommandTransport === "typed-service-ping-v1"
&& claimedDeviceRef
)
? await config.offerCommand(claimedDeviceRef)
: null;
sendEventResult(connection, envelope, {
acceptance: receipt.acceptance,
...(commandOffer ? { commandOffer } : {}),
});
totalEventsAccepted += 1;
} catch (error) {
sendEventRejection(connection, envelope, error);
totalEventsRejected += 1;
}
}
async function acceptCommandStatus(connection, envelope) {
try {
await config.recordCommandStatus(envelope.payload?.status);
sendEventResult(connection, envelope, { status: "recorded" });
totalEventsAccepted += 1;
} catch (error) {
sendEventRejection(connection, envelope, error);
totalEventsRejected += 1;
}
}
function sendEventResult(connection, envelope, result) {
send(connection, "event.accepted", { result }, {
trackerSessionId: envelope.trackerSessionId,
adapterProfileRef: envelope.adapterProfileRef,
correlationId: envelope.correlationId,
});
}
function sendEventRejection(connection, envelope, error) {
send(connection, "event.rejected", {
errorCode: safeErrorCode(error),
}, {
trackerSessionId: envelope.trackerSessionId,
adapterProfileRef: envelope.adapterProfileRef,
correlationId: envelope.correlationId,
});
}
function send(connection, messageKind, payload, metadata) {
assertCurrent(connection);
if (!connection.channelGeneration) {
throw new Error("device_gateway_core_channel_generation_absent");
}
connection.coreSequence += 1;
const now = config.now();
const envelope = createChannelEnvelope({
edgeRegistrationId: connection.registration.edgeRegistrationId,
channelGeneration: connection.channelGeneration,
trackerSessionId: metadata.trackerSessionId,
adapterProfileRef: metadata.adapterProfileRef,
sequence: connection.coreSequence,
eventAt: metadata.eventAt ?? now,
receivedAt: now,
messageKind,
correlationId: metadata.correlationId,
payload,
}, {
direction: "core-to-edge",
maxEnvelopeBytes: config.maxEnvelopeBytes,
});
connection.request.write(encodeChannelEnvelope(envelope, {
direction: "core-to-edge",
maxEnvelopeBytes: config.maxEnvelopeBytes,
}));
}
function checkChannelHealth(connection) {
if (connection.closed || state !== connection) return;
if (config.clock() - connection.lastEdgeActivityAt >= config.deadPeerMs) {
failConnection(connection, new Error("device_gateway_core_edge_dead_peer"));
return;
}
if (connection.ready) {
try {
send(connection, "channel.heartbeat", { status: "alive" }, {
trackerSessionId: CHANNEL_TRACKER_SESSION_ID,
adapterProfileRef: CHANNEL_PROFILE_REF,
correlationId: `correlation:${randomUUID()}`,
});
} catch (error) {
failConnection(connection, error);
}
}
}
function verifyEdgePeer(connection) {
const socket = connection.session.socket;
if (!socket?.authorized || socket.alpnProtocol !== "h2") {
throw new Error("device_gateway_core_edge_tls_unauthorized");
}
const observed = normalizeCertificateFingerprint(
socket.getPeerCertificate()?.fingerprint256,
);
const identity = connection.registration.certificateIdentities.find(
(candidate) => candidate.fingerprint === observed,
);
if (!identity) {
throw new Error("device_gateway_core_edge_identity_mismatch");
}
connection.observedEdgeIdentity = identity;
}
function failConnection(connection, error) {
if (connection.closed) return;
totalProtocolFailures += 1;
lastErrorCode = safeErrorCode(error);
closeConnection(connection, true);
}
function closeConnection(connection, reconnect) {
if (connection.closed) return;
connection.closed = true;
clearTimeout(connection.connectTimer);
connection.connectTimer = null;
clearInterval(connection.heartbeatTimer);
connection.heartbeatTimer = null;
connection.sessionChains.clear();
try {
connection.request?.close();
} catch {}
try {
connection.session?.close();
} catch {}
if (state === connection) state = null;
if (reconnect && running) scheduleReconnect();
}
function scheduleReconnect() {
if (!running || reconnectTimer || state) return;
const delay = nextReconnectDelay(reconnectAttempt, {
minimumMs: config.reconnectMinimumMs,
maximumMs: config.reconnectMaximumMs,
random: config.random,
});
reconnectAttempt += 1;
totalReconnects += 1;
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
void connectNow();
}, delay);
reconnectTimer.unref?.();
}
function assertCurrent(connection) {
if (!running || connection.closed || state !== connection) {
throw new Error("device_gateway_core_channel_unavailable");
}
}
function resolveReadyWaiters() {
const value = status();
for (const waiter of readyWaiters) {
clearTimeout(waiter.timer);
waiter.resolve(value);
}
readyWaiters.clear();
}
function rejectReadyWaiters(code) {
for (const waiter of readyWaiters) {
clearTimeout(waiter.timer);
waiter.reject(new Error(code));
}
readyWaiters.clear();
}
}
function normalizeDiscoveryReceipt(input) {
if (
!input
|| typeof input !== "object"
|| Array.isArray(input)
|| typeof input.created !== "boolean"
) {
throw new TypeError("device_gateway_core_discovery_receipt_invalid");
}
const discovery = assertSafeProjection(input.value);
if (
discovery.schemaVersion !== DEVICE_DISCOVERY_VIEW_SCHEMA
|| !["quarantine", "claimed"].includes(discovery.lifecycleState)
|| discovery.commandTransport !== "disabled"
) {
throw new TypeError("device_gateway_core_discovery_receipt_invalid");
}
return Object.freeze({
discovery,
claimedDeviceRef: input.claimedDeviceRef ?? null,
});
}
function normalizeAdapterReceipt(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new TypeError("device_gateway_core_adapter_receipt_invalid");
}
const acceptanceValue = input.value ?? (
input.schemaVersion === "nodedc.device-adapter-acceptance.v1"
? input
: null
);
return Object.freeze({
acceptance: normalizeAdapterAcceptance(acceptanceValue),
claimedDeviceRef: input.claimedDeviceRef ?? null,
});
}
function normalizeConfig(options) {
if (typeof options.observeDiscovery !== "function") {
throw new TypeError("device_gateway_core_observe_discovery_invalid");
}
if (typeof options.acceptMessage !== "function") {
throw new TypeError("device_gateway_core_accept_message_invalid");
}
const commandTransport = options.commandTransport ?? "disabled";
if (!["disabled", "typed-service-ping-v1"].includes(commandTransport)) {
throw new TypeError("device_gateway_core_command_transport_invalid");
}
const offerCommand = options.offerCommand ?? (async () => null);
const recordCommandStatus = options.recordCommandStatus ?? (async () => undefined);
if (typeof offerCommand !== "function" || typeof recordCommandStatus !== "function") {
throw new TypeError("device_gateway_core_command_runtime_invalid");
}
const registrationProvider = typeof options.registrationProvider === "function"
? options.registrationProvider
: async () => options.registration;
const tls = normalizeTls(options.tls);
const keepaliveMs = normalizeInteger(
options.keepaliveMs,
10,
120_000,
DEVICE_EDGE_CHANNEL_LIMITS.keepaliveMs,
"keepalive",
);
const deadPeerMs = normalizeInteger(
options.deadPeerMs,
keepaliveMs * 2,
120_000,
DEVICE_EDGE_CHANNEL_LIMITS.deadPeerMs,
"dead_peer",
);
const reconnectMinimumMs = normalizeInteger(
options.reconnectMinimumMs,
10,
120_000,
DEVICE_EDGE_CHANNEL_LIMITS.reconnectMinimumMs,
"reconnect_minimum",
);
const reconnectMaximumMs = normalizeInteger(
options.reconnectMaximumMs,
10,
120_000,
DEVICE_EDGE_CHANNEL_LIMITS.reconnectMaximumMs,
"reconnect_maximum",
);
if (reconnectMaximumMs < reconnectMinimumMs) {
throw new TypeError("device_gateway_core_reconnect_range_invalid");
}
return Object.freeze({
registrationProvider,
tls,
coreIdentity: normalizeRef(options.coreIdentity, "core_identity"),
observeDiscovery: options.observeDiscovery,
acceptMessage: options.acceptMessage,
commandTransport,
offerCommand,
recordCommandStatus,
keepaliveMs,
deadPeerMs,
connectTimeoutMs: normalizeInteger(
options.connectTimeoutMs,
10,
120_000,
DEFAULT_CONNECT_TIMEOUT_MS,
"connect_timeout",
),
reconnectMinimumMs,
reconnectMaximumMs,
maxEnvelopeBytes: normalizeInteger(
options.maxEnvelopeBytes,
256,
DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes,
DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes,
"max_envelope_bytes",
),
random: typeof options.random === "function" ? options.random : Math.random,
clock: typeof options.clock === "function" ? options.clock : Date.now,
now: typeof options.now === "function"
? () => new Date(options.now()).toISOString()
: () => new Date().toISOString(),
});
}
function normalizeRegistration(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_gateway_core_edge_registration_invalid");
}
let endpoint;
try {
endpoint = new URL(String(value.endpoint || ""));
} catch {
throw new TypeError("device_gateway_core_edge_endpoint_invalid");
}
if (
endpoint.protocol !== "https:"
|| endpoint.username
|| endpoint.password
|| endpoint.pathname !== "/"
|| endpoint.search
|| endpoint.hash
) {
throw new TypeError("device_gateway_core_edge_endpoint_invalid");
}
if (!["active", "revoked", "disabled"].includes(value.lifecycleState)) {
throw new TypeError("device_gateway_core_edge_lifecycle_invalid");
}
const servername = String(value.servername || "");
if (!/^[A-Za-z0-9.-]{1,253}$/.test(servername)) {
throw new TypeError("device_gateway_core_edge_servername_invalid");
}
return Object.freeze({
edgeRegistrationId: normalizeRef(
value.edgeRegistrationId,
"edge_registration_id",
),
channelGeneration: normalizeRef(
value.channelGeneration,
"channel_generation",
),
endpoint: endpoint.toString(),
servername,
certificateIdentities: normalizeCertificateIdentities(
value.certificateIdentities,
),
lifecycleState: value.lifecycleState,
});
}
function normalizeTls(value) {
if (!value || typeof value !== "object") {
throw new TypeError("device_gateway_core_channel_tls_invalid");
}
for (const key of ["key", "cert", "ca"]) {
if (!(typeof value[key] === "string" || Buffer.isBuffer(value[key]))) {
throw new TypeError(`device_gateway_core_channel_tls_${key}_invalid`);
}
}
return Object.freeze({ key: value.key, cert: value.cert, ca: value.ca });
}
function safeErrorCode(error) {
const value = String(error?.message || error || "device_gateway_core_error")
.toLowerCase()
.replaceAll(/[^a-z0-9._:-]/g, "_")
.slice(0, 128);
return /^[a-z][a-z0-9._:-]{2,127}$/.test(value)
? value
: "device_gateway_core_event_rejected";
}
function isCompatibleCommandTransport(configured, offered) {
if (offered === configured) return true;
return configured === "typed-service-ping-v1" && offered === "disabled";
}
function normalizeRef(value, field) {
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
throw new TypeError(`device_gateway_core_${field}_invalid`);
}
return value;
}
function normalizeInteger(value, minimum, maximum, fallback, field) {
const number = value == null ? fallback : Number(value);
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
throw new TypeError(`device_gateway_core_${field}_invalid`);
}
return number;
}
@@ -0,0 +1,307 @@
import { randomUUID } from "node:crypto";
export async function observeQuarantineDiscovery({
pool,
identifierDigest,
safeView,
sessionRef,
routeRef = null,
}) {
if ((safeView.routeRef ?? null) !== routeRef) {
throw new TypeError("device_discovery_route_ref_mismatch");
}
const routeId = routeRef == null ? null : parseEntityRef(routeRef, "route");
const client = await pool.connect();
try {
await client.query("begin");
const route = routeId == null
? null
: await findActiveRoute(client, routeId, safeView);
const enrollment = route == null
? null
: await findMatchingEnrollment(client, {
route,
identifierDigest,
safeView,
});
const result = await client.query(
`insert into device_discoveries (
id,
identifier_kind,
identifier_digest,
identifier_masked,
model_profile_ref,
protocol,
lifecycle_state,
first_observed_at,
last_observed_at,
evidence,
session_ref,
project_id,
route_id,
enrollment_intent_id
) values (
$1, $2, $3, $4, $5, $6, 'quarantine', $7, $7, $8::jsonb,
$9, $10, $11, $12
)
on conflict (identifier_kind, identifier_digest, model_profile_ref)
do update set
last_observed_at = case
when device_discoveries.lifecycle_state = 'claimed'
and excluded.route_id is distinct from device_discoveries.route_id
then device_discoveries.last_observed_at
else greatest(
device_discoveries.last_observed_at,
excluded.last_observed_at
)
end,
evidence = case
when device_discoveries.lifecycle_state = 'claimed'
and excluded.route_id is distinct from device_discoveries.route_id
then device_discoveries.evidence
else excluded.evidence
end,
session_ref = case
when device_discoveries.lifecycle_state = 'claimed'
and excluded.route_id is distinct from device_discoveries.route_id
then device_discoveries.session_ref
else excluded.session_ref
end,
project_id = case
when device_discoveries.lifecycle_state = 'claimed'
then device_discoveries.project_id
when excluded.enrollment_intent_id is not null
and (
device_discoveries.enrollment_intent_id is null
or device_discoveries.enrollment_intent_id = excluded.enrollment_intent_id
or device_discoveries.lifecycle_state in ('rejected', 'expired')
) then excluded.project_id
when device_discoveries.project_id is null
then excluded.project_id
else device_discoveries.project_id
end,
route_id = case
when device_discoveries.lifecycle_state = 'claimed'
then device_discoveries.route_id
when excluded.enrollment_intent_id is not null
and (
device_discoveries.enrollment_intent_id is null
or device_discoveries.enrollment_intent_id = excluded.enrollment_intent_id
or device_discoveries.lifecycle_state in ('rejected', 'expired')
) then excluded.route_id
when device_discoveries.route_id is null
then excluded.route_id
else device_discoveries.route_id
end,
enrollment_intent_id = case
when device_discoveries.lifecycle_state = 'claimed'
then device_discoveries.enrollment_intent_id
when excluded.enrollment_intent_id is not null
and (
device_discoveries.enrollment_intent_id is null
or device_discoveries.enrollment_intent_id = excluded.enrollment_intent_id
or device_discoveries.lifecycle_state in ('rejected', 'expired')
) then excluded.enrollment_intent_id
else device_discoveries.enrollment_intent_id
end,
lifecycle_state = case
when device_discoveries.lifecycle_state = 'claimed' then 'claimed'
when excluded.enrollment_intent_id is not null
and device_discoveries.lifecycle_state in ('rejected', 'expired')
then 'quarantine'
else device_discoveries.lifecycle_state
end,
resolution_code = case
when excluded.enrollment_intent_id is not null
and device_discoveries.lifecycle_state in ('rejected', 'expired')
then null
else device_discoveries.resolution_code
end,
resolved_at = case
when excluded.enrollment_intent_id is not null
and device_discoveries.lifecycle_state in ('rejected', 'expired')
then null
else device_discoveries.resolved_at
end,
resolved_by_ref = case
when excluded.enrollment_intent_id is not null
and device_discoveries.lifecycle_state in ('rejected', 'expired')
then null
else device_discoveries.resolved_by_ref
end,
updated_at = now()
returning id, lifecycle_state, model_profile_ref, protocol,
identifier_kind, identifier_masked, first_observed_at,
last_observed_at, evidence, project_id, route_id,
enrollment_intent_id, claimed_device_id, (xmax = 0) as created`,
[
randomUUID(),
safeView.identifier.kind,
identifierDigest,
safeView.identifier.masked,
safeView.modelProfileRef,
safeView.protocol,
safeView.observedAt,
JSON.stringify(safeView.evidence),
sessionRef,
route?.project_id ?? null,
route?.id ?? null,
enrollment?.id ?? null,
],
);
const row = result.rows[0];
if (
enrollment
&& row.enrollment_intent_id !== enrollment.id
) {
throw domainError("device_discovery_enrollment_conflict", 409);
}
if (enrollment && row.lifecycle_state === "quarantine") {
const observed = await client.query(
`update device_enrollment_intents
set lifecycle_state = 'observed',
observed_discovery_id = $2,
observed_at = greatest(coalesce(observed_at, $3), $3),
resolution_code = null,
resolved_at = null,
resolved_by_ref = null,
updated_at = now()
where id = $1
and lifecycle_state in ('pending', 'observed')
returning id`,
[enrollment.id, row.id, safeView.observedAt],
);
if (!observed.rows[0]) {
throw domainError("device_enrollment_not_observable", 409);
}
}
await client.query("commit");
return {
created: row.created === true,
value: discoveryView(row),
claimedDeviceRef: row.claimed_device_id
? `device:${row.claimed_device_id}`
: null,
};
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw error;
} finally {
client.release();
}
}
async function findActiveRoute(client, routeId, safeView) {
const result = await client.query(
`select id, project_id, model_profile_ref, protocol, lifecycle_state
from device_routes
where id = $1
for share`,
[routeId],
);
const route = result.rows[0];
if (!route) throw domainError("device_discovery_route_not_found", 404);
if (route.lifecycle_state !== "active") {
throw domainError("device_discovery_route_inactive", 409);
}
if (
route.model_profile_ref !== safeView.modelProfileRef
|| route.protocol !== safeView.protocol
) {
throw domainError("device_discovery_route_profile_mismatch", 409);
}
return route;
}
async function findMatchingEnrollment(client, {
route,
identifierDigest,
safeView,
}) {
await client.query(
`update device_enrollment_intents
set lifecycle_state = 'expired',
resolution_code = 'deadline_elapsed',
resolved_at = $6,
updated_at = now()
where project_id = $1
and route_id = $2
and model_profile_ref = $3
and expected_identifier_kind = $4
and expected_identifier_digest = $5
and lifecycle_state = 'pending'
and expires_at is not null
and expires_at <= $6`,
[
route.project_id,
route.id,
safeView.modelProfileRef,
safeView.identifier.kind,
identifierDigest,
safeView.observedAt,
],
);
const result = await client.query(
`select id, project_id, route_id, model_profile_ref, lifecycle_state
from device_enrollment_intents
where project_id = $1
and route_id = $2
and model_profile_ref = $3
and expected_identifier_kind = $4
and expected_identifier_digest = $5
and lifecycle_state in ('pending', 'observed')
and (expires_at is null or expires_at > $6)
for update`,
[
route.project_id,
route.id,
safeView.modelProfileRef,
safeView.identifier.kind,
identifierDigest,
safeView.observedAt,
],
);
if (result.rows.length > 1) {
throw domainError("device_enrollment_identity_ambiguous", 409);
}
return result.rows[0] ?? null;
}
function discoveryView(row) {
return {
schemaVersion: "nodedc.device.discovery-view.v1",
discoveryRef: `discovery:${row.id}`,
...(row.route_id ? { routeRef: `route:${row.route_id}` } : {}),
...(row.enrollment_intent_id
? { enrollmentIntentRef: `enrollment-intent:${row.enrollment_intent_id}` }
: {}),
modelProfileRef: row.model_profile_ref,
protocol: row.protocol,
observedAt: new Date(row.last_observed_at).toISOString(),
lifecycleState: row.lifecycle_state,
identifier: {
kind: row.identifier_kind,
masked: row.identifier_masked,
},
evidence: row.evidence,
commandTransport: "disabled",
};
}
function parseEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,376 @@
import { createHash, X509Certificate } from "node:crypto";
import { lstat, readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import {
normalizeCertificateFingerprint,
normalizeCertificateIdentities,
} from "../../../packages/device-edge-channel-contract/src/index.mjs";
import {
createDeviceGatewayCoreChannelClient,
} from "./device-gateway-core-runtime.mjs";
const DEFAULT_TRUST_ROOT = "/run/nodedc-secrets/device-edge-channel/peers";
export function createDeviceEdgeChannelSupervisor(options = {}) {
const config = normalizeConfiguration(options);
const clients = new Map();
const failures = new Map();
let running = false;
let timer = null;
let reconcilePromise = null;
let requestedCount = 0;
let reconciliationFailures = 0;
let lastErrorCode = null;
return Object.freeze({
async start() {
if (running) return;
running = true;
await reconcile();
schedule();
},
async stop() {
running = false;
clearTimeout(timer);
timer = null;
if (reconcilePromise) await reconcilePromise.catch(() => undefined);
const stopping = [...clients.values()].map(({ client }) => client.stop());
clients.clear();
failures.clear();
await Promise.allSettled(stopping);
},
reconcile,
status,
});
async function reconcile() {
if (!running) return status();
if (reconcilePromise) return reconcilePromise;
reconcilePromise = performReconcile().finally(() => {
reconcilePromise = null;
});
return reconcilePromise;
}
async function performReconcile() {
let registrations;
try {
registrations = await config.repository
.listActiveEdgeChannelRegistrations(config.maxEdges);
if (!Array.isArray(registrations) || registrations.length > config.maxEdges) {
throw new TypeError("device_edge_channel_registration_set_invalid");
}
registrations = registrations.map(normalizeRegistration);
if (new Set(registrations.map((item) => item.edgeRegistrationId)).size
!== registrations.length) {
throw new TypeError("device_edge_channel_registration_set_invalid");
}
requestedCount = registrations.length;
} catch (error) {
reconciliationFailures += 1;
lastErrorCode = safeErrorCode(error);
return status();
}
const desiredIds = new Set(registrations.map((item) => item.edgeRegistrationId));
for (const [edgeRegistrationId, active] of clients) {
if (!desiredIds.has(edgeRegistrationId)) {
clients.delete(edgeRegistrationId);
await active.client.stop().catch(() => undefined);
}
}
for (const edgeRegistrationId of failures.keys()) {
if (!desiredIds.has(edgeRegistrationId)) failures.delete(edgeRegistrationId);
}
for (const registration of registrations) {
const digest = registrationDigest(registration);
const current = clients.get(registration.edgeRegistrationId);
if (current?.digest === digest) {
failures.delete(registration.edgeRegistrationId);
continue;
}
if (current) {
clients.delete(registration.edgeRegistrationId);
await current.client.stop().catch(() => undefined);
}
try {
const ca = await config.readPeerTrust({
registration,
trustRoot: config.trustRoot,
});
const client = config.clientFactory({
registration,
tls: {
key: config.coreIdentity.key,
cert: config.coreIdentity.cert,
ca,
},
coreIdentity: config.coreIdentity.identityRef,
observeDiscovery: (signal) => config.gatewayIngest.observeDiscovery(
signal,
{ authenticatedEdgeRef: registration.edgeRegistrationId },
),
acceptMessage: (message) => config.gatewayIngest.acceptMessage(
message,
{ authenticatedEdgeRef: registration.edgeRegistrationId },
),
commandTransport: config.typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
offerCommand: config.typedCommandRuntime?.offerForDevice,
recordCommandStatus: config.typedCommandRuntime?.recordStatus,
});
assertClient(client);
clients.set(registration.edgeRegistrationId, { client, digest });
failures.delete(registration.edgeRegistrationId);
await client.start();
} catch (error) {
const code = safeErrorCode(error);
failures.set(registration.edgeRegistrationId, code);
lastErrorCode = code;
}
}
return status();
}
function schedule() {
if (!running) return;
timer = setTimeout(async () => {
timer = null;
await reconcile().catch(() => undefined);
schedule();
}, config.reconcileIntervalMs);
timer.unref?.();
}
function status() {
let accepted = 0;
let connecting = 0;
let degraded = failures.size;
const edges = [];
for (const [edgeRegistrationId, { client }] of clients) {
const clientStatus = client.status();
if (clientStatus.channel === "accepted") accepted += 1;
else connecting += 1;
if (clientStatus.lastErrorCode) degraded += 1;
edges.push(Object.freeze({
edgeRegistrationId,
channel: clientStatus.channel,
lastErrorCode: clientStatus.lastErrorCode ?? null,
}));
}
for (const [edgeRegistrationId, code] of failures) {
edges.push(Object.freeze({
edgeRegistrationId,
channel: "absent",
lastErrorCode: code,
}));
}
edges.sort((left, right) =>
left.edgeRegistrationId.localeCompare(right.edgeRegistrationId)
);
return Object.freeze({
enabled: true,
running,
configured: requestedCount,
accepted,
connecting,
degraded,
reconciliationFailures,
lastErrorCode,
commandTransport: config.typedCommandRuntime
? "typed-service-ping-v1"
: "disabled",
edges: Object.freeze(edges),
});
}
}
export async function readPinnedEdgeTrust({ registration, trustRoot }) {
const match = registration.trustBundleRef.match(
/^edge-trust:([a-z][a-z0-9-]{1,62})$/,
);
if (!match) throw new TypeError("device_edge_channel_trust_bundle_ref_invalid");
const root = resolve(trustRoot);
const path = resolve(root, `${match[1]}.pem`);
if (dirname(path) !== root) {
throw new TypeError("device_edge_channel_trust_bundle_path_invalid");
}
const state = await lstat(path);
if (state.isSymbolicLink() || !state.isFile() || state.size < 1 || state.size > 64 * 1024) {
throw new Error("device_edge_channel_trust_bundle_file_invalid");
}
const pem = await readFile(path);
const blocks = pem.toString("utf8").match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g,
);
if (!blocks || blocks.length < 1 || blocks.length > 2) {
throw new Error("device_edge_channel_trust_bundle_invalid");
}
const expected = new Set(
registration.certificateIdentities.map((item) => item.fingerprint),
);
const observed = new Set(blocks.map((block) => normalizeCertificateFingerprint(
new X509Certificate(block).fingerprint256,
)));
if (
observed.size !== expected.size
|| [...observed].some((fingerprint) => !expected.has(fingerprint))
) {
throw new Error("device_edge_channel_trust_bundle_identity_mismatch");
}
return pem;
}
function normalizeConfiguration(options) {
if (
!options.repository
|| typeof options.repository.listActiveEdgeChannelRegistrations !== "function"
) {
throw new TypeError("device_edge_channel_repository_required");
}
if (
!options.gatewayIngest
|| typeof options.gatewayIngest.observeDiscovery !== "function"
|| typeof options.gatewayIngest.acceptMessage !== "function"
) {
throw new TypeError("device_edge_channel_gateway_ingest_required");
}
const coreIdentity = options.coreIdentity;
if (
!coreIdentity
|| !(typeof coreIdentity.key === "string" || Buffer.isBuffer(coreIdentity.key))
|| !(typeof coreIdentity.cert === "string" || Buffer.isBuffer(coreIdentity.cert))
|| !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(coreIdentity.identityRef)
) {
throw new TypeError("device_edge_channel_core_identity_invalid");
}
const maxEdges = normalizeInteger(options.maxEdges, 1, 64, 32);
const reconcileIntervalMs = normalizeInteger(
options.reconcileIntervalMs,
1_000,
300_000,
15_000,
);
const trustRoot = resolve(options.trustRoot ?? DEFAULT_TRUST_ROOT);
return Object.freeze({
repository: options.repository,
gatewayIngest: options.gatewayIngest,
typedCommandRuntime: normalizeTypedCommandRuntime(options.typedCommandRuntime),
coreIdentity: Object.freeze({ ...coreIdentity }),
maxEdges,
reconcileIntervalMs,
trustRoot,
readPeerTrust: options.readPeerTrust ?? readPinnedEdgeTrust,
clientFactory: options.clientFactory ?? createDeviceGatewayCoreChannelClient,
});
}
function normalizeTypedCommandRuntime(value) {
if (value == null) return null;
if (
typeof value.offerForDevice !== "function"
|| typeof value.recordStatus !== "function"
) {
throw new TypeError("device_edge_channel_typed_command_runtime_invalid");
}
return value;
}
function normalizeRegistration(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_edge_channel_registration_invalid");
}
const endpoint = new URL(String(value.endpoint || ""));
if (
endpoint.protocol !== "https:"
|| endpoint.username
|| endpoint.password
|| endpoint.pathname !== "/"
|| endpoint.search
|| endpoint.hash
|| endpoint.port !== ""
|| endpoint.hostname !== String(value.servername || "").toLowerCase()
|| !isPublicIpv4(endpoint.hostname)
) {
throw new TypeError("device_edge_channel_registration_endpoint_invalid");
}
if (value.lifecycleState !== "active") {
throw new TypeError("device_edge_channel_registration_inactive");
}
return Object.freeze({
edgeRegistrationId: normalizeRef(value.edgeRegistrationId),
endpoint: endpoint.toString(),
servername: endpoint.hostname,
channelGeneration: normalizeRef(value.channelGeneration),
trustBundleRef: normalizeTrustRef(value.trustBundleRef),
certificateIdentities: normalizeCertificateIdentities(
value.certificateIdentities,
),
lifecycleState: "active",
});
}
function normalizeRef(value) {
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
throw new TypeError("device_edge_channel_registration_ref_invalid");
}
return value;
}
function normalizeTrustRef(value) {
if (typeof value !== "string" || !/^edge-trust:[a-z][a-z0-9-]{1,62}$/.test(value)) {
throw new TypeError("device_edge_channel_trust_bundle_ref_invalid");
}
return value;
}
function isPublicIpv4(value) {
const octets = value.split(".").map(Number);
if (octets.length !== 4 || octets.some((item) =>
!Number.isInteger(item) || item < 0 || item > 255
)) return false;
const [a, b, c] = octets;
return a >= 1 && a < 224
&& a !== 10 && a !== 127
&& !(a === 100 && b >= 64 && b <= 127)
&& !(a === 169 && b === 254)
&& !(a === 172 && b >= 16 && b <= 31)
&& !(a === 192 && (b === 0 || b === 168))
&& !(a === 192 && b === 88 && c === 99)
&& !(a === 198 && (b === 18 || b === 19 || b === 51))
&& !(a === 203 && b === 0 && c === 113);
}
function registrationDigest(value) {
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
}
function assertClient(value) {
if (
!value
|| typeof value.start !== "function"
|| typeof value.stop !== "function"
|| typeof value.status !== "function"
) throw new TypeError("device_edge_channel_client_invalid");
}
function normalizeInteger(value, minimum, maximum, fallback) {
const parsed = Number(value ?? fallback);
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
throw new TypeError("device_edge_channel_integer_invalid");
}
return parsed;
}
function safeErrorCode(error) {
const value = String(error?.message || error || "device_edge_channel_error")
.toLowerCase()
.replaceAll(/[^a-z0-9._:-]/g, "_")
.slice(0, 128);
return /^[a-z][a-z0-9._:-]{2,127}$/.test(value)
? value
: "device_edge_channel_error";
}
@@ -0,0 +1,125 @@
import { createHash } from "node:crypto";
import {
assertSafeProjection,
hashRestrictedIdentifier,
normalizeAdapterAcceptance,
normalizeAdapterMessage,
normalizeDiscoverySignal,
toSafeAdapterMessageView,
toSafeDiscoveryView,
} from "../../../packages/device-protocol-contract/src/index.mjs";
export function createDeviceGatewayIngest({ repository, identifierPepper } = {}) {
if (!repository || typeof repository.upsertQuarantineDiscovery !== "function") {
throw new TypeError("device_discovery_repository_required");
}
if (typeof repository.acceptAdapterMessage !== "function") {
throw new TypeError("device_gateway_message_repository_required");
}
if (typeof identifierPepper !== "string" || identifierPepper.length < 32) {
throw new TypeError("device_identifier_pepper_invalid");
}
return Object.freeze({
async observeDiscovery(input, context = {}) {
const receivedSignal = normalizeDiscoverySignal(input);
const identifierDigest = hashRestrictedIdentifier(
receivedSignal.identifier,
identifierPepper,
);
const routeRef = await resolveAuthenticatedRoute(repository, {
edgeRef: context.authenticatedEdgeRef,
modelProfileRef: receivedSignal.modelProfileRef,
protocol: receivedSignal.protocol,
identifierKind: receivedSignal.identifier.kind,
identifierDigest,
observedAt: receivedSignal.observedAt,
});
const signal = routeRef === undefined
? receivedSignal
: normalizeDiscoverySignal({
...withoutKeys(receivedSignal, ["routeRef"]),
...(routeRef ? { routeRef } : {}),
});
const safeView = assertSafeProjection(toSafeDiscoveryView(signal));
const discovery = await repository.upsertQuarantineDiscovery({
identifierDigest,
safeView,
sessionRef: signal.sessionRef,
routeRef: signal.routeRef ?? null,
});
return Object.freeze({
created: discovery.created === true,
value: assertSafeProjection(discovery.value),
claimedDeviceRef: discovery.claimedDeviceRef ?? null,
});
},
async acceptMessage(input, context = {}) {
const receivedMessage = normalizeAdapterMessage(input);
const identifierDigest = hashRestrictedIdentifier(
receivedMessage.identifier,
identifierPepper,
);
const routeRef = await resolveAuthenticatedRoute(repository, {
edgeRef: context.authenticatedEdgeRef,
modelProfileRef: receivedMessage.protocolProfileRef,
protocol: receivedMessage.protocol,
identifierKind: receivedMessage.identifier.kind,
identifierDigest,
observedAt: receivedMessage.observedAt,
});
const message = routeRef === undefined
? receivedMessage
: normalizeAdapterMessage({
...withoutKeys(receivedMessage, ["edgeRef", "routeRef"]),
edgeRef: context.authenticatedEdgeRef,
...(routeRef ? { routeRef } : {}),
});
const safeView = assertSafeProjection(toSafeAdapterMessageView(message));
const requestDigest = gatewayMessageRequestDigest({
edgeRef: safeView.edgeRef,
adapterRef: safeView.adapterRef,
protocolProfileRef: safeView.protocolProfileRef,
protocol: safeView.protocol,
routeRef: safeView.routeRef ?? null,
idempotencyKey: safeView.idempotencyKey,
identifierKind: safeView.identifier.kind,
identifierDigest,
payloadSchemaRef: safeView.payloadSchemaRef,
payload: safeView.payload,
});
const receipt = await repository.acceptAdapterMessage({
identifierDigest,
requestDigest,
safeView,
});
return Object.freeze({
value: normalizeAdapterAcceptance(receipt.acceptance),
claimedDeviceRef: receipt.claimedDeviceRef ?? null,
});
},
});
}
async function resolveAuthenticatedRoute(repository, input) {
if (input.edgeRef == null) return undefined;
if (typeof repository.resolveInboundRoute !== "function") {
throw new TypeError("device_inbound_route_repository_required");
}
return repository.resolveInboundRoute(input);
}
function withoutKeys(value, keys) {
const omitted = new Set(keys);
return Object.fromEntries(
Object.entries(value).filter(([key]) => !omitted.has(key)),
);
}
function gatewayMessageRequestDigest(value) {
return `sha256:${createHash("sha256")
.update(JSON.stringify(value), "utf8")
.digest("hex")}`;
}
@@ -0,0 +1,203 @@
import { randomUUID } from "node:crypto";
export async function acceptGatewayMessage({
pool,
identifierDigest,
requestDigest,
safeView,
}) {
const routeId = safeView.routeRef == null
? null
: parseEntityRef(safeView.routeRef, "route");
const client = await pool.connect();
try {
await client.query("begin");
const route = routeId == null
? null
: await findActiveRoute(client, routeId, safeView);
const claimedDeviceRef = route == null
? null
: await findClaimedDeviceRef(client, {
identifierDigest,
route,
safeView,
});
const id = randomUUID();
const inserted = await client.query(
`insert into device_gateway_message_receipts (
id,
idempotency_key,
request_digest,
edge_ref,
adapter_ref,
protocol_profile_ref,
protocol,
route_id,
project_id,
session_ref,
message_ref,
message_type,
sequence,
identifier_kind,
identifier_digest,
identifier_masked,
payload_schema_ref,
payload,
observed_at
) values (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18::jsonb, $19
)
on conflict (idempotency_key) do nothing
returning id, idempotency_key, accepted_at`,
[
id,
safeView.idempotencyKey,
requestDigest,
safeView.edgeRef,
safeView.adapterRef,
safeView.protocolProfileRef,
safeView.protocol,
route?.id ?? null,
route?.project_id ?? null,
safeView.sessionRef,
safeView.messageRef,
safeView.messageType,
safeView.sequence,
safeView.identifier.kind,
identifierDigest,
safeView.identifier.masked,
safeView.payloadSchemaRef,
JSON.stringify(safeView.payload),
safeView.observedAt,
],
);
if (inserted.rows[0]) {
await client.query("commit");
return receiptView(inserted.rows[0], false, claimedDeviceRef);
}
const existing = await client.query(
`select id, idempotency_key, request_digest, accepted_at
from device_gateway_message_receipts
where idempotency_key = $1
for share`,
[safeView.idempotencyKey],
);
const row = existing.rows[0];
if (!row) throw domainError("device_gateway_receipt_missing", 409);
if (row.request_digest !== requestDigest) {
throw domainError("device_gateway_idempotency_conflict", 409);
}
await client.query("commit");
return receiptView(row, true, claimedDeviceRef);
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw error;
} finally {
client.release();
}
}
async function findClaimedDeviceRef(client, {
identifierDigest,
route,
safeView,
}) {
const result = await client.query(
`select claimed_device_id
from device_discoveries
where identifier_kind = $1
and identifier_digest = $2
and model_profile_ref = $3
and lifecycle_state = 'claimed'
and project_id = $4
and route_id = $5
and claimed_device_id is not null
for share`,
[
safeView.identifier.kind,
identifierDigest,
safeView.protocolProfileRef,
route.project_id,
route.id,
],
);
const row = result.rows[0];
return row?.claimed_device_id
? `device:${row.claimed_device_id}`
: null;
}
async function findActiveRoute(client, routeId, safeView) {
const edgeId = parseEntityRef(safeView.edgeRef, "edge");
const result = await client.query(
`select r.id, r.project_id, r.edge_id, r.model_profile_ref,
r.protocol, r.lifecycle_state,
e.lifecycle_state as edge_lifecycle_state,
p.lifecycle_state as profile_lifecycle_state,
ap.package_key as adapter_ref,
ap.lifecycle_state as adapter_lifecycle_state,
av.lifecycle_state as adapter_version_lifecycle_state
from device_routes r
join device_edges e on e.id = r.edge_id
join device_model_profiles p on p.profile_ref = r.model_profile_ref
join device_adapter_versions av on av.id = p.adapter_version_id
join device_adapter_packages ap on ap.id = av.adapter_package_id
where r.id = $1
for share`,
[routeId],
);
const route = result.rows[0];
if (!route) throw domainError("device_gateway_route_not_found", 404);
if (
route.lifecycle_state !== "active"
|| route.edge_lifecycle_state !== "active"
|| route.profile_lifecycle_state !== "active"
|| route.adapter_lifecycle_state !== "active"
|| route.adapter_version_lifecycle_state !== "active"
) {
throw domainError("device_gateway_route_not_active", 409);
}
if (
route.edge_id !== edgeId
|| route.model_profile_ref !== safeView.protocolProfileRef
|| route.protocol !== safeView.protocol
|| route.adapter_ref !== safeView.adapterRef
) {
throw domainError("device_gateway_route_contract_mismatch", 409);
}
return route;
}
function receiptView(row, replayed, claimedDeviceRef) {
return {
acceptance: {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: `acceptance:${row.id}`,
idempotencyKey: row.idempotency_key,
status: "accepted",
replayed,
acceptedAt: new Date(row.accepted_at).toISOString(),
},
claimedDeviceRef,
};
}
function parseEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,111 @@
export async function resolveInboundRoute(client, input = {}) {
if (!client || typeof client.query !== "function") {
throw new TypeError("device_inbound_route_client_required");
}
const edgeId = parseEntityRef(input.edgeRef, "edge");
const modelProfileRef = normalizeOpaqueRef(
input.modelProfileRef,
"model_profile_ref",
);
const protocol = normalizeUpperToken(input.protocol, "protocol");
const identifierKind = normalizeLowerToken(
input.identifierKind,
"identifier_kind",
);
const identifierDigest = normalizeIdentifierDigest(input.identifierDigest);
const observedAt = normalizeTimestamp(input.observedAt, "observed_at");
const result = await client.query(
`select r.id
from device_enrollment_intents ei
join device_routes r
on r.id = ei.route_id
and r.project_id = ei.project_id
and r.model_profile_ref = ei.model_profile_ref
join device_edges e on e.id = r.edge_id
where r.edge_id = $1
and r.model_profile_ref = $2
and r.protocol = $3
and r.lifecycle_state = 'active'
and e.lifecycle_state = 'active'
and e.channel_lifecycle_state = 'active'
and ei.expected_identifier_kind = $4
and ei.expected_identifier_digest = $5
and ei.lifecycle_state in ('pending', 'observed', 'claimed')
and (
ei.lifecycle_state = 'claimed'
or ei.expires_at is null
or ei.expires_at > $6
)
order by r.id
limit 2`,
[
edgeId,
modelProfileRef,
protocol,
identifierKind,
identifierDigest,
observedAt,
],
);
if (result.rows.length > 1) {
throw domainError("device_inbound_route_ambiguous", 409);
}
return result.rows[0]?.id ? `route:${result.rows[0].id}` : null;
}
function parseEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function normalizeOpaqueRef(value, name) {
if (
typeof value !== "string"
|| !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)
) {
throw new TypeError(`device_inbound_route_${name}_invalid`);
}
return value;
}
function normalizeUpperToken(value, name) {
if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]{0,31}$/.test(value)) {
throw new TypeError(`device_inbound_route_${name}_invalid`);
}
return value;
}
function normalizeLowerToken(value, name) {
if (typeof value !== "string" || !/^[a-z][a-z0-9._:-]{1,63}$/.test(value)) {
throw new TypeError(`device_inbound_route_${name}_invalid`);
}
return value;
}
function normalizeIdentifierDigest(value) {
if (typeof value !== "string" || !/^hmac-sha256:[a-f0-9]{64}$/.test(value)) {
throw new TypeError("device_inbound_route_identifier_digest_invalid");
}
return value;
}
function normalizeTimestamp(value, name) {
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
throw new TypeError(`device_inbound_route_${name}_invalid`);
}
return value;
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,505 @@
import {
assertIdentifierDigest,
assertSafeProjection,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import {
normalizeCertificateIdentities,
} from "../../../packages/device-edge-channel-contract/src/index.mjs";
import { normalizeManagementActor } from "./project-management.mjs";
export const DEVICE_INFRASTRUCTURE_COMMAND_KINDS = Object.freeze([
"adapter_package.ensure",
"adapter_version.register",
"model_profile.register",
"edge.ensure",
"route.ensure",
"enrollment_intent.ensure",
]);
const commandKindSet = new Set(DEVICE_INFRASTRUCTURE_COMMAND_KINDS);
const keyPattern = /^[a-z][a-z0-9-]{1,62}$/;
const opaqueRefPattern = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{2,255}$/;
const profileRefPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
const protocolPattern = /^[A-Z][A-Z0-9_]{0,31}$/;
const capabilityPattern = /^[a-z][a-z0-9._-]{1,63}$/;
const semverPattern = /^[0-9]+\.[0-9]+\.[0-9]+(?:[+-][A-Za-z0-9.-]+)?$/;
const digestPattern = /^sha256:[a-f0-9]{64}$/;
const isoTimestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
export function isInfrastructureManagementCommand(kind) {
return commandKindSet.has(kind);
}
export function normalizeInfrastructureManagementCommand(kind, input) {
if (!commandKindSet.has(kind)) {
throw new TypeError("device_infrastructure_command_kind_invalid");
}
assertPlainObject(input, "device_infrastructure_command_invalid");
if (kind === "adapter_package.ensure") {
assertAllowedKeys(input, [
"packageKey",
"displayName",
"publisherRef",
"lifecycleState",
]);
return Object.freeze({
packageKey: normalizeKey(input.packageKey, "device_adapter_package_key_invalid"),
displayName: normalizeDisplayText(
input.displayName,
160,
"device_adapter_package_name_invalid",
),
publisherRef: normalizeOpaqueRef(
input.publisherRef,
"device_adapter_publisher_ref_invalid",
),
lifecycleState: normalizeEnum(
input.lifecycleState ?? "active",
new Set(["active", "retired"]),
"device_adapter_package_state_invalid",
),
});
}
if (kind === "adapter_version.register") {
assertAllowedKeys(input, [
"adapterPackageRef",
"version",
"runtimePackageRef",
"contentDigest",
"contractVersion",
"capabilities",
"lifecycleState",
]);
return Object.freeze({
adapterPackageId: normalizeEntityRef(
input.adapterPackageRef,
"adapter-package",
"device_adapter_package_ref_invalid",
),
version: normalizePattern(
input.version,
semverPattern,
"device_adapter_version_invalid",
),
runtimePackageRef: normalizeOpaqueRef(
input.runtimePackageRef,
"device_adapter_runtime_package_ref_invalid",
),
contentDigest: normalizePattern(
input.contentDigest,
digestPattern,
"device_adapter_content_digest_invalid",
),
contractVersion: normalizeProfileRef(
input.contractVersion,
"device_adapter_contract_version_invalid",
),
capabilities: Object.freeze(normalizeCapabilities(input.capabilities ?? [])),
lifecycleState: normalizeEnum(
input.lifecycleState ?? "draft",
new Set(["draft", "active", "retired"]),
"device_adapter_version_state_invalid",
),
});
}
if (kind === "model_profile.register") {
assertAllowedKeys(input, [
"adapterVersionRef",
"profileRef",
"schemaVersion",
"vendor",
"model",
"deviceType",
"protocol",
"schemaArtifactRef",
"profileDigest",
"capabilities",
"lifecycleState",
]);
return Object.freeze({
adapterVersionId: normalizeEntityRef(
input.adapterVersionRef,
"adapter-version",
"device_adapter_version_ref_invalid",
),
profileRef: normalizeProfileRef(
input.profileRef,
"device_model_profile_ref_invalid",
),
schemaVersion: normalizeProfileRef(
input.schemaVersion,
"device_model_profile_schema_version_invalid",
),
vendor: normalizeDisplayText(input.vendor, 120, "device_model_vendor_invalid"),
model: normalizeDisplayText(input.model, 120, "device_model_name_invalid"),
deviceType: normalizePattern(
input.deviceType,
capabilityPattern,
"device_model_type_invalid",
),
protocol: normalizePattern(
input.protocol,
protocolPattern,
"device_model_protocol_invalid",
),
schemaArtifactRef: normalizeOpaqueRef(
input.schemaArtifactRef,
"device_model_schema_artifact_ref_invalid",
),
profileDigest: normalizePattern(
input.profileDigest,
digestPattern,
"device_model_profile_digest_invalid",
),
capabilities: Object.freeze(normalizeCapabilities(input.capabilities ?? [])),
lifecycleState: normalizeEnum(
input.lifecycleState ?? "draft",
new Set(["draft", "active", "retired"]),
"device_model_profile_state_invalid",
),
});
}
if (kind === "edge.ensure") {
assertAllowedKeys(input, [
"edgeKey",
"displayName",
"deploymentRef",
"lifecycleState",
"channel",
]);
const normalized = {
edgeKey: normalizeKey(input.edgeKey, "device_edge_key_invalid"),
displayName: normalizeDisplayText(
input.displayName,
160,
"device_edge_name_invalid",
),
deploymentRef: normalizeOptionalOpaqueRef(
input.deploymentRef,
"device_edge_deployment_ref_invalid",
),
lifecycleState: normalizeEnum(
input.lifecycleState ?? "provisioning",
new Set(["provisioning", "active", "suspended", "retired"]),
"device_edge_state_invalid",
),
};
if (input.channel !== undefined) {
normalized.channel = normalizeEdgeChannel(input.channel);
}
return Object.freeze(normalized);
}
if (kind === "route.ensure") {
assertAllowedKeys(input, [
"projectRef",
"routeKey",
"displayName",
"edgeRef",
"modelProfileRef",
"listenerRef",
"protocol",
"direction",
"lifecycleState",
]);
return Object.freeze({
projectId: normalizeEntityRef(
input.projectRef,
"project",
"device_project_ref_invalid",
),
routeKey: normalizeKey(input.routeKey, "device_route_key_invalid"),
displayName: normalizeDisplayText(
input.displayName,
160,
"device_route_name_invalid",
),
edgeId: normalizeEntityRef(
input.edgeRef,
"edge",
"device_edge_ref_invalid",
),
modelProfileRef: normalizeProfileRef(
input.modelProfileRef,
"device_model_profile_ref_invalid",
),
listenerRef: normalizeOpaqueRef(
input.listenerRef,
"device_route_listener_ref_invalid",
),
protocol: normalizePattern(
input.protocol,
protocolPattern,
"device_route_protocol_invalid",
),
direction: normalizeEnum(
input.direction ?? "telemetry",
new Set(["telemetry", "bidirectional"]),
"device_route_direction_invalid",
),
lifecycleState: normalizeEnum(
input.lifecycleState ?? "draft",
new Set(["draft", "active", "suspended", "retired"]),
"device_route_state_invalid",
),
});
}
assertAllowedKeys(input, [
"projectRef",
"enrollmentKey",
"routeRef",
"modelProfileRef",
"displayName",
"identifierKind",
"identifierDigest",
"identifierMasked",
"expiresAt",
]);
const identifierMasked = normalizeDisplayText(
input.identifierMasked,
64,
"device_enrollment_identifier_masked_invalid",
);
assertSafeProjection({ identifierMasked });
return Object.freeze({
projectId: normalizeEntityRef(
input.projectRef,
"project",
"device_project_ref_invalid",
),
enrollmentKey: normalizeKey(
input.enrollmentKey,
"device_enrollment_key_invalid",
),
routeId: normalizeEntityRef(
input.routeRef,
"route",
"device_route_ref_invalid",
),
modelProfileRef: normalizeProfileRef(
input.modelProfileRef,
"device_model_profile_ref_invalid",
),
displayName: normalizeDisplayText(
input.displayName,
160,
"device_enrollment_name_invalid",
),
identifierKind: normalizePattern(
input.identifierKind,
/^[a-z][a-z0-9._-]{1,31}$/,
"device_enrollment_identifier_kind_invalid",
),
identifierDigest: assertIdentifierDigest(input.identifierDigest),
identifierMasked,
expiresAt: normalizeOptionalTimestamp(input.expiresAt),
});
}
export function assertPlatformCatalogAuthority(actorInput) {
const actor = normalizeManagementActor(actorInput);
if (actor.hubRole !== "owner") {
throw domainError("device_platform_catalog_access_denied", 403);
}
return actor;
}
function normalizeCapabilities(input) {
if (!Array.isArray(input) || input.length > 64) {
throw new TypeError("device_adapter_capabilities_invalid");
}
return [...new Set(input.map((capability) => normalizePattern(
capability,
capabilityPattern,
"device_adapter_capability_invalid",
)))].sort();
}
function normalizeEntityRef(value, prefix, code) {
if (typeof value !== "string") throw new TypeError(code);
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(code);
return match[1].toLowerCase();
}
function normalizeKey(value, code) {
return normalizePattern(value, keyPattern, code);
}
function normalizeProfileRef(value, code) {
return normalizePattern(value, profileRefPattern, code);
}
function normalizeOpaqueRef(value, code) {
return normalizePattern(value, opaqueRefPattern, code);
}
function normalizeOptionalOpaqueRef(value, code) {
if (value == null || value === "") return null;
return normalizeOpaqueRef(value, code);
}
function normalizeEdgeChannel(input) {
assertPlainObject(input, "device_edge_channel_invalid");
assertAllowedKeys(input, [
"endpoint",
"servername",
"generationRef",
"trustBundleRef",
"certificateIdentities",
"lifecycleState",
]);
const lifecycleState = normalizeEnum(
input.lifecycleState ?? "disabled",
new Set(["disabled", "active", "revoked"]),
"device_edge_channel_state_invalid",
);
if (lifecycleState === "disabled") {
if (Object.keys(input).some((key) => key !== "lifecycleState")) {
throw new TypeError("device_edge_channel_disabled_configuration_invalid");
}
return Object.freeze({
endpoint: null,
servername: null,
generationRef: null,
trustBundleRef: null,
certificateIdentities: Object.freeze([]),
lifecycleState,
});
}
const endpoint = normalizeEdgeEndpoint(input.endpoint);
const servername = normalizePattern(
input.servername,
/^[A-Za-z0-9.-]{1,253}$/,
"device_edge_channel_servername_invalid",
).toLowerCase();
if (servername !== endpoint.hostname) {
throw new TypeError("device_edge_channel_servername_mismatch");
}
return Object.freeze({
endpoint: endpoint.toString(),
servername,
generationRef: normalizeProfileRef(
input.generationRef,
"device_edge_channel_generation_invalid",
),
trustBundleRef: normalizePattern(
input.trustBundleRef,
/^edge-trust:[a-z][a-z0-9-]{1,62}$/,
"device_edge_channel_trust_bundle_ref_invalid",
),
certificateIdentities: normalizeCertificateIdentities(
input.certificateIdentities,
),
lifecycleState,
});
}
function normalizeEdgeEndpoint(value) {
let endpoint;
try {
endpoint = new URL(String(value || ""));
} catch {
throw new TypeError("device_edge_channel_endpoint_invalid");
}
if (
endpoint.protocol !== "https:"
|| endpoint.username
|| endpoint.password
|| endpoint.pathname !== "/"
|| endpoint.search
|| endpoint.hash
|| endpoint.port !== ""
|| !isPublicIpv4(endpoint.hostname)
) {
throw new TypeError("device_edge_channel_endpoint_invalid");
}
return endpoint;
}
function isPublicIpv4(value) {
const octets = value.split(".").map(Number);
if (
octets.length !== 4
|| octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
) return false;
const [a, b, c] = octets;
if (a < 1 || a >= 224) return false;
if (a === 10 || a === 127) return false;
if (a === 100 && b >= 64 && b <= 127) return false;
if (a === 169 && b === 254) return false;
if (a === 172 && b >= 16 && b <= 31) return false;
if (a === 192 && (b === 0 || b === 168)) return false;
if (a === 192 && b === 88 && c === 99) return false;
if (a === 198 && (b === 18 || b === 19 || b === 51)) return false;
if (a === 203 && b === 0 && c === 113) return false;
return true;
}
function normalizePattern(value, pattern, code) {
if (typeof value !== "string" || !pattern.test(value)) {
throw new TypeError(code);
}
return value;
}
function normalizeDisplayText(value, maxLength, code) {
if (typeof value !== "string") throw new TypeError(code);
const normalized = value.trim();
if (normalized.length < 1 || normalized.length > maxLength) {
throw new TypeError(code);
}
if (/\u0000|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(normalized)) {
throw new TypeError(code);
}
return normalized;
}
function normalizeOptionalTimestamp(value) {
if (value == null || value === "") return null;
if (typeof value !== "string" || !isoTimestampPattern.test(value)) {
throw new TypeError("device_enrollment_expires_at_invalid");
}
const parsed = new Date(value);
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {
throw new TypeError("device_enrollment_expires_at_invalid");
}
return value;
}
function normalizeEnum(value, allowed, code) {
if (typeof value !== "string" || !allowed.has(value)) {
throw new TypeError(code);
}
return value;
}
function assertPlainObject(value, code) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(code);
}
}
function assertAllowedKeys(input, allowed) {
const allowedSet = new Set(allowed);
for (const key of Object.keys(input)) {
if (!allowedSet.has(key)) {
throw new TypeError(`device_management_command_field_unexpected:${key}`);
}
}
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,964 @@
import { randomUUID } from "node:crypto";
import {
assertPlatformCatalogAuthority,
isInfrastructureManagementCommand,
} from "./infrastructure-management.mjs";
import {
assertProjectCapability,
toProjectRef,
} from "./project-management.mjs";
export async function applyInfrastructureManagementCommand(
client,
{ commandKind, actor, command },
) {
if (!isInfrastructureManagementCommand(commandKind)) {
throw new TypeError("device_infrastructure_command_kind_invalid");
}
if (commandKind === "adapter_package.ensure") {
return ensureAdapterPackage(client, actor, command);
}
if (commandKind === "adapter_version.register") {
return registerAdapterVersion(client, actor, command);
}
if (commandKind === "model_profile.register") {
return registerModelProfile(client, actor, command);
}
if (commandKind === "edge.ensure") {
return ensureEdge(client, actor, command);
}
if (commandKind === "route.ensure") {
return ensureRoute(client, actor, command);
}
return ensureEnrollmentIntent(client, actor, command);
}
export async function authorizeInfrastructureManagementReplay(
client,
{ commandKind, actor, command },
) {
if (!isInfrastructureManagementCommand(commandKind)) {
throw new TypeError("device_infrastructure_command_kind_invalid");
}
if (
commandKind === "adapter_package.ensure"
|| commandKind === "adapter_version.register"
|| commandKind === "model_profile.register"
|| commandKind === "edge.ensure"
) {
assertPlatformCatalogAuthority(actor);
return;
}
const capability = commandKind === "route.ensure"
? "route.manage"
: "device.enroll";
await assertCurrentProjectCapability(client, actor, command.projectId, capability);
}
async function ensureAdapterPackage(client, actor, command) {
assertPlatformCatalogAuthority(actor);
const result = await client.query(
`insert into device_adapter_packages (
id,
package_key,
display_name,
publisher_ref,
lifecycle_state,
created_by_ref
) values ($1, $2, $3, $4, $5, $6)
on conflict (package_key) do update set
display_name = excluded.display_name,
lifecycle_state = excluded.lifecycle_state,
updated_at = now()
where device_adapter_packages.publisher_ref = excluded.publisher_ref
and (
device_adapter_packages.lifecycle_state = excluded.lifecycle_state
or (
device_adapter_packages.lifecycle_state = 'active'
and excluded.lifecycle_state = 'retired'
)
)
returning id, package_key, display_name, publisher_ref, lifecycle_state,
created_at, updated_at, (xmax = 0) as created`,
[
randomUUID(),
command.packageKey,
command.displayName,
command.publisherRef,
command.lifecycleState,
actor.userRef,
],
);
const row = requireMutationRow(
result,
"device_adapter_package_identity_conflict",
);
await addAudit(client, {
eventType: row.created
? "adapter_package.created"
: "adapter_package.updated",
actorRef: actor.userRef,
payload: {
adapterPackageRef: `adapter-package:${row.id}`,
packageKey: row.package_key,
publisherRef: row.publisher_ref,
lifecycleState: row.lifecycle_state,
},
});
return {
created: row.created === true,
adapterPackage: adapterPackageView(row),
};
}
async function registerAdapterVersion(client, actor, command) {
assertPlatformCatalogAuthority(actor);
const adapterPackage = await findAdapterPackage(
client,
command.adapterPackageId,
);
if (adapterPackage.lifecycle_state !== "active") {
throw domainError("device_adapter_package_inactive", 409);
}
const result = await client.query(
`insert into device_adapter_versions (
id,
adapter_package_id,
version,
runtime_package_ref,
content_digest,
contract_version,
capabilities,
lifecycle_state,
registered_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
on conflict (adapter_package_id, version) do update set
lifecycle_state = excluded.lifecycle_state,
updated_at = now()
where device_adapter_versions.runtime_package_ref = excluded.runtime_package_ref
and device_adapter_versions.content_digest = excluded.content_digest
and device_adapter_versions.contract_version = excluded.contract_version
and device_adapter_versions.capabilities = excluded.capabilities
and (
device_adapter_versions.lifecycle_state = excluded.lifecycle_state
or (
device_adapter_versions.lifecycle_state = 'draft'
and excluded.lifecycle_state in ('active', 'retired')
)
or (
device_adapter_versions.lifecycle_state = 'active'
and excluded.lifecycle_state = 'retired'
)
)
returning id, adapter_package_id, version, runtime_package_ref,
content_digest, contract_version, capabilities, lifecycle_state,
created_at, updated_at, (xmax = 0) as created`,
[
randomUUID(),
command.adapterPackageId,
command.version,
command.runtimePackageRef,
command.contentDigest,
command.contractVersion,
command.capabilities,
command.lifecycleState,
actor.userRef,
],
);
const row = requireMutationRow(
result,
"device_adapter_version_identity_conflict",
);
await addAudit(client, {
eventType: row.created
? "adapter_version.registered"
: "adapter_version.lifecycle_updated",
actorRef: actor.userRef,
payload: {
adapterPackageRef: `adapter-package:${row.adapter_package_id}`,
adapterVersionRef: `adapter-version:${row.id}`,
version: row.version,
contentDigest: row.content_digest,
lifecycleState: row.lifecycle_state,
},
});
return {
created: row.created === true,
adapterPackage: adapterPackageView(adapterPackage),
adapterVersion: adapterVersionView(row),
};
}
async function registerModelProfile(client, actor, command) {
assertPlatformCatalogAuthority(actor);
const adapterVersion = await findAdapterVersion(
client,
command.adapterVersionId,
);
if (
adapterVersion.package_lifecycle_state !== "active"
|| adapterVersion.lifecycle_state === "retired"
) {
throw domainError("device_adapter_version_inactive", 409);
}
if (
command.lifecycleState === "active"
&& adapterVersion.lifecycle_state !== "active"
) {
throw domainError("device_model_profile_adapter_not_active", 409);
}
const profile = {
schemaVersion: command.schemaVersion,
profileRef: command.profileRef,
vendor: command.vendor,
model: command.model,
deviceType: command.deviceType,
protocol: command.protocol,
schemaArtifactRef: command.schemaArtifactRef,
capabilities: command.capabilities,
};
const existingProfile = await findOptionalModelProfileRegistration(
client,
command.profileRef,
);
const adoptsLegacyProfile = isLegacyMetadataOnlyProfile(existingProfile);
const result = await client.query(
`insert into device_model_profiles (
profile_ref,
schema_version,
vendor,
model,
device_type,
protocol,
profile,
adapter_version_id,
schema_artifact_ref,
profile_digest,
capabilities,
lifecycle_state
) values ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10, $11, $12)
on conflict (profile_ref) do update set
adapter_version_id = case
when device_model_profiles.adapter_version_id is null
then excluded.adapter_version_id
else device_model_profiles.adapter_version_id
end,
schema_artifact_ref = case
when device_model_profiles.schema_artifact_ref is null
then excluded.schema_artifact_ref
else device_model_profiles.schema_artifact_ref
end,
profile_digest = case
when device_model_profiles.profile_digest is null
then excluded.profile_digest
else device_model_profiles.profile_digest
end,
capabilities = case
when cardinality(device_model_profiles.capabilities) = 0
then excluded.capabilities
else device_model_profiles.capabilities
end,
lifecycle_state = excluded.lifecycle_state,
updated_at = now()
where device_model_profiles.schema_version = excluded.schema_version
and device_model_profiles.vendor = excluded.vendor
and device_model_profiles.model = excluded.model
and device_model_profiles.device_type = excluded.device_type
and device_model_profiles.protocol = excluded.protocol
and (
(
(
device_model_profiles.profile = excluded.profile
or (
jsonb_typeof(device_model_profiles.profile) = 'object'
and device_model_profiles.profile ->> 'schemaVersion' = excluded.schema_version
and device_model_profiles.profile ->> 'profileRef' = excluded.profile_ref
and device_model_profiles.profile ->> 'vendor' = excluded.vendor
and device_model_profiles.profile ->> 'model' = excluded.model
and device_model_profiles.profile ->> 'deviceType' = excluded.device_type
and device_model_profiles.profile ->> 'protocol' = excluded.protocol
)
)
and device_model_profiles.adapter_version_id = excluded.adapter_version_id
and device_model_profiles.schema_artifact_ref = excluded.schema_artifact_ref
and device_model_profiles.profile_digest = excluded.profile_digest
and device_model_profiles.capabilities = excluded.capabilities
and (
device_model_profiles.lifecycle_state = excluded.lifecycle_state
or (
device_model_profiles.lifecycle_state = 'draft'
and excluded.lifecycle_state in ('active', 'retired')
)
or (
device_model_profiles.lifecycle_state = 'active'
and excluded.lifecycle_state = 'retired'
)
)
)
or (
jsonb_typeof(device_model_profiles.profile) = 'object'
and device_model_profiles.profile ->> 'schemaVersion' = excluded.schema_version
and device_model_profiles.profile ->> 'profileRef' = excluded.profile_ref
and device_model_profiles.profile ->> 'vendor' = excluded.vendor
and device_model_profiles.profile ->> 'model' = excluded.model
and device_model_profiles.profile ->> 'deviceType' = excluded.device_type
and device_model_profiles.profile ->> 'protocol' = excluded.protocol
and device_model_profiles.adapter_version_id is null
and device_model_profiles.schema_artifact_ref is null
and device_model_profiles.profile_digest is null
and cardinality(device_model_profiles.capabilities) = 0
and device_model_profiles.lifecycle_state = 'active'
and excluded.lifecycle_state = 'draft'
)
)
returning profile_ref, schema_version, vendor, model, device_type,
protocol, adapter_version_id, schema_artifact_ref, profile_digest,
capabilities, lifecycle_state, created_at, updated_at,
(xmax = 0) as created`,
[
command.profileRef,
command.schemaVersion,
command.vendor,
command.model,
command.deviceType,
command.protocol,
JSON.stringify(profile),
command.adapterVersionId,
command.schemaArtifactRef,
command.profileDigest,
command.capabilities,
command.lifecycleState,
],
);
const row = requireMutationRow(
result,
"device_model_profile_identity_conflict",
);
await addAudit(client, {
eventType: row.created
? "model_profile.registered"
: adoptsLegacyProfile
? "model_profile.registry_adopted"
: "model_profile.lifecycle_updated",
actorRef: actor.userRef,
payload: {
adapterVersionRef: `adapter-version:${row.adapter_version_id}`,
modelProfileRef: row.profile_ref,
profileDigest: row.profile_digest,
lifecycleState: row.lifecycle_state,
},
});
return {
created: row.created === true,
adapterVersion: adapterVersionView(adapterVersion),
modelProfile: modelProfileView(row),
};
}
async function findOptionalModelProfileRegistration(client, profileRef) {
const result = await client.query(
`select profile_ref, adapter_version_id, schema_artifact_ref,
profile_digest, capabilities, lifecycle_state
from device_model_profiles
where profile_ref = $1
for update`,
[profileRef],
);
return result.rows[0] ?? null;
}
function isLegacyMetadataOnlyProfile(profile) {
return profile != null
&& profile.adapter_version_id == null
&& profile.schema_artifact_ref == null
&& profile.profile_digest == null
&& Array.isArray(profile.capabilities)
&& profile.capabilities.length === 0
&& profile.lifecycle_state === "active";
}
async function ensureEdge(client, actor, command) {
assertPlatformCatalogAuthority(actor);
const channelProvided = command.channel !== undefined;
const channel = command.channel ?? {
endpoint: null,
servername: null,
generationRef: null,
trustBundleRef: null,
certificateIdentities: [],
lifecycleState: "disabled",
};
const result = await client.query(
`insert into device_edges (
id,
edge_key,
display_name,
deployment_ref,
lifecycle_state,
channel_endpoint,
channel_servername,
channel_generation_ref,
channel_trust_bundle_ref,
channel_certificate_identities,
channel_lifecycle_state,
created_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11, $13)
on conflict (edge_key) do update set
display_name = excluded.display_name,
deployment_ref = excluded.deployment_ref,
lifecycle_state = excluded.lifecycle_state,
channel_endpoint = case when $12 then excluded.channel_endpoint
else device_edges.channel_endpoint end,
channel_servername = case when $12 then excluded.channel_servername
else device_edges.channel_servername end,
channel_generation_ref = case when $12 then excluded.channel_generation_ref
else device_edges.channel_generation_ref end,
channel_trust_bundle_ref = case when $12 then excluded.channel_trust_bundle_ref
else device_edges.channel_trust_bundle_ref end,
channel_certificate_identities = case when $12
then excluded.channel_certificate_identities
else device_edges.channel_certificate_identities end,
channel_lifecycle_state = case when $12
then excluded.channel_lifecycle_state
else device_edges.channel_lifecycle_state end,
updated_at = now()
where (
device_edges.lifecycle_state = excluded.lifecycle_state
or (
device_edges.lifecycle_state = 'provisioning'
and excluded.lifecycle_state in ('active', 'retired')
)
or (
device_edges.lifecycle_state = 'active'
and excluded.lifecycle_state in ('suspended', 'retired')
)
or (
device_edges.lifecycle_state = 'suspended'
and excluded.lifecycle_state in ('active', 'retired')
)
)
and (
not $12
or device_edges.channel_lifecycle_state = excluded.channel_lifecycle_state
or (
device_edges.channel_lifecycle_state = 'disabled'
and excluded.channel_lifecycle_state = 'active'
)
or (
device_edges.channel_lifecycle_state = 'active'
and excluded.channel_lifecycle_state in ('disabled', 'revoked')
)
)
returning id, edge_key, display_name, deployment_ref, lifecycle_state,
channel_endpoint, channel_servername, channel_generation_ref,
channel_trust_bundle_ref, channel_certificate_identities,
channel_lifecycle_state, created_at, updated_at,
(xmax = 0) as created`,
[
randomUUID(),
command.edgeKey,
command.displayName,
command.deploymentRef,
command.lifecycleState,
channel.endpoint,
channel.servername,
channel.generationRef,
channel.trustBundleRef,
JSON.stringify(channel.certificateIdentities),
channel.lifecycleState,
channelProvided,
actor.userRef,
],
);
const row = requireMutationRow(result, "device_edge_identity_conflict");
await addAudit(client, {
eventType: row.created ? "edge.created" : "edge.updated",
actorRef: actor.userRef,
payload: {
edgeRef: `edge:${row.id}`,
edgeKey: row.edge_key,
deploymentRef: row.deployment_ref,
lifecycleState: row.lifecycle_state,
channelLifecycleState: row.channel_lifecycle_state,
channelGenerationRef: row.channel_generation_ref,
channelTrustBundleRef: row.channel_trust_bundle_ref,
},
});
return {
created: row.created === true,
edge: edgeView(row),
};
}
async function ensureRoute(client, actor, command) {
await assertCurrentProjectCapability(
client,
actor,
command.projectId,
"route.manage",
);
const edge = await findEdge(client, command.edgeId);
const profile = await findModelProfile(client, command.modelProfileRef);
assertRouteDependencies(command, edge, profile);
const result = await client.query(
`insert into device_routes (
id,
project_id,
route_key,
display_name,
edge_id,
model_profile_ref,
listener_ref,
protocol,
direction,
lifecycle_state,
created_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
on conflict (project_id, route_key) do update set
display_name = excluded.display_name,
edge_id = excluded.edge_id,
model_profile_ref = excluded.model_profile_ref,
listener_ref = excluded.listener_ref,
protocol = excluded.protocol,
direction = excluded.direction,
lifecycle_state = excluded.lifecycle_state,
updated_at = now()
where
device_routes.lifecycle_state = excluded.lifecycle_state
or (
device_routes.lifecycle_state = 'draft'
and excluded.lifecycle_state in ('active', 'retired')
)
or (
device_routes.lifecycle_state = 'active'
and excluded.lifecycle_state in ('suspended', 'retired')
)
or (
device_routes.lifecycle_state = 'suspended'
and excluded.lifecycle_state in ('active', 'retired')
)
returning id, project_id, route_key, display_name, edge_id,
model_profile_ref, listener_ref, protocol, direction, lifecycle_state,
created_at, updated_at, (xmax = 0) as created`,
[
randomUUID(),
command.projectId,
command.routeKey,
command.displayName,
command.edgeId,
command.modelProfileRef,
command.listenerRef,
command.protocol,
command.direction,
command.lifecycleState,
actor.userRef,
],
);
const row = requireMutationRow(result, "device_route_identity_conflict");
await addAudit(client, {
eventType: row.created ? "route.created" : "route.updated",
actorRef: actor.userRef,
projectId: command.projectId,
payload: {
projectRef: toProjectRef(command.projectId),
routeRef: `route:${row.id}`,
routeKey: row.route_key,
edgeRef: `edge:${row.edge_id}`,
modelProfileRef: row.model_profile_ref,
listenerRef: row.listener_ref,
direction: row.direction,
lifecycleState: row.lifecycle_state,
},
});
return {
created: row.created === true,
route: routeView(row),
};
}
async function ensureEnrollmentIntent(client, actor, command) {
await assertCurrentProjectCapability(
client,
actor,
command.projectId,
"device.enroll",
);
const route = await findProjectRoute(
client,
command.projectId,
command.routeId,
);
if (route.lifecycle_state !== "active") {
throw domainError("device_enrollment_route_inactive", 409);
}
if (route.model_profile_ref !== command.modelProfileRef) {
throw domainError("device_enrollment_profile_mismatch", 409);
}
const result = await client.query(
`insert into device_enrollment_intents (
id,
project_id,
enrollment_key,
route_id,
model_profile_ref,
display_name,
expected_identifier_kind,
expected_identifier_digest,
expected_identifier_masked,
expires_at,
created_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
on conflict (project_id, enrollment_key) do update set
display_name = excluded.display_name,
expires_at = excluded.expires_at,
updated_at = now()
where device_enrollment_intents.route_id = excluded.route_id
and device_enrollment_intents.model_profile_ref = excluded.model_profile_ref
and device_enrollment_intents.expected_identifier_kind = excluded.expected_identifier_kind
and device_enrollment_intents.expected_identifier_digest = excluded.expected_identifier_digest
and device_enrollment_intents.expected_identifier_masked = excluded.expected_identifier_masked
and device_enrollment_intents.lifecycle_state = 'pending'
returning id, project_id, enrollment_key, route_id, model_profile_ref,
display_name, expected_identifier_kind, expected_identifier_masked,
lifecycle_state, expires_at, claimed_device_id, created_at, updated_at,
(xmax = 0) as created`,
[
randomUUID(),
command.projectId,
command.enrollmentKey,
command.routeId,
command.modelProfileRef,
command.displayName,
command.identifierKind,
command.identifierDigest,
command.identifierMasked,
command.expiresAt,
actor.userRef,
],
);
const row = requireMutationRow(
result,
"device_enrollment_intent_identity_conflict",
);
await addAudit(client, {
eventType: row.created
? "enrollment_intent.created"
: "enrollment_intent.updated",
actorRef: actor.userRef,
projectId: command.projectId,
payload: {
projectRef: toProjectRef(command.projectId),
enrollmentIntentRef: `enrollment-intent:${row.id}`,
enrollmentKey: row.enrollment_key,
routeRef: `route:${row.route_id}`,
modelProfileRef: row.model_profile_ref,
identifier: {
kind: row.expected_identifier_kind,
masked: row.expected_identifier_masked,
},
lifecycleState: row.lifecycle_state,
},
});
return {
created: row.created === true,
enrollmentIntent: enrollmentIntentView(row),
};
}
async function assertCurrentProjectCapability(
client,
actor,
projectId,
capability,
) {
const project = await client.query(
`select p.id, p.lifecycle_state, os.lifecycle_state as owner_lifecycle_state
from device_projects p
join device_owner_scopes os on os.id = p.owner_scope_id
where p.id = $1
for share of p, os`,
[projectId],
);
const row = project.rows[0];
if (!row) throw domainError("device_project_not_found", 404);
if (row.owner_lifecycle_state !== "active") {
throw domainError("device_owner_scope_inactive", 409);
}
if (row.lifecycle_state !== "active") {
throw domainError("device_project_inactive", 409);
}
const grants = await client.query(
`select id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state
from device_project_grants
where project_id = $1
order by created_at, id
for share`,
[projectId],
);
assertProjectCapability(
actor,
grants.rows.map((grant) => ({
grantRef: `grant:${grant.id}`,
principalKind: grant.principal_kind,
principalRef: grant.principal_ref,
projectRole: grant.project_role,
capabilityAllow: grant.capability_allow ?? [],
capabilityDeny: grant.capability_deny ?? [],
lifecycleState: grant.lifecycle_state,
})),
capability,
);
}
async function findAdapterPackage(client, adapterPackageId) {
const result = await client.query(
`select id, package_key, display_name, publisher_ref, lifecycle_state,
created_at, updated_at
from device_adapter_packages
where id = $1
for share`,
[adapterPackageId],
);
if (!result.rows[0]) {
throw domainError("device_adapter_package_not_found", 404);
}
return result.rows[0];
}
async function findAdapterVersion(client, adapterVersionId) {
const result = await client.query(
`select av.id, av.adapter_package_id, av.version,
av.runtime_package_ref, av.content_digest, av.contract_version,
av.capabilities, av.lifecycle_state, av.created_at, av.updated_at,
ap.lifecycle_state as package_lifecycle_state
from device_adapter_versions av
join device_adapter_packages ap on ap.id = av.adapter_package_id
where av.id = $1
for share of av, ap`,
[adapterVersionId],
);
if (!result.rows[0]) {
throw domainError("device_adapter_version_not_found", 404);
}
return result.rows[0];
}
async function findEdge(client, edgeId) {
const result = await client.query(
`select id, edge_key, display_name, deployment_ref, lifecycle_state,
created_at, updated_at
from device_edges
where id = $1
for share`,
[edgeId],
);
if (!result.rows[0]) throw domainError("device_edge_not_found", 404);
return result.rows[0];
}
async function findModelProfile(client, profileRef) {
const result = await client.query(
`select mp.profile_ref, mp.schema_version, mp.vendor, mp.model,
mp.device_type, mp.protocol, mp.adapter_version_id,
mp.schema_artifact_ref, mp.profile_digest, mp.capabilities,
mp.lifecycle_state, mp.created_at, mp.updated_at,
av.lifecycle_state as adapter_lifecycle_state,
ap.lifecycle_state as package_lifecycle_state
from device_model_profiles mp
left join device_adapter_versions av on av.id = mp.adapter_version_id
left join device_adapter_packages ap on ap.id = av.adapter_package_id
where mp.profile_ref = $1
for share of mp`,
[profileRef],
);
if (!result.rows[0]) {
throw domainError("device_model_profile_not_found", 404);
}
return result.rows[0];
}
async function findProjectRoute(client, projectId, routeId) {
const result = await client.query(
`select id, project_id, route_key, display_name, edge_id,
model_profile_ref, listener_ref, protocol, direction, lifecycle_state,
created_at, updated_at
from device_routes
where id = $1 and project_id = $2
for share`,
[routeId, projectId],
);
if (!result.rows[0]) throw domainError("device_route_not_found", 404);
return result.rows[0];
}
function assertRouteDependencies(command, edge, profile) {
if (profile.adapter_version_id == null) {
throw domainError("device_model_profile_unregistered", 409);
}
if (profile.protocol !== command.protocol) {
throw domainError("device_route_protocol_mismatch", 409);
}
if (
edge.lifecycle_state === "retired"
|| profile.lifecycle_state === "retired"
|| profile.adapter_lifecycle_state === "retired"
|| profile.package_lifecycle_state === "retired"
) {
throw domainError("device_route_dependency_inactive", 409);
}
if (
command.lifecycleState === "active"
&& (
edge.lifecycle_state !== "active"
|| profile.lifecycle_state !== "active"
|| profile.adapter_lifecycle_state !== "active"
|| profile.package_lifecycle_state !== "active"
)
) {
throw domainError("device_route_dependency_not_active", 409);
}
}
async function addAudit(client, {
eventType,
actorRef,
projectId = null,
payload,
}) {
await client.query(
`insert into device_audit_events (
id,
event_type,
actor_ref,
project_id,
payload
) values ($1, $2, $3, $4, $5::jsonb)`,
[randomUUID(), eventType, actorRef, projectId, JSON.stringify(payload)],
);
}
function requireMutationRow(result, code) {
if (!result.rows[0]) throw domainError(code, 409);
return result.rows[0];
}
function adapterPackageView(row) {
return {
adapterPackageRef: `adapter-package:${row.id}`,
packageKey: row.package_key,
displayName: row.display_name,
publisherRef: row.publisher_ref,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function adapterVersionView(row) {
return {
adapterVersionRef: `adapter-version:${row.id}`,
adapterPackageRef: `adapter-package:${row.adapter_package_id}`,
version: row.version,
runtimePackageRef: row.runtime_package_ref,
contentDigest: row.content_digest,
contractVersion: row.contract_version,
capabilities: [...(row.capabilities ?? [])].sort(),
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function modelProfileView(row) {
return {
modelProfileRef: row.profile_ref,
adapterVersionRef: `adapter-version:${row.adapter_version_id}`,
schemaVersion: row.schema_version,
vendor: row.vendor,
model: row.model,
deviceType: row.device_type,
protocol: row.protocol,
schemaArtifactRef: row.schema_artifact_ref,
profileDigest: row.profile_digest,
capabilities: [...(row.capabilities ?? [])].sort(),
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function edgeView(row) {
return {
edgeRef: `edge:${row.id}`,
edgeKey: row.edge_key,
displayName: row.display_name,
deploymentRef: row.deployment_ref ?? null,
lifecycleState: row.lifecycle_state,
channel: {
lifecycleState: row.channel_lifecycle_state ?? "disabled",
endpoint: row.channel_endpoint ?? null,
servername: row.channel_servername ?? null,
generationRef: row.channel_generation_ref ?? null,
trustBundleRef: row.channel_trust_bundle_ref ?? null,
certificateIdentities: [...(row.channel_certificate_identities ?? [])],
},
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function routeView(row) {
return {
routeRef: `route:${row.id}`,
projectRef: toProjectRef(row.project_id),
routeKey: row.route_key,
displayName: row.display_name,
edgeRef: `edge:${row.edge_id}`,
modelProfileRef: row.model_profile_ref,
listenerRef: row.listener_ref,
protocol: row.protocol,
direction: row.direction,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function enrollmentIntentView(row) {
return {
enrollmentIntentRef: `enrollment-intent:${row.id}`,
projectRef: toProjectRef(row.project_id),
enrollmentKey: row.enrollment_key,
routeRef: `route:${row.route_id}`,
modelProfileRef: row.model_profile_ref,
displayName: row.display_name,
identifier: {
kind: row.expected_identifier_kind,
masked: row.expected_identifier_masked,
},
lifecycleState: row.lifecycle_state,
expiresAt: toIso(row.expires_at),
claimedDeviceRef: row.claimed_device_id
? `device:${row.claimed_device_id}`
: null,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function toIso(value) {
return value == null ? null : new Date(value).toISOString();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,175 @@
export const DEVICE_LIFECYCLE_COMMAND_KINDS = Object.freeze([
"device.claim",
"device.update",
"device.transfer",
"discovery.reject",
"discovery.expire",
]);
const commandKindSet = new Set(DEVICE_LIFECYCLE_COMMAND_KINDS);
const keyPattern = /^[a-z][a-z0-9-]{1,62}$/;
const resolutionPattern = /^[a-z][a-z0-9._-]{1,63}$/;
export function isLifecycleManagementCommand(kind) {
return commandKindSet.has(kind);
}
export function normalizeLifecycleManagementCommand(kind, input) {
if (!commandKindSet.has(kind)) {
throw new TypeError("device_lifecycle_command_kind_invalid");
}
assertPlainObject(input);
if (kind === "device.claim") {
assertAllowedKeys(input, [
"projectRef",
"enrollmentIntentRef",
"discoveryRef",
"deviceKey",
"displayName",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
enrollmentIntentId: normalizeEntityRef(
input.enrollmentIntentRef,
"enrollment-intent",
),
discoveryId: normalizeEntityRef(input.discoveryRef, "discovery"),
deviceKey: normalizePattern(
input.deviceKey,
keyPattern,
"device_key_invalid",
),
displayName: normalizeDisplayText(input.displayName, 160),
});
}
if (kind === "device.update") {
assertAllowedKeys(input, [
"projectRef",
"deviceRef",
"displayName",
"integrationDeviceId",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
deviceId: normalizeEntityRef(input.deviceRef, "device"),
displayName: normalizeDisplayText(input.displayName, 160),
integrationDeviceId: Object.prototype.hasOwnProperty.call(input, "integrationDeviceId")
? normalizeOptionalDisplayText(
input.integrationDeviceId,
160,
"device_integration_device_id_invalid",
)
: undefined,
});
}
if (kind === "device.transfer") {
assertAllowedKeys(input, [
"deviceRef",
"sourceProjectRef",
"targetProjectRef",
"targetDeviceKey",
]);
const sourceProjectId = normalizeEntityRef(
input.sourceProjectRef,
"project",
);
const targetProjectId = normalizeEntityRef(
input.targetProjectRef,
"project",
);
if (sourceProjectId === targetProjectId) {
throw new TypeError("device_transfer_target_same_as_source");
}
return Object.freeze({
deviceId: normalizeEntityRef(input.deviceRef, "device"),
sourceProjectId,
targetProjectId,
targetDeviceKey: normalizePattern(
input.targetDeviceKey,
keyPattern,
"device_transfer_target_key_invalid",
),
});
}
assertAllowedKeys(input, [
"projectRef",
"discoveryRef",
"resolutionCode",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
discoveryId: normalizeEntityRef(input.discoveryRef, "discovery"),
resolutionCode: normalizePattern(
input.resolutionCode,
resolutionPattern,
"device_discovery_resolution_code_invalid",
),
});
}
function normalizeEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function normalizePattern(value, pattern, code) {
if (typeof value !== "string" || !pattern.test(value)) {
throw new TypeError(code);
}
return value;
}
function normalizeDisplayText(value, maxLength) {
if (typeof value !== "string") {
throw new TypeError("device_display_name_invalid");
}
const normalized = value.trim();
if (
normalized.length < 1
|| normalized.length > maxLength
|| /\u0000|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(normalized)
) {
throw new TypeError("device_display_name_invalid");
}
return normalized;
}
function normalizeOptionalDisplayText(value, maxLength, code) {
if (value === null || value === undefined || value === "") return null;
if (typeof value !== "string") throw new TypeError(code);
const normalized = value.trim();
if (
normalized.length < 1
|| normalized.length > maxLength
|| /\u0000|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(normalized)
) {
throw new TypeError(code);
}
return normalized;
}
function assertPlainObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_lifecycle_command_invalid");
}
}
function assertAllowedKeys(input, allowed) {
const allowedSet = new Set(allowed);
for (const key of Object.keys(input)) {
if (!allowedSet.has(key)) {
throw new TypeError(`device_management_command_field_unexpected:${key}`);
}
}
}
@@ -0,0 +1,754 @@
import { randomUUID } from "node:crypto";
import {
normalizeRestrictedIdentifierProjection,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import { isLifecycleManagementCommand } from "./lifecycle-management.mjs";
import {
assertProjectCapability,
toProjectRef,
} from "./project-management.mjs";
export async function applyLifecycleManagementCommand(
client,
{ commandKind, actor, command },
) {
if (!isLifecycleManagementCommand(commandKind)) {
throw new TypeError("device_lifecycle_command_kind_invalid");
}
if (commandKind === "device.claim") {
return claimDevice(client, actor, command);
}
if (commandKind === "device.update") {
return updateDevice(client, actor, command);
}
if (commandKind === "device.transfer") {
return transferDevice(client, actor, command);
}
return resolveDiscovery(client, actor, command, commandKind);
}
export async function authorizeLifecycleManagementReplay(
client,
{ commandKind, actor, command },
) {
if (!isLifecycleManagementCommand(commandKind)) {
throw new TypeError("device_lifecycle_command_kind_invalid");
}
if (commandKind === "device.transfer") {
await findProjectWithCapability(
client,
actor,
command.sourceProjectId,
"device.transfer",
);
await findProjectWithCapability(
client,
actor,
command.targetProjectId,
"device.transfer",
);
return;
}
if (commandKind === "device.update") {
await findProjectWithCapability(
client,
actor,
command.projectId,
"project.manage",
);
const device = await findDeviceForUpdate(client, command.deviceId);
if (device.project_id !== command.projectId) {
throw domainError("device_update_project_mismatch", 409);
}
return;
}
await findProjectWithCapability(
client,
actor,
command.projectId,
"device.claim",
);
if (commandKind === "device.claim") {
const current = await client.query(
`select di.project_id
from device_discoveries dd
join device_instances di on di.id = dd.claimed_device_id
where dd.id = $1`,
[command.discoveryId],
);
const currentProjectId = current.rows[0]?.project_id;
if (currentProjectId && currentProjectId !== command.projectId) {
await findProjectWithCapability(
client,
actor,
currentProjectId,
"device.claim",
);
}
}
}
async function updateDevice(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"project.manage",
);
const current = await findDeviceForUpdate(client, command.deviceId);
if (current.project_id !== project.id) {
throw domainError("device_update_project_mismatch", 409);
}
if (current.lifecycle_state !== "claimed" && current.lifecycle_state !== "active") {
throw domainError("device_update_lifecycle_blocked", 409);
}
const updated = await client.query(
`update device_instances
set display_name = $3,
integration_device_id = case when $5 then $4 else integration_device_id end,
updated_at = now()
where id = $1 and project_id = $2
returning id, owner_scope_id, project_id, device_key,
model_profile_ref, display_name, integration_device_id, identifier_kind,
identifier_masked, lifecycle_state, created_at, updated_at`,
[
current.id,
project.id,
command.displayName,
command.integrationDeviceId ?? null,
command.integrationDeviceId !== undefined,
],
);
const device = updated.rows[0];
if (!device) throw domainError("device_update_failed", 409);
await addAudit(client, {
eventType: "device.updated",
actorRef: actor.userRef,
projectId: project.id,
deviceId: device.id,
payload: {
deviceRef: `device:${device.id}`,
projectRef: toProjectRef(project.id),
changedFields: command.integrationDeviceId === undefined
? ["displayName"]
: ["displayName", "integrationDeviceId"],
},
});
return {
updated: true,
device: deviceView(device, project),
};
}
async function claimDevice(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"device.claim",
);
const enrollment = await findEnrollmentForUpdate(
client,
command.projectId,
command.enrollmentIntentId,
);
const discovery = await findDiscoveryForUpdate(
client,
command.projectId,
command.discoveryId,
);
assertClaimEvidence(command, enrollment, discovery);
const deviceId = randomUUID();
const inserted = await client.query(
`insert into device_instances (
id,
contour_id,
owner_scope_id,
project_id,
device_key,
model_profile_ref,
display_name,
identifier_kind,
identifier_digest,
identifier_masked,
lifecycle_state
) values ($1, null, $2, $3, $4, $5, $6, $7, $8, $9, 'claimed')
returning id, owner_scope_id, project_id, device_key,
model_profile_ref, display_name, integration_device_id, identifier_kind,
identifier_masked, lifecycle_state, created_at, updated_at`,
[
deviceId,
project.owner_scope_id,
project.id,
command.deviceKey,
discovery.model_profile_ref,
command.displayName,
discovery.identifier_kind,
discovery.identifier_digest,
discovery.identifier_masked,
],
);
const device = inserted.rows[0];
if (!device) throw domainError("device_claim_insert_failed", 409);
const identifierId = randomUUID();
await client.query(
`insert into device_restricted_identifiers (
id,
device_id,
owner_scope_id,
project_id,
identifier_kind,
identifier_digest,
identifier_masked,
provenance_kind,
is_primary,
created_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, 'claim', true, $8)`,
[
identifierId,
device.id,
project.owner_scope_id,
project.id,
discovery.identifier_kind,
discovery.identifier_digest,
discovery.identifier_masked,
actor.userRef,
],
);
const claimedDiscovery = await client.query(
`update device_discoveries
set lifecycle_state = 'claimed',
claimed_device_id = $2,
claimed_at = now(),
claimed_by = $3,
resolution_code = 'claimed',
resolved_at = now(),
resolved_by_ref = $3,
updated_at = now()
where id = $1
and lifecycle_state = 'quarantine'
and enrollment_intent_id = $4
returning id`,
[discovery.id, device.id, actor.userRef, enrollment.id],
);
if (!claimedDiscovery.rows[0]) {
throw domainError("device_discovery_not_claimable", 409);
}
const claimedEnrollment = await client.query(
`update device_enrollment_intents
set lifecycle_state = 'claimed',
claimed_device_id = $2,
claimed_at = now(),
resolution_code = 'claimed',
resolved_at = now(),
resolved_by_ref = $3,
updated_at = now()
where id = $1
and lifecycle_state = 'observed'
and observed_discovery_id = $4
and (expires_at is null or expires_at > now())
returning id`,
[enrollment.id, device.id, actor.userRef, discovery.id],
);
if (!claimedEnrollment.rows[0]) {
throw domainError("device_enrollment_not_claimable", 409);
}
const transitionId = randomUUID();
await client.query(
`insert into device_ownership_transitions (
id,
device_id,
transition_kind,
target_owner_scope_id,
target_project_id,
actor_ref
) values ($1, $2, 'claim', $3, $4, $5)`,
[
transitionId,
device.id,
project.owner_scope_id,
project.id,
actor.userRef,
],
);
await addAudit(client, {
eventType: "device.claimed",
actorRef: actor.userRef,
projectId: project.id,
deviceId: device.id,
discoveryId: discovery.id,
payload: {
deviceRef: `device:${device.id}`,
projectRef: toProjectRef(project.id),
enrollmentIntentRef: `enrollment-intent:${enrollment.id}`,
discoveryRef: `discovery:${discovery.id}`,
ownershipTransitionRef: `ownership-transition:${transitionId}`,
identifierRef: `identifier:${identifierId}`,
modelProfileRef: device.model_profile_ref,
},
});
return {
created: true,
device: deviceView(device, project),
enrollmentIntentRef: `enrollment-intent:${enrollment.id}`,
discoveryRef: `discovery:${discovery.id}`,
ownershipTransitionRef: `ownership-transition:${transitionId}`,
identifierRef: `identifier:${identifierId}`,
};
}
async function resolveDiscovery(client, actor, command, commandKind) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"device.claim",
);
const discovery = await findDiscoveryForUpdate(
client,
command.projectId,
command.discoveryId,
);
if (
discovery.lifecycle_state !== "quarantine"
|| !discovery.enrollment_intent_id
) {
throw domainError("device_discovery_not_resolvable", 409);
}
const enrollment = await findEnrollmentForUpdate(
client,
command.projectId,
discovery.enrollment_intent_id,
);
if (
enrollment.lifecycle_state !== "observed"
|| enrollment.observed_discovery_id !== discovery.id
) {
throw domainError("device_enrollment_not_resolvable", 409);
}
const discoveryState = commandKind === "discovery.reject"
? "rejected"
: "expired";
const enrollmentState = commandKind === "discovery.reject"
? "cancelled"
: "expired";
await client.query(
`update device_discoveries
set lifecycle_state = $2,
resolution_code = $3,
resolved_at = now(),
resolved_by_ref = $4,
updated_at = now()
where id = $1 and lifecycle_state = 'quarantine'`,
[discovery.id, discoveryState, command.resolutionCode, actor.userRef],
);
await client.query(
`update device_enrollment_intents
set lifecycle_state = $2,
resolution_code = $3,
resolved_at = now(),
resolved_by_ref = $4,
updated_at = now()
where id = $1 and lifecycle_state = 'observed'`,
[enrollment.id, enrollmentState, command.resolutionCode, actor.userRef],
);
await addAudit(client, {
eventType: `discovery.${discoveryState}`,
actorRef: actor.userRef,
projectId: project.id,
discoveryId: discovery.id,
payload: {
projectRef: toProjectRef(project.id),
discoveryRef: `discovery:${discovery.id}`,
enrollmentIntentRef: `enrollment-intent:${enrollment.id}`,
lifecycleState: discoveryState,
resolutionCode: command.resolutionCode,
},
});
return {
discovery: {
discoveryRef: `discovery:${discovery.id}`,
projectRef: toProjectRef(project.id),
enrollmentIntentRef: `enrollment-intent:${enrollment.id}`,
lifecycleState: discoveryState,
identifier: {
kind: discovery.identifier_kind,
masked: discovery.identifier_masked,
},
resolutionCode: command.resolutionCode,
},
};
}
async function transferDevice(client, actor, command) {
const device = await findDeviceForUpdate(client, command.deviceId);
if (device.project_id !== command.sourceProjectId) {
throw domainError("device_transfer_source_mismatch", 409);
}
if (!device.owner_scope_id || !device.project_id || device.contour_id) {
throw domainError("device_transfer_legacy_ownership_unsupported", 409);
}
if (["online", "retired"].includes(device.lifecycle_state)) {
throw domainError("device_transfer_lifecycle_blocked", 409);
}
const sourceProject = await findProjectWithCapability(
client,
actor,
command.sourceProjectId,
"device.transfer",
);
const targetProject = await findProjectWithCapability(
client,
actor,
command.targetProjectId,
"device.transfer",
);
if (sourceProject.owner_scope_id !== device.owner_scope_id) {
throw domainError("device_transfer_owner_mismatch", 409);
}
const activeSessions = await client.query(
`select exists (
select 1 from device_sessions
where device_id = $1
and lifecycle_state in ('connecting', 'online', 'closing')
) as active`,
[device.id],
);
if (activeSessions.rows[0]?.active === true) {
throw domainError("device_transfer_active_session", 409);
}
const activeCredentialBindings = await client.query(
`select exists (
select 1 from device_credential_bindings
where device_id = $1 and lifecycle_state = 'active'
) as active`,
[device.id],
);
if (activeCredentialBindings.rows[0]?.active === true) {
throw domainError("device_transfer_active_credential_binding", 409);
}
const activeResourceBindings = await client.query(
`select exists (
select 1 from device_resource_bindings
where device_id = $1
and lifecycle_state in ('pending_external_approval', 'active')
) as active`,
[device.id],
);
if (activeResourceBindings.rows[0]?.active === true) {
throw domainError("device_transfer_active_resource_binding", 409);
}
const configurationState = await client.query(
`select desired_revision_id, applied_revision_id
from device_configuration_state
where device_id = $1
for update`,
[device.id],
);
if (configurationState.rows[0]?.applied_revision_id) {
throw domainError("device_transfer_applied_configuration", 409);
}
const nonterminalCommands = await client.query(
`select exists (
select 1 from device_commands
where device_id = $1
and lifecycle_state not in ('verified', 'failed', 'expired', 'unknown')
) as active`,
[device.id],
);
if (nonterminalCommands.rows[0]?.active === true) {
throw domainError("device_transfer_nonterminal_command", 409);
}
const clearedConfiguration = await client.query(
`delete from device_configuration_state
where device_id = $1 and applied_revision_id is null`,
[device.id],
);
const detached = await client.query(
`delete from device_collection_members
where device_id = $1 and project_id = $2`,
[device.id, sourceProject.id],
);
const updated = await client.query(
`update device_instances
set owner_scope_id = $2,
project_id = $3,
device_key = $4,
updated_at = now()
where id = $1
returning id, contour_id, owner_scope_id, project_id, device_key,
model_profile_ref, display_name, integration_device_id, identifier_kind,
identifier_masked, lifecycle_state, created_at, updated_at`,
[
device.id,
targetProject.owner_scope_id,
targetProject.id,
command.targetDeviceKey,
],
);
const moved = updated.rows[0];
if (!moved) throw domainError("device_transfer_update_failed", 409);
const movedIdentifiers = await client.query(
`update device_restricted_identifiers
set owner_scope_id = $2,
project_id = $3,
updated_at = now()
where device_id = $1 and lifecycle_state = 'active'`,
[device.id, targetProject.owner_scope_id, targetProject.id],
);
if (Number(movedIdentifiers.rowCount || 0) < 1) {
throw domainError("device_identifier_projection_missing", 409);
}
const transitionId = randomUUID();
await client.query(
`insert into device_ownership_transitions (
id,
device_id,
transition_kind,
source_owner_scope_id,
source_project_id,
target_owner_scope_id,
target_project_id,
actor_ref
) values ($1, $2, 'transfer', $3, $4, $5, $6, $7)`,
[
transitionId,
device.id,
sourceProject.owner_scope_id,
sourceProject.id,
targetProject.owner_scope_id,
targetProject.id,
actor.userRef,
],
);
const auditPayload = {
deviceRef: `device:${device.id}`,
ownershipTransitionRef: `ownership-transition:${transitionId}`,
sourceProjectRef: toProjectRef(sourceProject.id),
targetProjectRef: toProjectRef(targetProject.id),
detachedCollectionCount: Number(detached.rowCount || 0),
transferredIdentifierCount: Number(movedIdentifiers.rowCount || 0),
clearedDesiredConfiguration: Number(clearedConfiguration.rowCount || 0) > 0,
};
await addAudit(client, {
eventType: "device.transferred_out",
actorRef: actor.userRef,
projectId: sourceProject.id,
deviceId: device.id,
payload: auditPayload,
});
await addAudit(client, {
eventType: "device.transferred_in",
actorRef: actor.userRef,
projectId: targetProject.id,
deviceId: device.id,
payload: auditPayload,
});
return {
transferred: true,
device: deviceView(moved, targetProject),
sourceProjectRef: toProjectRef(sourceProject.id),
ownershipTransitionRef: `ownership-transition:${transitionId}`,
detachedCollectionCount: Number(detached.rowCount || 0),
transferredIdentifierCount: Number(movedIdentifiers.rowCount || 0),
clearedDesiredConfiguration: Number(clearedConfiguration.rowCount || 0) > 0,
};
}
export async function findProjectWithCapability(
client,
actor,
projectId,
capability,
{ lock = true } = {},
) {
const projectLockClause = lock ? "for share of p, os" : "";
const result = await client.query(
`select p.id, p.owner_scope_id, p.project_key, p.name, p.description,
p.lifecycle_state, p.created_at, p.updated_at,
os.scope_kind, os.owner_ref, os.display_name as owner_display_name,
os.lifecycle_state as owner_lifecycle_state
from device_projects p
join device_owner_scopes os on os.id = p.owner_scope_id
where p.id = $1
${projectLockClause}`,
[projectId],
);
const project = result.rows[0];
if (!project) throw domainError("device_project_not_found", 404);
if (project.owner_lifecycle_state !== "active") {
throw domainError("device_owner_scope_inactive", 409);
}
if (project.lifecycle_state !== "active") {
throw domainError("device_project_inactive", 409);
}
const grants = await client.query(
`select id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state
from device_project_grants
where project_id = $1
order by created_at, id
${lock ? "for share" : ""}`,
[projectId],
);
assertProjectCapability(
actor,
grants.rows.map((grant) => ({
grantRef: `grant:${grant.id}`,
principalKind: grant.principal_kind,
principalRef: grant.principal_ref,
projectRole: grant.project_role,
capabilityAllow: grant.capability_allow ?? [],
capabilityDeny: grant.capability_deny ?? [],
lifecycleState: grant.lifecycle_state,
})),
capability,
);
return project;
}
async function findEnrollmentForUpdate(client, projectId, enrollmentId) {
const result = await client.query(
`select id, project_id, route_id, model_profile_ref,
expected_identifier_kind, expected_identifier_digest,
expected_identifier_masked, lifecycle_state,
observed_discovery_id, claimed_device_id, expires_at
from device_enrollment_intents
where id = $1 and project_id = $2
for update`,
[enrollmentId, projectId],
);
if (!result.rows[0]) {
throw domainError("device_enrollment_intent_not_found", 404);
}
return result.rows[0];
}
async function findDiscoveryForUpdate(client, projectId, discoveryId) {
const result = await client.query(
`select id, project_id, route_id, enrollment_intent_id,
model_profile_ref, protocol, identifier_kind, identifier_digest,
identifier_masked, lifecycle_state, claimed_device_id
from device_discoveries
where id = $1 and project_id = $2
for update`,
[discoveryId, projectId],
);
if (!result.rows[0]) throw domainError("device_discovery_not_found", 404);
return result.rows[0];
}
async function findDeviceForUpdate(client, deviceId) {
const result = await client.query(
`select id, contour_id, owner_scope_id, project_id, device_key,
model_profile_ref, display_name, integration_device_id, identifier_kind,
identifier_masked, lifecycle_state, created_at, updated_at
from device_instances
where id = $1
for update`,
[deviceId],
);
if (!result.rows[0]) throw domainError("device_not_found", 404);
return result.rows[0];
}
function assertClaimEvidence(command, enrollment, discovery) {
if (
enrollment.lifecycle_state !== "observed"
|| enrollment.observed_discovery_id !== discovery.id
|| discovery.lifecycle_state !== "quarantine"
|| discovery.enrollment_intent_id !== enrollment.id
|| discovery.project_id !== command.projectId
|| discovery.route_id !== enrollment.route_id
|| discovery.model_profile_ref !== enrollment.model_profile_ref
|| discovery.identifier_kind !== enrollment.expected_identifier_kind
|| discovery.identifier_digest !== enrollment.expected_identifier_digest
|| discovery.identifier_masked !== enrollment.expected_identifier_masked
) {
throw domainError("device_claim_evidence_mismatch", 409);
}
}
async function addAudit(client, {
eventType,
actorRef,
projectId,
deviceId = null,
discoveryId = null,
payload,
}) {
await client.query(
`insert into device_audit_events (
id,
event_type,
actor_ref,
project_id,
device_id,
discovery_id,
payload
) values ($1, $2, $3, $4, $5, $6, $7::jsonb)`,
[
randomUUID(),
eventType,
actorRef,
projectId,
deviceId,
discoveryId,
JSON.stringify(payload),
],
);
}
function deviceView(row, project) {
return {
deviceRef: `device:${row.id}`,
deviceKey: row.device_key,
projectRef: toProjectRef(row.project_id),
ownerScope: {
ownerScopeRef: `owner-scope:${row.owner_scope_id}`,
scopeKind: project.scope_kind,
ownerRef: project.owner_ref,
},
modelProfileRef: row.model_profile_ref,
displayName: row.display_name,
integrationDeviceId: row.integration_device_id ?? null,
identifier: normalizeRestrictedIdentifierProjection({
kind: row.identifier_kind,
masked: row.identifier_masked,
}),
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function toIso(value) {
return new Date(value).toISOString();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,48 @@
import {
DEVICE_INFRASTRUCTURE_COMMAND_KINDS,
isInfrastructureManagementCommand,
normalizeInfrastructureManagementCommand,
} from "./infrastructure-management.mjs";
import {
DEVICE_CONTROL_RESOURCE_COMMAND_KINDS,
isControlResourceManagementCommand,
normalizeControlResourceManagementCommand,
} from "./control-resource-management.mjs";
import {
DEVICE_LIFECYCLE_COMMAND_KINDS,
isLifecycleManagementCommand,
normalizeLifecycleManagementCommand,
} from "./lifecycle-management.mjs";
import {
DEVICE_MANAGEMENT_COMMAND_KINDS,
normalizeManagementCommand,
} from "./project-management.mjs";
import {
DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS,
isSensitiveReferenceManagementCommand,
normalizeSensitiveReferenceManagementCommand,
} from "./sensitive-reference-management.mjs";
export const ALL_DEVICE_MANAGEMENT_COMMAND_KINDS = Object.freeze([
...DEVICE_MANAGEMENT_COMMAND_KINDS,
...DEVICE_INFRASTRUCTURE_COMMAND_KINDS,
...DEVICE_LIFECYCLE_COMMAND_KINDS,
...DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS,
...DEVICE_CONTROL_RESOURCE_COMMAND_KINDS,
]);
export function normalizeDeviceManagementCommand(kind, input) {
if (isControlResourceManagementCommand(kind)) {
return normalizeControlResourceManagementCommand(kind, input);
}
if (isSensitiveReferenceManagementCommand(kind)) {
return normalizeSensitiveReferenceManagementCommand(kind, input);
}
if (isLifecycleManagementCommand(kind)) {
return normalizeLifecycleManagementCommand(kind, input);
}
if (isInfrastructureManagementCommand(kind)) {
return normalizeInfrastructureManagementCommand(kind, input);
}
return normalizeManagementCommand(kind, input);
}
@@ -0,0 +1,916 @@
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
import { observeQuarantineDiscovery } from "./discovery-repository.mjs";
import { acceptGatewayMessage } from "./gateway-message-repository.mjs";
import { resolveInboundRoute } from "./inbound-route-repository.mjs";
import {
applyControlResourceManagementCommand,
authorizeControlResourceManagementReplay,
} from "./control-resource-repository.mjs";
import {
isControlResourceManagementCommand,
} from "./control-resource-management.mjs";
import {
getDeviceProjectWorkspace,
listAccessibleDeviceProjects,
} from "./project-query-repository.mjs";
import {
applyInfrastructureManagementCommand,
authorizeInfrastructureManagementReplay,
} from "./infrastructure-repository.mjs";
import { isInfrastructureManagementCommand } from "./infrastructure-management.mjs";
import { isLifecycleManagementCommand } from "./lifecycle-management.mjs";
import {
applyLifecycleManagementCommand,
authorizeLifecycleManagementReplay,
} from "./lifecycle-repository.mjs";
import {
applySensitiveReferenceManagementCommand,
authorizeSensitiveReferenceManagementReplay,
} from "./sensitive-reference-repository.mjs";
import {
isSensitiveReferenceManagementCommand,
} from "./sensitive-reference-management.mjs";
import {
assertActorCanManageOwnerScope,
assertGrantMutationAllowed,
assertProjectCapability,
toProjectRef,
} from "./project-management.mjs";
import {
dispatchTypedCommand,
planTypedServicePing,
recordTypedCommandStatus,
} from "./typed-command-repository.mjs";
const { Pool } = pg;
const serviceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const migrationFiles = [
"001_device_plane_foundation.sql",
"002_device_project_access.sql",
"003_device_management_commands.sql",
"004_device_registry_foundation.sql",
"005_device_registry_commands.sql",
"006_device_lifecycle_ownership.sql",
"007_device_lifecycle_commands.sql",
"008_device_sensitive_references.sql",
"009_device_sensitive_reference_commands.sql",
"010_device_control_resources.sql",
"011_device_control_resource_commands.sql",
"012_device_gateway_message_receipts.sql",
"013_device_edge_channels.sql",
"014_device_registry_profile_commands.sql",
"015_device_integration_identity.sql",
];
export class PostgresDeviceRepository {
constructor({ databaseUrl, poolSize = 10, pool = null } = {}) {
if (pool) {
if (
typeof pool.query !== "function" ||
typeof pool.connect !== "function" ||
typeof pool.end !== "function"
) {
throw new TypeError("device_database_pool_invalid");
}
this.pool = pool;
return;
}
if (typeof databaseUrl !== "string" || databaseUrl.trim() === "") {
throw new TypeError("device_database_url_required");
}
this.pool = new Pool({
connectionString: databaseUrl,
max: normalizePoolSize(poolSize),
});
}
async migrate() {
for (const migrationFile of migrationFiles) {
const sql = await readFile(
resolve(serviceRoot, "migrations", migrationFile),
"utf8",
);
await this.pool.query(sql);
}
}
async health() {
await this.pool.query("select 1");
return "ready";
}
async upsertQuarantineDiscovery(input) {
return observeQuarantineDiscovery({
pool: this.pool,
...input,
});
}
async acceptAdapterMessage(input) {
return acceptGatewayMessage({
pool: this.pool,
...input,
});
}
async resolveInboundRoute(input) {
return this.#executeRead((client) => resolveInboundRoute(client, input));
}
async executeManagementCommand({
idempotencyKey,
commandKind,
requestDigest,
actor,
command,
}) {
const client = await this.pool.connect();
try {
await client.query("begin");
const receipt = await claimManagementReceipt(client, {
idempotencyKey,
commandKind,
requestDigest,
actorRef: actor.userRef,
});
if (receipt.replayed) {
await authorizeManagementReplay(client, {
commandKind,
actor,
command,
});
await client.query("commit");
return {
replayed: true,
result: receipt.responseBody,
};
}
const result = await applyManagementCommand(client, {
commandKind,
actor,
command,
});
await completeManagementReceipt(client, receipt.id, result);
await client.query("commit");
return { replayed: false, result };
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw mapPostgresError(error);
} finally {
client.release();
}
}
async listAccessibleProjects(actor) {
return this.#executeRead((client) =>
listAccessibleDeviceProjects(client, actor)
);
}
async getProjectWorkspace(actor, projectId, options) {
return this.#executeRead((client) =>
getDeviceProjectWorkspace(client, actor, projectId, options)
);
}
async listActiveEdgeChannelRegistrations(limit = 64) {
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 64) {
throw new TypeError("device_edge_channel_registration_limit_invalid");
}
return this.#executeRead(async (client) => {
const result = await client.query(
`select id, channel_endpoint, channel_servername,
channel_generation_ref, channel_trust_bundle_ref,
channel_certificate_identities, channel_lifecycle_state
from device_edges
where lifecycle_state = 'active'
and channel_lifecycle_state = 'active'
order by id
limit $1`,
[limit],
);
return result.rows.map((row) => Object.freeze({
edgeRegistrationId: `edge:${row.id}`,
endpoint: row.channel_endpoint,
servername: row.channel_servername,
channelGeneration: row.channel_generation_ref,
trustBundleRef: row.channel_trust_bundle_ref,
certificateIdentities: Object.freeze(
[...(row.channel_certificate_identities ?? [])],
),
lifecycleState: row.channel_lifecycle_state,
}));
});
}
async planTypedServicePing(input) {
return this.#executeWrite((client) => planTypedServicePing(client, input));
}
async dispatchTypedCommand(input) {
return this.#executeWrite((client) => dispatchTypedCommand(client, input));
}
async recordTypedCommandStatus(input) {
return this.#executeWrite((client) => recordTypedCommandStatus(client, input));
}
async #executeWrite(operation) {
const client = await this.pool.connect();
try {
await client.query("begin");
const result = await operation(client);
await client.query("commit");
return result;
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw mapPostgresError(error);
} finally {
client.release();
}
}
async #executeRead(operation) {
const client = await this.pool.connect();
try {
await client.query("begin transaction read only");
const result = await operation(client);
await client.query("commit");
return result;
} catch (error) {
await client.query("rollback").catch(() => undefined);
throw mapPostgresError(error);
} finally {
client.release();
}
}
async close() {
await this.pool.end();
}
}
async function claimManagementReceipt(client, {
idempotencyKey,
commandKind,
requestDigest,
actorRef,
}) {
const id = randomUUID();
const inserted = await client.query(
`insert into device_management_command_receipts (
id,
actor_ref,
command_kind,
idempotency_key,
request_digest
) values ($1, $2, $3, $4, $5)
on conflict (actor_ref, command_kind, idempotency_key) do nothing
returning id`,
[id, actorRef, commandKind, idempotencyKey, requestDigest],
);
if (inserted.rows.length === 1) {
return { id, replayed: false, responseBody: null };
}
const existing = await client.query(
`select id, request_digest, lifecycle_state, response_body
from device_management_command_receipts
where actor_ref = $1
and command_kind = $2
and idempotency_key = $3
for update`,
[actorRef, commandKind, idempotencyKey],
);
const row = existing.rows[0];
if (!row) throw domainError("device_idempotency_receipt_missing", 409);
if (row.request_digest !== requestDigest) {
throw domainError("device_idempotency_key_conflict", 409);
}
if (row.lifecycle_state !== "completed" || !row.response_body) {
throw domainError("device_idempotency_command_in_progress", 409);
}
return {
id: row.id,
replayed: true,
responseBody: row.response_body,
};
}
async function completeManagementReceipt(client, receiptId, result) {
await client.query(
`update device_management_command_receipts
set lifecycle_state = 'completed',
response_status = 200,
response_body = $2::jsonb,
completed_at = now(),
updated_at = now()
where id = $1`,
[receiptId, JSON.stringify(result)],
);
}
async function applyManagementCommand(client, { commandKind, actor, command }) {
if (isControlResourceManagementCommand(commandKind)) {
return applyControlResourceManagementCommand(client, {
commandKind,
actor,
command,
});
}
if (isSensitiveReferenceManagementCommand(commandKind)) {
return applySensitiveReferenceManagementCommand(client, {
commandKind,
actor,
command,
});
}
if (isLifecycleManagementCommand(commandKind)) {
return applyLifecycleManagementCommand(client, {
commandKind,
actor,
command,
});
}
if (isInfrastructureManagementCommand(commandKind)) {
return applyInfrastructureManagementCommand(client, {
commandKind,
actor,
command,
});
}
if (commandKind === "owner_scope.ensure") {
return ensureOwnerScope(client, actor, command);
}
if (commandKind === "project.ensure") {
return ensureProject(client, actor, command);
}
if (commandKind === "collection.ensure") {
return ensureCollection(client, actor, command);
}
if (commandKind === "project_grant.upsert") {
return upsertProjectGrant(client, actor, command);
}
throw new TypeError("device_management_command_kind_invalid");
}
async function authorizeManagementReplay(client, { commandKind, actor, command }) {
if (isControlResourceManagementCommand(commandKind)) {
return authorizeControlResourceManagementReplay(client, {
commandKind,
actor,
command,
});
}
if (isSensitiveReferenceManagementCommand(commandKind)) {
return authorizeSensitiveReferenceManagementReplay(client, {
commandKind,
actor,
command,
});
}
if (isLifecycleManagementCommand(commandKind)) {
return authorizeLifecycleManagementReplay(client, {
commandKind,
actor,
command,
});
}
if (isInfrastructureManagementCommand(commandKind)) {
return authorizeInfrastructureManagementReplay(client, {
commandKind,
actor,
command,
});
}
if (commandKind === "owner_scope.ensure") {
assertActorCanManageOwnerScope(actor, command);
const ownerScope = await findOwnerScope(
client,
command.scopeKind,
command.ownerRef,
);
assertOwnerScopeActive(ownerScope);
return;
}
if (commandKind === "project.ensure") {
const ownerScope = await findOwnerScope(
client,
command.scopeKind,
command.ownerRef,
);
assertOwnerScopeActive(ownerScope);
const project = await findProjectByOwnerAndKey(
client,
ownerScope.id,
command.projectKey,
false,
);
assertProjectActive(project);
const grants = await listProjectGrants(client, project.id);
assertProjectCapability(actor, grants, "project.manage");
return;
}
const lockForGrantMutation = commandKind === "project_grant.upsert";
const { project } = await findProjectContext(
client,
command.projectId,
lockForGrantMutation,
);
assertProjectActive(project);
const grants = await listProjectGrants(client, project.id);
if (commandKind === "collection.ensure") {
assertProjectCapability(actor, grants, "collection.manage");
return;
}
if (commandKind === "project_grant.upsert") {
const existing = grants.find(
(grant) =>
grant.principalKind === command.principalKind &&
grant.principalRef === command.principalRef,
) ?? null;
assertGrantMutationAllowed(actor, grants, command, existing);
return;
}
throw new TypeError("device_management_command_kind_invalid");
}
async function ensureOwnerScope(client, actor, command) {
assertActorCanManageOwnerScope(actor, command);
const result = await client.query(
`insert into device_owner_scopes (
id,
scope_kind,
owner_ref,
display_name,
created_by_ref
) values ($1, $2, $3, $4, $5)
on conflict (scope_kind, owner_ref) do update set
display_name = excluded.display_name,
updated_at = now()
returning id, scope_kind, owner_ref, display_name, lifecycle_state,
created_at, updated_at, (xmax = 0) as created`,
[randomUUID(), command.scopeKind, command.ownerRef, command.displayName, actor.userRef],
);
const row = result.rows[0];
assertOwnerScopeActive(row);
await addManagementAudit(client, {
eventType: row.created ? "owner_scope.created" : "owner_scope.updated",
actorRef: actor.userRef,
payload: {
scopeKind: row.scope_kind,
ownerRef: row.owner_ref,
lifecycleState: row.lifecycle_state,
},
});
return {
created: row.created === true,
ownerScope: projectOwnerScopeView(row),
};
}
async function ensureProject(client, actor, command) {
const ownerScope = await findOwnerScope(client, command.scopeKind, command.ownerRef);
assertOwnerScopeActive(ownerScope);
const inserted = await client.query(
`insert into device_projects (
id,
owner_scope_id,
project_key,
name,
description,
created_by_ref
) values ($1, $2, $3, $4, $5, $6)
on conflict (owner_scope_id, project_key) do nothing
returning id, owner_scope_id, project_key, name, description,
lifecycle_state, created_at, updated_at`,
[
randomUUID(),
ownerScope.id,
command.projectKey,
command.name,
command.description,
actor.userRef,
],
);
let row = inserted.rows[0];
const created = Boolean(row);
if (created) {
assertActorCanManageOwnerScope(actor, command);
await client.query(
`insert into device_project_grants (
id,
project_id,
principal_kind,
principal_ref,
project_role,
capability_allow,
capability_deny,
lifecycle_state,
created_by_ref
) values ($1, $2, 'user', $3, 'owner', '{}', '{}', 'active', $3)`,
[randomUUID(), row.id, actor.userRef],
);
} else {
row = await findProjectByOwnerAndKey(
client,
ownerScope.id,
command.projectKey,
true,
);
assertProjectActive(row);
const grants = await listProjectGrants(client, row.id);
assertProjectCapability(actor, grants, "project.manage");
const updated = await client.query(
`update device_projects
set name = $2,
description = $3,
updated_at = now()
where id = $1
returning id, owner_scope_id, project_key, name, description,
lifecycle_state, created_at, updated_at`,
[row.id, command.name, command.description],
);
row = updated.rows[0];
}
await addManagementAudit(client, {
eventType: created ? "project.created" : "project.updated",
actorRef: actor.userRef,
projectId: row.id,
payload: {
projectRef: toProjectRef(row.id),
projectKey: row.project_key,
ownerScope: {
scopeKind: ownerScope.scope_kind,
ownerRef: ownerScope.owner_ref,
},
},
});
return {
created,
project: projectView(row, ownerScope),
...(created
? {
initialGrant: {
principalKind: "user",
principalRef: actor.userRef,
projectRole: "owner",
lifecycleState: "active",
},
}
: {}),
};
}
async function ensureCollection(client, actor, command) {
const { project, ownerScope } = await findProjectContext(client, command.projectId);
assertProjectActive(project);
const grants = await listProjectGrants(client, project.id);
assertProjectCapability(actor, grants, "collection.manage");
const result = await client.query(
`insert into device_collections (
id,
project_id,
collection_key,
name,
description,
created_by_ref
) values ($1, $2, $3, $4, $5, $6)
on conflict (project_id, collection_key) do update set
name = excluded.name,
description = excluded.description,
updated_at = now()
returning id, project_id, collection_key, name, description,
lifecycle_state, created_at, updated_at, (xmax = 0) as created`,
[
randomUUID(),
project.id,
command.collectionKey,
command.name,
command.description,
actor.userRef,
],
);
const row = result.rows[0];
assertCollectionActive(row);
await addManagementAudit(client, {
eventType: row.created ? "collection.created" : "collection.updated",
actorRef: actor.userRef,
projectId: project.id,
payload: {
projectRef: toProjectRef(project.id),
collectionRef: `collection:${row.id}`,
collectionKey: row.collection_key,
},
});
return {
created: row.created === true,
project: projectView(project, ownerScope),
collection: collectionView(row),
};
}
async function upsertProjectGrant(client, actor, command) {
const { project, ownerScope } = await findProjectContext(
client,
command.projectId,
true,
);
assertProjectActive(project);
const grants = await listProjectGrants(client, project.id);
const existing = grants.find(
(grant) =>
grant.principalKind === command.principalKind &&
grant.principalRef === command.principalRef,
) ?? null;
assertGrantMutationAllowed(actor, grants, command, existing);
const removesActiveOwner =
existing?.projectRole === "owner" &&
existing.lifecycleState === "active" &&
(command.projectRole !== "owner" || command.lifecycleState !== "active");
if (removesActiveOwner) {
const remaining = grants.filter(
(grant) =>
grant.grantRef !== existing.grantRef &&
grant.projectRole === "owner" &&
grant.lifecycleState === "active" &&
grant.principalKind === "user",
);
if (remaining.length === 0) {
throw domainError("device_project_last_owner_required", 409);
}
}
const result = await client.query(
`insert into device_project_grants (
id,
project_id,
principal_kind,
principal_ref,
project_role,
capability_allow,
capability_deny,
lifecycle_state,
created_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
on conflict (project_id, principal_kind, principal_ref) do update set
project_role = excluded.project_role,
capability_allow = excluded.capability_allow,
capability_deny = excluded.capability_deny,
lifecycle_state = excluded.lifecycle_state,
updated_at = now()
returning id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state, created_at,
updated_at, (xmax = 0) as created`,
[
randomUUID(),
project.id,
command.principalKind,
command.principalRef,
command.projectRole,
command.capabilityAllow,
command.capabilityDeny,
command.lifecycleState,
actor.userRef,
],
);
const row = result.rows[0];
await addManagementAudit(client, {
eventType: row.created ? "project_grant.created" : "project_grant.updated",
actorRef: actor.userRef,
projectId: project.id,
payload: {
projectRef: toProjectRef(project.id),
grantRef: `grant:${row.id}`,
principalKind: row.principal_kind,
principalRef: row.principal_ref,
projectRole: row.project_role,
lifecycleState: row.lifecycle_state,
},
});
return {
created: row.created === true,
project: projectView(project, ownerScope),
grant: grantView(row),
};
}
async function findOwnerScope(client, scopeKind, ownerRef) {
const result = await client.query(
`select id, scope_kind, owner_ref, display_name, lifecycle_state,
created_at, updated_at
from device_owner_scopes
where scope_kind = $1 and owner_ref = $2
for share`,
[scopeKind, ownerRef],
);
if (!result.rows[0]) throw domainError("device_owner_scope_not_found", 404);
return result.rows[0];
}
async function findProjectByOwnerAndKey(
client,
ownerScopeId,
projectKey,
forUpdate = false,
) {
const lockClause = forUpdate ? "for update" : "for share";
const result = await client.query(
`select id, owner_scope_id, project_key, name, description,
lifecycle_state, created_at, updated_at
from device_projects
where owner_scope_id = $1 and project_key = $2
${lockClause}`,
[ownerScopeId, projectKey],
);
if (!result.rows[0]) throw domainError("device_project_not_found", 404);
return result.rows[0];
}
async function findProjectContext(client, projectId, forUpdate = false) {
const lockClause = forUpdate ? "for update of p" : "for share of p, os";
const result = await client.query(
`select
p.id,
p.owner_scope_id,
p.project_key,
p.name,
p.description,
p.lifecycle_state,
p.created_at,
p.updated_at,
os.scope_kind,
os.owner_ref,
os.display_name as owner_display_name,
os.lifecycle_state as owner_lifecycle_state,
os.created_at as owner_created_at,
os.updated_at as owner_updated_at
from device_projects p
join device_owner_scopes os on os.id = p.owner_scope_id
where p.id = $1
${lockClause}`,
[projectId],
);
const row = result.rows[0];
if (!row) throw domainError("device_project_not_found", 404);
assertOwnerScopeActive({ lifecycle_state: row.owner_lifecycle_state });
return {
project: row,
ownerScope: {
id: row.owner_scope_id,
scope_kind: row.scope_kind,
owner_ref: row.owner_ref,
display_name: row.owner_display_name,
lifecycle_state: row.owner_lifecycle_state,
created_at: row.owner_created_at,
updated_at: row.owner_updated_at,
},
};
}
async function listProjectGrants(client, projectId) {
const result = await client.query(
`select id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state,
created_at, updated_at
from device_project_grants
where project_id = $1
order by created_at, id
for share`,
[projectId],
);
return result.rows.map(grantView);
}
async function addManagementAudit(client, {
eventType,
actorRef,
projectId = null,
payload,
}) {
await client.query(
`insert into device_audit_events (
id,
event_type,
actor_ref,
project_id,
payload
) values ($1, $2, $3, $4, $5::jsonb)`,
[randomUUID(), eventType, actorRef, projectId, JSON.stringify(payload)],
);
}
function projectOwnerScopeView(row) {
return {
ownerScopeRef: `owner-scope:${row.id}`,
scopeKind: row.scope_kind,
ownerRef: row.owner_ref,
displayName: row.display_name,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function projectView(row, ownerScope) {
return {
projectRef: toProjectRef(row.id),
ownerScope: projectOwnerScopeView(ownerScope),
projectKey: row.project_key,
name: row.name,
description: row.description ?? null,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function collectionView(row) {
return {
collectionRef: `collection:${row.id}`,
projectRef: toProjectRef(row.project_id),
collectionKey: row.collection_key,
name: row.name,
description: row.description ?? null,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function grantView(row) {
return {
grantRef: `grant:${row.id}`,
principalKind: row.principal_kind,
principalRef: row.principal_ref,
projectRole: row.project_role,
capabilityAllow: [...(row.capability_allow ?? [])].sort(),
capabilityDeny: [...(row.capability_deny ?? [])].sort(),
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function assertOwnerScopeActive(ownerScope) {
if (ownerScope.lifecycle_state !== "active") {
throw domainError("device_owner_scope_inactive", 409);
}
}
function assertProjectActive(project) {
if (project.lifecycle_state !== "active") {
throw domainError("device_project_inactive", 409);
}
}
function assertCollectionActive(collection) {
if (collection.lifecycle_state !== "active") {
throw domainError("device_collection_inactive", 409);
}
}
function toIso(value) {
return new Date(value).toISOString();
}
function mapPostgresError(error) {
if (error?.statusCode) return error;
if (error?.code === "23503") {
return domainError("device_management_reference_invalid", 409);
}
if (error?.code === "23505") {
return domainError("device_management_identity_conflict", 409);
}
if (error?.code === "23514") {
return domainError("device_management_constraint_failed", 400);
}
return error;
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
function normalizePoolSize(value) {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 50) {
throw new TypeError("device_database_pool_size_invalid");
}
return parsed;
}
@@ -0,0 +1,538 @@
export const DEVICE_PROJECT_CAPABILITIES = Object.freeze([
"project.read",
"project.manage",
"access.manage",
"inventory.read",
"device.enroll",
"device.claim",
"device.transfer",
"collection.manage",
"route.manage",
"binding.manage",
"telemetry.observe",
"configuration.read",
"configuration.manage",
"command.plan",
"command.confirm",
"command.dispatch",
"credential.manage",
"audit.read",
]);
export const DEVICE_PROJECT_ROLES = Object.freeze([
"viewer",
"operator",
"engineer",
"admin",
"owner",
]);
export const DEVICE_HUB_ROLES = Object.freeze([
"viewer",
"member",
"admin",
"owner",
]);
export const DEVICE_MANAGEMENT_COMMAND_KINDS = Object.freeze([
"owner_scope.ensure",
"project.ensure",
"collection.ensure",
"project_grant.upsert",
]);
const capabilitySet = new Set(DEVICE_PROJECT_CAPABILITIES);
const projectRoleSet = new Set(DEVICE_PROJECT_ROLES);
const hubRoleSet = new Set(DEVICE_HUB_ROLES);
const commandKindSet = new Set(DEVICE_MANAGEMENT_COMMAND_KINDS);
const opaqueRefPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,255}$/;
const keyPattern = /^[a-z][a-z0-9-]{1,62}$/;
const projectRefPattern = /^project:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i;
const roleCapabilities = Object.freeze({
viewer: Object.freeze([
"project.read",
"inventory.read",
"telemetry.observe",
"configuration.read",
"audit.read",
]),
operator: Object.freeze([
"project.read",
"inventory.read",
"telemetry.observe",
"configuration.read",
"command.plan",
"command.confirm",
"command.dispatch",
"audit.read",
]),
engineer: Object.freeze([
"project.read",
"inventory.read",
"device.enroll",
"device.claim",
"collection.manage",
"route.manage",
"binding.manage",
"telemetry.observe",
"configuration.read",
"configuration.manage",
"command.plan",
"audit.read",
]),
admin: Object.freeze([
"project.read",
"project.manage",
"access.manage",
"inventory.read",
"device.enroll",
"device.claim",
"collection.manage",
"route.manage",
"binding.manage",
"telemetry.observe",
"configuration.read",
"configuration.manage",
"command.plan",
"command.confirm",
"command.dispatch",
"credential.manage",
"audit.read",
]),
owner: DEVICE_PROJECT_CAPABILITIES,
});
const hubRoleCeilings = Object.freeze({
viewer: roleCapabilities.viewer,
member: Object.freeze([
...new Set([
...roleCapabilities.viewer,
...roleCapabilities.operator,
...roleCapabilities.engineer,
]),
]),
admin: roleCapabilities.admin,
owner: DEVICE_PROJECT_CAPABILITIES,
});
const projectRoleWeight = Object.freeze({
viewer: 10,
operator: 20,
engineer: 30,
admin: 40,
owner: 50,
});
export function normalizeManagementActor(input) {
assertPlainObject(input, "device_management_actor_invalid");
assertAllowedKeys(
input,
["userRef", "hubRole", "groupRefs", "ownerScopes"],
"device_management_actor_field_unexpected",
);
const userRef = normalizeOpaqueRef(input.userRef, "device_actor_user_ref_invalid");
const hubRole = normalizeEnum(input.hubRole, hubRoleSet, "device_actor_hub_role_invalid");
const groupRefs = normalizeOpaqueRefArray(
input.groupRefs ?? [],
"device_actor_group_refs_invalid",
);
const ownerScopes = normalizeOwnerScopeClaims(input.ownerScopes ?? []);
return Object.freeze({
userRef,
hubRole,
groupRefs: Object.freeze(groupRefs),
ownerScopes: Object.freeze(ownerScopes),
});
}
export function normalizeManagementCommand(kind, input) {
const normalizedKind = normalizeEnum(
kind,
commandKindSet,
"device_management_command_kind_invalid",
);
assertPlainObject(input, "device_management_command_invalid");
if (normalizedKind === "owner_scope.ensure") {
assertAllowedKeys(
input,
["scopeKind", "ownerRef", "displayName"],
"device_management_command_field_unexpected",
);
return Object.freeze({
scopeKind: normalizeScopeKind(input.scopeKind),
ownerRef: normalizeOpaqueRef(input.ownerRef, "device_owner_ref_invalid"),
displayName: normalizeDisplayText(input.displayName, 160, "device_owner_name_invalid"),
});
}
if (normalizedKind === "project.ensure") {
assertAllowedKeys(
input,
["scopeKind", "ownerRef", "projectKey", "name", "description"],
"device_management_command_field_unexpected",
);
return Object.freeze({
scopeKind: normalizeScopeKind(input.scopeKind),
ownerRef: normalizeOpaqueRef(input.ownerRef, "device_owner_ref_invalid"),
projectKey: normalizeKey(input.projectKey, "device_project_key_invalid"),
name: normalizeDisplayText(input.name, 160, "device_project_name_invalid"),
description: normalizeOptionalText(
input.description,
2000,
"device_project_description_invalid",
),
});
}
if (normalizedKind === "collection.ensure") {
assertAllowedKeys(
input,
["projectRef", "collectionKey", "name", "description"],
"device_management_command_field_unexpected",
);
return Object.freeze({
projectId: normalizeProjectRef(input.projectRef),
collectionKey: normalizeKey(
input.collectionKey,
"device_collection_key_invalid",
),
name: normalizeDisplayText(input.name, 160, "device_collection_name_invalid"),
description: normalizeOptionalText(
input.description,
2000,
"device_collection_description_invalid",
),
});
}
assertAllowedKeys(
input,
[
"projectRef",
"principalKind",
"principalRef",
"projectRole",
"capabilityAllow",
"capabilityDeny",
"lifecycleState",
],
"device_management_command_field_unexpected",
);
const principalKind = normalizeEnum(
input.principalKind,
new Set(["user", "group"]),
"device_project_principal_kind_invalid",
);
const projectRole = normalizeEnum(
input.projectRole,
projectRoleSet,
"device_project_role_invalid",
);
if (projectRole === "owner" && principalKind !== "user") {
throw domainError("device_project_owner_must_be_user", 400);
}
const capabilityAllow = normalizeCapabilities(input.capabilityAllow ?? []);
const capabilityDeny = normalizeCapabilities(input.capabilityDeny ?? []);
if (capabilityAllow.some((capability) => capabilityDeny.includes(capability))) {
throw domainError("device_project_capability_overlap", 400);
}
return Object.freeze({
projectId: normalizeProjectRef(input.projectRef),
principalKind,
principalRef: normalizeOpaqueRef(
input.principalRef,
"device_project_principal_ref_invalid",
),
projectRole,
capabilityAllow: Object.freeze(capabilityAllow),
capabilityDeny: Object.freeze(capabilityDeny),
lifecycleState: normalizeEnum(
input.lifecycleState ?? "active",
new Set(["active", "revoked"]),
"device_project_grant_state_invalid",
),
});
}
export function assertActorCanManageOwnerScope(actorInput, scopeInput) {
const actor = normalizeManagementActor(actorInput);
const scope = {
scopeKind: normalizeScopeKind(scopeInput?.scopeKind),
ownerRef: normalizeOpaqueRef(scopeInput?.ownerRef, "device_owner_ref_invalid"),
};
if (scope.scopeKind === "personal") {
if (
actor.userRef !== scope.ownerRef ||
!["admin", "owner"].includes(actor.hubRole)
) {
throw domainError("device_owner_scope_access_denied", 403);
}
return actor;
}
const hasClaim = actor.ownerScopes.some(
(claim) => claim.scopeKind === "company" && claim.ownerRef === scope.ownerRef,
);
if (!hasClaim || !["admin", "owner"].includes(actor.hubRole)) {
throw domainError("device_owner_scope_access_denied", 403);
}
return actor;
}
export function resolveProjectAccess({ actor: actorInput, grants = [] }) {
const actor = normalizeManagementActor(actorInput);
if (!Array.isArray(grants)) {
throw new TypeError("device_project_grants_invalid");
}
const active = grants
.map(normalizeStoredGrant)
.filter((grant) => grant.lifecycleState === "active");
const direct = active.find(
(grant) => grant.principalKind === "user" && grant.principalRef === actor.userRef,
);
const matching = direct
? [direct]
: active
.filter(
(grant) =>
grant.principalKind === "group" &&
actor.groupRefs.includes(grant.principalRef),
)
.sort(compareGrantPriority);
if (matching.length === 0) {
return Object.freeze({
allowed: false,
projectRole: null,
capabilities: Object.freeze([]),
sourceRefs: Object.freeze([]),
});
}
const primary = matching[0];
const allowed = new Set();
const denied = new Set();
for (const grant of matching) {
for (const capability of roleCapabilities[grant.projectRole]) {
allowed.add(capability);
}
for (const capability of grant.capabilityAllow) allowed.add(capability);
for (const capability of grant.capabilityDeny) denied.add(capability);
}
for (const capability of denied) allowed.delete(capability);
const hubCeiling = new Set(hubRoleCeilings[actor.hubRole]);
const capabilities = [...allowed]
.filter((capability) => hubCeiling.has(capability))
.sort();
if (!capabilities.includes("project.read")) {
return Object.freeze({
allowed: false,
projectRole: primary.projectRole,
capabilities: Object.freeze([]),
sourceRefs: Object.freeze(matching.map((grant) => grant.grantRef)),
});
}
return Object.freeze({
allowed: true,
projectRole: primary.projectRole,
capabilities: Object.freeze(capabilities),
sourceRefs: Object.freeze(matching.map((grant) => grant.grantRef)),
});
}
export function assertProjectCapability(actor, grants, capability) {
if (!capabilitySet.has(capability)) {
throw new TypeError("device_project_capability_invalid");
}
const access = resolveProjectAccess({ actor, grants });
if (!access.capabilities.includes(capability)) {
throw domainError("device_project_capability_denied", 403);
}
return access;
}
export function assertGrantMutationAllowed(actor, grants, command, existingGrant = null) {
const access = assertProjectCapability(actor, grants, "access.manage");
if (
command.projectRole === "owner" ||
existingGrant?.projectRole === "owner"
) {
if (!access.capabilities.includes("device.transfer")) {
throw domainError("device_project_owner_transfer_denied", 403);
}
}
return access;
}
export function toProjectRef(projectId) {
if (typeof projectId !== "string" || !projectRefPattern.test(`project:${projectId}`)) {
throw new TypeError("device_project_id_invalid");
}
return `project:${projectId.toLowerCase()}`;
}
function normalizeStoredGrant(input) {
assertPlainObject(input, "device_project_grant_invalid");
const grant = {
grantRef: normalizeOpaqueRef(input.grantRef, "device_project_grant_ref_invalid"),
principalKind: normalizeEnum(
input.principalKind,
new Set(["user", "group"]),
"device_project_principal_kind_invalid",
),
principalRef: normalizeOpaqueRef(
input.principalRef,
"device_project_principal_ref_invalid",
),
projectRole: normalizeEnum(
input.projectRole,
projectRoleSet,
"device_project_role_invalid",
),
capabilityAllow: normalizeCapabilities(input.capabilityAllow ?? []),
capabilityDeny: normalizeCapabilities(input.capabilityDeny ?? []),
lifecycleState: normalizeEnum(
input.lifecycleState,
new Set(["active", "revoked"]),
"device_project_grant_state_invalid",
),
};
if (grant.projectRole === "owner" && grant.principalKind !== "user") {
throw new TypeError("device_project_owner_must_be_user");
}
return grant;
}
function compareGrantPriority(left, right) {
return (
projectRoleWeight[right.projectRole] - projectRoleWeight[left.projectRole] ||
left.principalRef.localeCompare(right.principalRef)
);
}
function normalizeOwnerScopeClaims(input) {
if (!Array.isArray(input) || input.length > 128) {
throw new TypeError("device_actor_owner_scopes_invalid");
}
const claims = input.map((claim) => {
assertPlainObject(claim, "device_actor_owner_scope_invalid");
assertAllowedKeys(
claim,
["scopeKind", "ownerRef"],
"device_actor_owner_scope_field_unexpected",
);
return {
scopeKind: normalizeScopeKind(claim.scopeKind),
ownerRef: normalizeOpaqueRef(claim.ownerRef, "device_owner_ref_invalid"),
};
});
const byKey = new Map(
claims.map((claim) => [`${claim.scopeKind}\0${claim.ownerRef}`, claim]),
);
return [...byKey.values()].sort((left, right) =>
`${left.scopeKind}:${left.ownerRef}`.localeCompare(
`${right.scopeKind}:${right.ownerRef}`,
),
);
}
function normalizeCapabilities(input) {
if (!Array.isArray(input) || input.length > DEVICE_PROJECT_CAPABILITIES.length) {
throw new TypeError("device_project_capabilities_invalid");
}
const normalized = input.map((capability) =>
normalizeEnum(
capability,
capabilitySet,
"device_project_capability_invalid",
),
);
return [...new Set(normalized)].sort();
}
function normalizeOpaqueRefArray(input, code) {
if (!Array.isArray(input) || input.length > 128) throw new TypeError(code);
return [...new Set(input.map((value) => normalizeOpaqueRef(value, code)))].sort();
}
function normalizeProjectRef(value) {
if (typeof value !== "string") throw new TypeError("device_project_ref_invalid");
const match = value.match(projectRefPattern);
if (!match) throw new TypeError("device_project_ref_invalid");
return match[1].toLowerCase();
}
function normalizeScopeKind(value) {
return normalizeEnum(
value,
new Set(["company", "personal"]),
"device_owner_scope_kind_invalid",
);
}
function normalizeKey(value, code) {
if (typeof value !== "string" || !keyPattern.test(value)) {
throw new TypeError(code);
}
return value;
}
function normalizeOpaqueRef(value, code) {
if (typeof value !== "string" || !opaqueRefPattern.test(value)) {
throw new TypeError(code);
}
return value;
}
function normalizeDisplayText(value, maxLength, code) {
if (typeof value !== "string") throw new TypeError(code);
const normalized = value.trim();
if (normalized.length < 1 || normalized.length > maxLength) {
throw new TypeError(code);
}
if (/\u0000|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(normalized)) {
throw new TypeError(code);
}
return normalized;
}
function normalizeOptionalText(value, maxLength, code) {
if (value == null || value === "") return null;
return normalizeDisplayText(value, maxLength, code);
}
function normalizeEnum(value, allowed, code) {
if (typeof value !== "string" || !allowed.has(value)) {
throw new TypeError(code);
}
return value;
}
function assertAllowedKeys(input, allowed, code) {
const allowedSet = new Set(allowed);
for (const key of Object.keys(input)) {
if (!allowedSet.has(key)) throw new TypeError(`${code}:${key}`);
}
}
function assertPlainObject(value, code) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError(code);
}
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,663 @@
import {
resolveProjectAccess,
toProjectRef,
} from "./project-management.mjs";
import { findProjectWithCapability } from "./lifecycle-repository.mjs";
export async function listAccessibleDeviceProjects(client, actor) {
const result = await client.query(
`select p.id, p.project_key, p.name, p.description,
p.lifecycle_state, p.created_at, p.updated_at,
os.id as owner_scope_id, os.scope_kind, os.owner_ref,
os.display_name as owner_display_name,
g.id as grant_id, g.principal_kind, g.principal_ref,
g.project_role, g.capability_allow, g.capability_deny,
g.lifecycle_state as grant_lifecycle_state,
(select count(*)::bigint from device_instances di
where di.project_id = p.id) as device_count,
(select count(*)::bigint from device_collections dc
where dc.project_id = p.id and dc.lifecycle_state = 'active') as collection_count,
(select count(*)::bigint from device_discoveries dd
where dd.project_id = p.id and dd.lifecycle_state = 'quarantine') as discovery_count
from device_projects p
join device_owner_scopes os on os.id = p.owner_scope_id
join device_project_grants g on g.project_id = p.id
where p.lifecycle_state <> 'archived'
and os.lifecycle_state = 'active'
and g.lifecycle_state = 'active'
and (
(g.principal_kind = 'user' and g.principal_ref = $1)
or (g.principal_kind = 'group' and g.principal_ref = any($2::text[]))
)
order by os.display_name, p.name, g.created_at, g.id`,
[actor.userRef, actor.groupRefs],
);
const projects = new Map();
for (const row of result.rows) {
const entry = projects.get(row.id) ?? { row, grants: [] };
entry.grants.push(grantView(row));
projects.set(row.id, entry);
}
return [...projects.values()].flatMap(({ row, grants }) => {
const access = resolveProjectAccess({ actor, grants });
return access.allowed ? [projectSummaryView(row, access)] : [];
});
}
export async function getDeviceProjectWorkspace(
client,
actor,
projectId,
{ commandTransport = "disabled" } = {},
) {
const project = await findProjectWithCapability(
client,
actor,
projectId,
"project.read",
{ lock: false },
);
const grantsResult = await client.query(
`select id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state
from device_project_grants
where project_id = $1
order by created_at, id`,
[projectId],
);
const access = resolveProjectAccess({
actor,
grants: grantsResult.rows.map(storedGrantView),
});
const devices = await client.query(
`select di.id, di.device_key, di.display_name, di.integration_device_id,
di.model_profile_ref,
di.lifecycle_state, di.created_at, di.updated_at,
identifier.identifier_kind, identifier.identifier_masked,
session.lifecycle_state as session_state,
session.last_seen_at
from device_instances di
left join lateral (
select dri.identifier_kind, dri.identifier_masked
from device_restricted_identifiers dri
where dri.device_id = di.id
and dri.lifecycle_state = 'active'
order by dri.is_primary desc, dri.created_at
limit 1
) identifier on true
left join lateral (
select ds.lifecycle_state, ds.last_seen_at
from device_sessions ds
where ds.device_id = di.id
and ds.lifecycle_state in ('connecting', 'online')
order by ds.last_seen_at desc
limit 1
) session on true
where di.project_id = $1
order by di.display_name, di.id`,
[projectId],
);
const collections = await client.query(
`select dc.id, dc.collection_key, dc.name, dc.description,
dc.lifecycle_state, dc.created_at, dc.updated_at,
count(dcm.device_id)::bigint as member_count
from device_collections dc
left join device_collection_members dcm
on dcm.collection_id = dc.id and dcm.project_id = dc.project_id
where dc.project_id = $1
group by dc.id
order by dc.name, dc.id`,
[projectId],
);
const discoveries = await client.query(
`select dd.id, dd.identifier_kind, dd.identifier_masked,
dd.model_profile_ref, dd.protocol, dd.lifecycle_state,
dd.first_observed_at, dd.last_observed_at,
dd.enrollment_intent_id, dd.claimed_device_id
from device_discoveries dd
where dd.project_id = $1
order by dd.last_observed_at desc, dd.id`,
[projectId],
);
const enrollments = await client.query(
`select dei.id, dei.enrollment_key, dei.display_name,
dei.model_profile_ref, dei.expected_identifier_kind,
dei.expected_identifier_masked, dei.lifecycle_state,
dei.observed_discovery_id, dei.claimed_device_id,
dei.expires_at, dei.created_at, dei.updated_at
from device_enrollment_intents dei
where dei.project_id = $1
order by dei.updated_at desc, dei.id`,
[projectId],
);
const capabilities = new Set(access.capabilities);
const mayManageRoutes = capabilities.has("route.manage");
const catalogParameters = [projectId, mayManageRoutes];
const adapterPackages = await client.query(
`select ap.id, ap.package_key, ap.display_name, ap.publisher_ref,
ap.lifecycle_state, ap.created_at, ap.updated_at
from device_adapter_packages ap
where $2::boolean
or exists (
select 1
from device_adapter_versions av
join device_model_profiles dmp on dmp.adapter_version_id = av.id
where av.adapter_package_id = ap.id
and (
exists (
select 1 from device_routes dr
where dr.project_id = $1 and dr.model_profile_ref = dmp.profile_ref
)
or exists (
select 1 from device_instances di
where di.project_id = $1 and di.model_profile_ref = dmp.profile_ref
)
)
)
order by ap.display_name, ap.id`,
catalogParameters,
);
const adapterVersions = await client.query(
`select av.id, av.adapter_package_id, av.version,
av.runtime_package_ref, av.content_digest, av.contract_version,
av.capabilities, av.lifecycle_state, av.created_at, av.updated_at
from device_adapter_versions av
where $2::boolean
or exists (
select 1 from device_model_profiles dmp
where dmp.adapter_version_id = av.id
and (
exists (
select 1 from device_routes dr
where dr.project_id = $1 and dr.model_profile_ref = dmp.profile_ref
)
or exists (
select 1 from device_instances di
where di.project_id = $1 and di.model_profile_ref = dmp.profile_ref
)
)
)
order by av.adapter_package_id, av.created_at, av.id`,
catalogParameters,
);
const modelProfiles = await client.query(
`select dmp.profile_ref, dmp.adapter_version_id, dmp.schema_version,
dmp.vendor, dmp.model, dmp.device_type, dmp.protocol,
dmp.schema_artifact_ref, dmp.profile_digest, dmp.capabilities,
dmp.lifecycle_state, dmp.created_at, dmp.updated_at
from device_model_profiles dmp
where $2::boolean
or exists (
select 1 from device_routes dr
where dr.project_id = $1 and dr.model_profile_ref = dmp.profile_ref
)
or exists (
select 1 from device_instances di
where di.project_id = $1 and di.model_profile_ref = dmp.profile_ref
)
order by dmp.vendor, dmp.model, dmp.profile_ref`,
catalogParameters,
);
const edges = await client.query(
`select de.id, de.edge_key, de.display_name, de.deployment_ref,
de.lifecycle_state, de.created_at, de.updated_at
from device_edges de
where $2::boolean
or exists (
select 1 from device_routes dr
where dr.project_id = $1 and dr.edge_id = de.id
)
order by de.display_name, de.id`,
catalogParameters,
);
const routes = await client.query(
`select dr.id, dr.route_key, dr.display_name, dr.edge_id,
de.display_name as edge_name, dr.model_profile_ref,
dmp.vendor as profile_vendor, dmp.model as profile_model,
dr.listener_ref, dr.protocol, dr.direction, dr.lifecycle_state,
dr.created_at, dr.updated_at,
count(ds.id)::bigint as session_count,
(count(ds.id) filter (
where ds.lifecycle_state in ('connecting', 'online')
))::bigint as active_session_count
from device_routes dr
join device_edges de on de.id = dr.edge_id
join device_model_profiles dmp on dmp.profile_ref = dr.model_profile_ref
left join device_sessions ds on ds.route_id = dr.id
where dr.project_id = $1
group by dr.id, de.display_name, dmp.vendor, dmp.model
order by dr.display_name, dr.id`,
[projectId],
);
const sessions = capabilities.has("telemetry.observe")
? await client.query(
`select ds.id, ds.route_id, dr.display_name as route_name,
ds.device_id, di.display_name as device_name,
ds.protocol, ds.lifecycle_state, ds.connected_at,
ds.last_seen_at, ds.disconnected_at, ds.close_reason_code,
ds.frame_count, ds.byte_count
from device_sessions ds
join device_routes dr on dr.id = ds.route_id
left join device_instances di on di.id = ds.device_id
where ds.project_id = $1
order by ds.last_seen_at desc, ds.id
limit 200`,
[projectId],
)
: { rows: [] };
const bindings = capabilities.has("binding.manage")
? await client.query(
`select drb.id, drb.binding_key, drb.display_name,
drb.source_kind, drb.device_id, drb.collection_id,
coalesce(di.display_name, dc.name) as source_name,
drb.target_kind, drb.target_ref, drb.capabilities,
drb.lifecycle_state, drb.source_approved_at,
drb.created_at, drb.updated_at
from device_resource_bindings drb
left join device_instances di on di.id = drb.device_id
left join device_collections dc on dc.id = drb.collection_id
where drb.project_id = $1
order by drb.updated_at desc, drb.id`,
[projectId],
)
: { rows: [] };
const configurationRevisions = capabilities.has("configuration.read")
? await client.query(
`select dcr.id, dcr.device_id, di.display_name as device_name,
dcr.revision_number, dcr.model_profile_ref,
dcr.schema_artifact_ref, dcr.configuration_digest,
dcr.change_summary, dcr.created_at
from device_configuration_revisions dcr
join device_instances di on di.id = dcr.device_id
where dcr.project_id = $1
order by dcr.created_at desc, dcr.id
limit 200`,
[projectId],
)
: { rows: [] };
const configurationStates = capabilities.has("configuration.read")
? await client.query(
`select dcs.device_id, di.display_name as device_name,
dcs.desired_revision_id, dcs.applied_revision_id,
dcs.applied_at, dcs.updated_at
from device_configuration_state dcs
join device_instances di on di.id = dcs.device_id
where dcs.project_id = $1
order by di.display_name, dcs.device_id`,
[projectId],
)
: { rows: [] };
const mayReadCommands = ["command.plan", "command.confirm", "command.dispatch"]
.some((capability) => capabilities.has(capability));
const commands = mayReadCommands
? await client.query(
`select dc.id, dc.device_id, di.display_name as device_name,
dc.command_key, dc.command_catalog_ref, dc.command_type,
dc.risk_class, dc.lifecycle_state, dc.planned_at, dc.expires_at,
dc.confirmed_at, dc.dispatched_at, dc.acknowledged_at,
dc.terminal_at, dc.terminal_reason_code,
dc.created_at, dc.updated_at
from device_commands dc
join device_instances di on di.id = dc.device_id
where dc.project_id = $1
order by dc.updated_at desc, dc.id
limit 200`,
[projectId],
)
: { rows: [] };
const auditEvents = capabilities.has("audit.read")
? await client.query(
`select dae.id, dae.event_type, dae.actor_ref,
dae.device_id, dae.discovery_id, dae.occurred_at
from device_audit_events dae
where dae.project_id = $1
order by dae.occurred_at desc, dae.id
limit 300`,
[projectId],
)
: { rows: [] };
const projectGrants = capabilities.has("access.manage")
? grantsResult
: { rows: [] };
return {
project: projectSummaryView(project, access),
devices: devices.rows.map(deviceView),
collections: collections.rows.map(collectionView),
discoveries: discoveries.rows.map(discoveryView),
enrollments: enrollments.rows.map(enrollmentView),
adapterPackages: adapterPackages.rows.map(adapterPackageView),
adapterVersions: adapterVersions.rows.map(adapterVersionView),
modelProfiles: modelProfiles.rows.map(modelProfileView),
edges: edges.rows.map(edgeView),
routes: routes.rows.map(routeView),
sessions: sessions.rows.map(sessionView),
bindings: bindings.rows.map(bindingView),
configurationRevisions: configurationRevisions.rows.map(
configurationRevisionView,
),
configurationStates: configurationStates.rows.map(configurationStateView),
commands: commands.rows.map(commandView),
auditEvents: auditEvents.rows.map(auditEventView),
grants: projectGrants.rows.map(storedGrantView),
policies: {
commandTransport,
commandPlanningApi: commandTransport === "disabled" ? "disabled" : "enabled",
identifierProjection: "masked-only",
auditPayloadProjection: "metadata-only",
},
};
}
function projectSummaryView(row, access) {
return {
projectRef: toProjectRef(row.id),
projectKey: row.project_key,
name: row.name,
description: row.description ?? null,
lifecycleState: row.lifecycle_state,
ownerScope: {
ownerScopeRef: `owner-scope:${row.owner_scope_id}`,
scopeKind: row.scope_kind,
ownerRef: row.owner_ref,
displayName: row.owner_display_name,
},
access: {
projectRole: access.projectRole,
capabilities: access.capabilities,
},
counts: {
devices: numericCount(row.device_count),
collections: numericCount(row.collection_count),
discoveries: numericCount(row.discovery_count),
},
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function deviceView(row) {
return {
deviceRef: `device:${row.id}`,
deviceKey: row.device_key,
displayName: row.display_name,
integrationDeviceId: row.integration_device_id ?? null,
modelProfileRef: row.model_profile_ref,
lifecycleState: row.lifecycle_state,
identifier: row.identifier_masked
? { kind: row.identifier_kind, masked: row.identifier_masked }
: null,
session: row.session_state
? { state: row.session_state, lastSeenAt: toIso(row.last_seen_at) }
: null,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function collectionView(row) {
return {
collectionRef: `collection:${row.id}`,
collectionKey: row.collection_key,
name: row.name,
description: row.description ?? null,
lifecycleState: row.lifecycle_state,
memberCount: numericCount(row.member_count),
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function discoveryView(row) {
return {
discoveryRef: `discovery:${row.id}`,
identifier: { kind: row.identifier_kind, masked: row.identifier_masked },
modelProfileRef: row.model_profile_ref,
protocol: row.protocol,
lifecycleState: row.lifecycle_state,
enrollmentIntentRef: row.enrollment_intent_id
? `enrollment-intent:${row.enrollment_intent_id}`
: null,
claimedDeviceRef: row.claimed_device_id ? `device:${row.claimed_device_id}` : null,
firstObservedAt: toIso(row.first_observed_at),
lastObservedAt: toIso(row.last_observed_at),
};
}
function enrollmentView(row) {
return {
enrollmentIntentRef: `enrollment-intent:${row.id}`,
enrollmentKey: row.enrollment_key,
displayName: row.display_name,
modelProfileRef: row.model_profile_ref,
expectedIdentifier: {
kind: row.expected_identifier_kind,
masked: row.expected_identifier_masked,
},
lifecycleState: row.lifecycle_state,
observedDiscoveryRef: row.observed_discovery_id
? `discovery:${row.observed_discovery_id}`
: null,
claimedDeviceRef: row.claimed_device_id ? `device:${row.claimed_device_id}` : null,
expiresAt: toIso(row.expires_at),
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function adapterPackageView(row) {
return {
adapterPackageRef: `adapter-package:${row.id}`,
packageKey: row.package_key,
displayName: row.display_name,
publisherRef: row.publisher_ref,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function adapterVersionView(row) {
return {
adapterVersionRef: `adapter-version:${row.id}`,
adapterPackageRef: `adapter-package:${row.adapter_package_id}`,
version: row.version,
runtimePackageRef: row.runtime_package_ref,
contentDigest: row.content_digest,
contractVersion: row.contract_version,
capabilities: [...(row.capabilities ?? [])].sort(),
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function modelProfileView(row) {
return {
modelProfileRef: row.profile_ref,
adapterVersionRef: row.adapter_version_id
? `adapter-version:${row.adapter_version_id}`
: null,
schemaVersion: row.schema_version,
vendor: row.vendor,
model: row.model,
deviceType: row.device_type,
protocol: row.protocol,
schemaArtifactRef: row.schema_artifact_ref ?? null,
profileDigest: row.profile_digest ?? null,
capabilities: [...(row.capabilities ?? [])].sort(),
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function edgeView(row) {
return {
edgeRef: `edge:${row.id}`,
edgeKey: row.edge_key,
displayName: row.display_name,
deploymentRef: row.deployment_ref ?? null,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function routeView(row) {
return {
routeRef: `route:${row.id}`,
routeKey: row.route_key,
displayName: row.display_name,
edgeRef: `edge:${row.edge_id}`,
edgeName: row.edge_name,
modelProfileRef: row.model_profile_ref,
profileName: `${row.profile_vendor} ${row.profile_model}`.trim(),
listenerRef: row.listener_ref,
protocol: row.protocol,
direction: row.direction,
lifecycleState: row.lifecycle_state,
sessionCount: numericCount(row.session_count),
activeSessionCount: numericCount(row.active_session_count),
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function sessionView(row) {
return {
sessionRef: `session:${row.id}`,
routeRef: `route:${row.route_id}`,
routeName: row.route_name,
deviceRef: row.device_id ? `device:${row.device_id}` : null,
deviceName: row.device_name ?? null,
protocol: row.protocol,
lifecycleState: row.lifecycle_state,
connectedAt: toIso(row.connected_at),
lastSeenAt: toIso(row.last_seen_at),
disconnectedAt: toIso(row.disconnected_at),
closeReasonCode: row.close_reason_code ?? null,
frameCount: numericCount(row.frame_count),
byteCount: numericCount(row.byte_count),
};
}
function bindingView(row) {
return {
bindingRef: `binding:${row.id}`,
bindingKey: row.binding_key,
displayName: row.display_name,
source: {
kind: row.source_kind,
ref: row.source_kind === "device"
? `device:${row.device_id}`
: `collection:${row.collection_id}`,
displayName: row.source_name,
},
target: { kind: row.target_kind, ref: row.target_ref },
capabilities: [...(row.capabilities ?? [])].sort(),
lifecycleState: row.lifecycle_state,
sourceApprovedAt: toIso(row.source_approved_at),
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function configurationRevisionView(row) {
return {
configurationRevisionRef: `configuration-revision:${row.id}`,
deviceRef: `device:${row.device_id}`,
deviceName: row.device_name,
revisionNumber: numericCount(row.revision_number),
modelProfileRef: row.model_profile_ref,
schemaArtifactRef: row.schema_artifact_ref,
configurationDigest: row.configuration_digest,
changeSummary: row.change_summary ?? null,
createdAt: toIso(row.created_at),
};
}
function configurationStateView(row) {
return {
deviceRef: `device:${row.device_id}`,
deviceName: row.device_name,
desiredConfigurationRevisionRef: row.desired_revision_id
? `configuration-revision:${row.desired_revision_id}`
: null,
appliedConfigurationRevisionRef: row.applied_revision_id
? `configuration-revision:${row.applied_revision_id}`
: null,
appliedAt: toIso(row.applied_at),
updatedAt: toIso(row.updated_at),
};
}
function commandView(row) {
return {
commandRef: `command:${row.id}`,
deviceRef: `device:${row.device_id}`,
deviceName: row.device_name,
commandKey: row.command_key,
commandCatalogRef: row.command_catalog_ref,
commandType: row.command_type,
riskClass: row.risk_class,
lifecycleState: row.lifecycle_state,
plannedAt: toIso(row.planned_at),
expiresAt: toIso(row.expires_at),
confirmedAt: toIso(row.confirmed_at),
dispatchedAt: toIso(row.dispatched_at),
acknowledgedAt: toIso(row.acknowledged_at),
terminalAt: toIso(row.terminal_at),
terminalReasonCode: row.terminal_reason_code ?? null,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function auditEventView(row) {
return {
auditEventRef: `audit-event:${row.id}`,
eventType: row.event_type,
actorRef: row.actor_ref,
deviceRef: row.device_id ? `device:${row.device_id}` : null,
discoveryRef: row.discovery_id ? `discovery:${row.discovery_id}` : null,
occurredAt: toIso(row.occurred_at),
};
}
function grantView(row) {
return storedGrantView({
id: row.grant_id,
principal_kind: row.principal_kind,
principal_ref: row.principal_ref,
project_role: row.project_role,
capability_allow: row.capability_allow,
capability_deny: row.capability_deny,
lifecycle_state: row.grant_lifecycle_state,
});
}
function storedGrantView(row) {
return {
grantRef: `grant:${row.id}`,
principalKind: row.principal_kind,
principalRef: row.principal_ref,
projectRole: row.project_role,
capabilityAllow: row.capability_allow ?? [],
capabilityDeny: row.capability_deny ?? [],
lifecycleState: row.lifecycle_state,
};
}
function numericCount(value) {
const count = Number(value ?? 0);
return Number.isSafeInteger(count) && count >= 0 ? count : 0;
}
function toIso(value) {
return value == null ? null : new Date(value).toISOString();
}
@@ -0,0 +1,97 @@
import {
normalizeNdcCredentialReference,
} from "./credential-reference.mjs";
export const DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS = Object.freeze([
"device_credential_binding.upsert",
"device_credential_binding.revoke",
]);
const commandKindSet = new Set(DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS);
const purposePattern = /^[a-z][a-z0-9._-]{1,63}$/;
const resolutionPattern = /^[a-z][a-z0-9._-]{1,63}$/;
export function isSensitiveReferenceManagementCommand(kind) {
return commandKindSet.has(kind);
}
export function normalizeSensitiveReferenceManagementCommand(kind, input) {
if (!commandKindSet.has(kind)) {
throw new TypeError("device_sensitive_reference_command_kind_invalid");
}
assertPlainObject(input);
if (kind === "device_credential_binding.upsert") {
assertAllowedKeys(input, [
"projectRef",
"deviceRef",
"purpose",
"credentialRef",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
deviceId: normalizeEntityRef(input.deviceRef, "device"),
purpose: normalizePattern(
input.purpose,
purposePattern,
"device_credential_purpose_invalid",
),
credentialRef: normalizeNdcCredentialReference(input.credentialRef),
});
}
assertAllowedKeys(input, [
"projectRef",
"deviceRef",
"purpose",
"resolutionCode",
]);
return Object.freeze({
projectId: normalizeEntityRef(input.projectRef, "project"),
deviceId: normalizeEntityRef(input.deviceRef, "device"),
purpose: normalizePattern(
input.purpose,
purposePattern,
"device_credential_purpose_invalid",
),
resolutionCode: normalizePattern(
input.resolutionCode,
resolutionPattern,
"device_credential_resolution_code_invalid",
),
});
}
function normalizeEntityRef(value, prefix) {
if (typeof value !== "string") {
throw new TypeError(`device_${prefix}_ref_invalid`);
}
const match = value.match(new RegExp(
`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`,
"i",
));
if (!match) throw new TypeError(`device_${prefix}_ref_invalid`);
return match[1].toLowerCase();
}
function normalizePattern(value, pattern, code) {
if (typeof value !== "string" || !pattern.test(value)) {
throw new TypeError(code);
}
return value;
}
function assertPlainObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_sensitive_reference_command_invalid");
}
}
function assertAllowedKeys(input, allowed) {
const allowedSet = new Set(allowed);
for (const key of Object.keys(input)) {
if (!allowedSet.has(key)) {
throw new TypeError(`device_management_command_field_unexpected:${key}`);
}
}
}
@@ -0,0 +1,269 @@
import { randomUUID } from "node:crypto";
import {
findProjectWithCapability,
} from "./lifecycle-repository.mjs";
import {
isSensitiveReferenceManagementCommand,
} from "./sensitive-reference-management.mjs";
import { toProjectRef } from "./project-management.mjs";
export async function applySensitiveReferenceManagementCommand(
client,
{ commandKind, actor, command },
) {
if (!isSensitiveReferenceManagementCommand(commandKind)) {
throw new TypeError("device_sensitive_reference_command_kind_invalid");
}
if (commandKind === "device_credential_binding.upsert") {
return upsertCredentialBinding(client, actor, command);
}
return revokeCredentialBinding(client, actor, command);
}
export async function authorizeSensitiveReferenceManagementReplay(
client,
{ commandKind, actor, command },
) {
if (!isSensitiveReferenceManagementCommand(commandKind)) {
throw new TypeError("device_sensitive_reference_command_kind_invalid");
}
await findProjectWithCapability(
client,
actor,
command.projectId,
"credential.manage",
);
const current = await client.query(
`select project_id
from device_instances
where id = $1`,
[command.deviceId],
);
const currentProjectId = current.rows[0]?.project_id;
if (!currentProjectId) throw domainError("device_not_found", 404);
if (currentProjectId !== command.projectId) {
await findProjectWithCapability(
client,
actor,
currentProjectId,
"credential.manage",
);
}
}
async function upsertCredentialBinding(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"credential.manage",
);
const device = await findDirectDeviceForUpdate(client, command);
const currentResult = await client.query(
`select id, device_id, owner_scope_id, project_id, purpose,
credential_owner, credential_ref, lifecycle_state,
created_at, updated_at
from device_credential_bindings
where device_id = $1
and purpose = $2
and lifecycle_state = 'active'
for update`,
[device.id, command.purpose],
);
const current = currentResult.rows[0] ?? null;
if (
current
&& current.credential_owner === command.credentialRef.owner
&& current.credential_ref === command.credentialRef.reference
) {
return {
created: false,
rotated: false,
credentialBinding: credentialBindingView(current),
};
}
if (current) {
await client.query(
`update device_credential_bindings
set lifecycle_state = 'revoked',
revoked_at = now(),
revoked_by_ref = $2,
revocation_code = 'credential_rotation',
updated_at = now()
where id = $1 and lifecycle_state = 'active'`,
[current.id, actor.userRef],
);
}
const inserted = await client.query(
`insert into device_credential_bindings (
id,
device_id,
owner_scope_id,
project_id,
purpose,
credential_owner,
credential_ref,
bound_by_ref
) values ($1, $2, $3, $4, $5, $6, $7, $8)
returning id, device_id, owner_scope_id, project_id, purpose,
credential_owner, lifecycle_state, created_at, updated_at`,
[
randomUUID(),
device.id,
project.owner_scope_id,
project.id,
command.purpose,
command.credentialRef.owner,
command.credentialRef.reference,
actor.userRef,
],
);
const binding = inserted.rows[0];
if (!binding) throw domainError("device_credential_binding_insert_failed", 409);
await addAudit(client, {
eventType: current
? "device_credential_binding.rotated"
: "device_credential_binding.created",
actorRef: actor.userRef,
projectId: project.id,
deviceId: device.id,
payload: {
deviceRef: `device:${device.id}`,
projectRef: toProjectRef(project.id),
credentialBindingRef: `credential-binding:${binding.id}`,
...(current
? { rotatedCredentialBindingRef: `credential-binding:${current.id}` }
: {}),
purpose: binding.purpose,
credentialOwner: binding.credential_owner,
},
});
return {
created: true,
rotated: Boolean(current),
credentialBinding: credentialBindingView(binding),
};
}
async function revokeCredentialBinding(client, actor, command) {
const project = await findProjectWithCapability(
client,
actor,
command.projectId,
"credential.manage",
);
const device = await findDirectDeviceForUpdate(client, command);
const revoked = await client.query(
`update device_credential_bindings
set lifecycle_state = 'revoked',
revoked_at = now(),
revoked_by_ref = $4,
revocation_code = $3,
updated_at = now()
where device_id = $1
and purpose = $2
and lifecycle_state = 'active'
returning id, device_id, owner_scope_id, project_id, purpose,
credential_owner, lifecycle_state, created_at, updated_at`,
[device.id, command.purpose, command.resolutionCode, actor.userRef],
);
const binding = revoked.rows[0];
if (!binding) throw domainError("device_credential_binding_not_found", 404);
await addAudit(client, {
eventType: "device_credential_binding.revoked",
actorRef: actor.userRef,
projectId: project.id,
deviceId: device.id,
payload: {
deviceRef: `device:${device.id}`,
projectRef: toProjectRef(project.id),
credentialBindingRef: `credential-binding:${binding.id}`,
purpose: binding.purpose,
credentialOwner: binding.credential_owner,
resolutionCode: command.resolutionCode,
},
});
return {
revoked: true,
credentialBinding: credentialBindingView(binding),
resolutionCode: command.resolutionCode,
};
}
async function findDirectDeviceForUpdate(client, command) {
const result = await client.query(
`select id, owner_scope_id, project_id, lifecycle_state
from device_instances
where id = $1
for update`,
[command.deviceId],
);
const device = result.rows[0];
if (!device) throw domainError("device_not_found", 404);
if (
!device.owner_scope_id
|| !device.project_id
|| device.project_id !== command.projectId
) {
throw domainError("device_credential_binding_project_mismatch", 409);
}
if (device.lifecycle_state === "retired") {
throw domainError("device_credential_binding_lifecycle_blocked", 409);
}
return device;
}
async function addAudit(client, {
eventType,
actorRef,
projectId,
deviceId,
payload,
}) {
await client.query(
`insert into device_audit_events (
id,
event_type,
actor_ref,
project_id,
device_id,
payload
) values ($1, $2, $3, $4, $5, $6::jsonb)`,
[
randomUUID(),
eventType,
actorRef,
projectId,
deviceId,
JSON.stringify(payload),
],
);
}
function credentialBindingView(row) {
return {
credentialBindingRef: `credential-binding:${row.id}`,
deviceRef: `device:${row.device_id}`,
projectRef: toProjectRef(row.project_id),
purpose: row.purpose,
credentialOwner: row.credential_owner,
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
};
}
function toIso(value) {
return new Date(value).toISOString();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
+255
View File
@@ -0,0 +1,255 @@
import {
createPrivateKey,
createPublicKey,
timingSafeEqual,
X509Certificate,
} from "node:crypto";
import { lstat, readFile } from "node:fs/promises";
import { createControlCoreApp } from "./app.mjs";
import { resolveDeviceDatabaseUrl } from "./database-config.mjs";
import {
createDeviceEdgeChannelSupervisor,
} from "./edge-channel-supervisor.mjs";
import { createDeviceGatewayIngest } from "./gateway-ingest.mjs";
import { PostgresDeviceRepository } from "./postgres-repository.mjs";
import { createTypedCommandRuntime } from "./typed-command-runtime.mjs";
const config = await readConfig();
const repository = new PostgresDeviceRepository({
databaseUrl: config.databaseUrl,
poolSize: config.databasePoolSize,
});
await repository.migrate();
const typedCommandRuntime = config.managementApiEnabled && config.edgeChannelEnabled
? createTypedCommandRuntime({ repository })
: null;
const gatewayIngest = config.discoveryIngestEnabled || config.edgeChannelEnabled
? createDeviceGatewayIngest({
repository,
identifierPepper: config.identifierPepper,
})
: null;
const edgeChannelSupervisor = config.edgeChannelEnabled
? createDeviceEdgeChannelSupervisor({
repository,
gatewayIngest,
coreIdentity: config.edgeChannelCoreIdentity,
trustRoot: config.edgeChannelTrustRoot,
maxEdges: config.edgeChannelMaxEdges,
reconcileIntervalMs: config.edgeChannelReconcileIntervalMs,
typedCommandRuntime,
})
: null;
await edgeChannelSupervisor?.start();
const server = createControlCoreApp({
repository,
gatewayToken: config.gatewayToken,
identifierPepper: config.identifierPepper,
discoveryIngestEnabled: config.discoveryIngestEnabled,
managementApiEnabled: config.managementApiEnabled,
managementToken: config.managementToken,
gatewayIngest,
edgeChannelStatusProvider: edgeChannelSupervisor
? () => edgeChannelSupervisor.status()
: null,
typedCommandRuntime,
});
server.listen(config.port, config.host, () => {
console.log(JSON.stringify({
event: "device_control_core_started",
host: config.host,
port: config.port,
discoveryIngest: config.discoveryIngestEnabled,
managementApi: config.managementApiEnabled,
edgeChannels: config.edgeChannelEnabled,
commandTransport: typedCommandRuntime ? "typed-service-ping-v1" : "disabled",
}));
});
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
async function shutdown() {
server.close(async () => {
await edgeChannelSupervisor?.stop();
await repository.close();
process.exit(0);
});
}
async function readConfig() {
const discoveryIngestEnabled = parseBoolean(
process.env.DEVICE_DISCOVERY_INGEST_ENABLED,
false,
);
const managementApiEnabled = parseBoolean(
process.env.DEVICE_MANAGEMENT_API_ENABLED,
false,
);
const edgeChannelEnabled = parseBoolean(
process.env.DEVICE_EDGE_CHANNEL_ENABLED,
false,
);
const edgeChannelCoreIdentity = edgeChannelEnabled
? await readCoreIdentity(process.env)
: null;
return {
host: String(process.env.HOST || "127.0.0.1").trim(),
port: parsePort(process.env.PORT, 18120),
databaseUrl: await resolveDeviceDatabaseUrl(process.env),
databasePoolSize: parsePositiveInt(
process.env.DEVICE_DATABASE_POOL_SIZE,
10,
),
discoveryIngestEnabled,
managementApiEnabled,
edgeChannelEnabled,
gatewayToken: discoveryIngestEnabled
? await readRequiredSecretFile(
process.env.DEVICE_GATEWAY_CORE_TOKEN_FILE,
"device_gateway_core_token_file_required",
)
: "",
identifierPepper:
discoveryIngestEnabled || managementApiEnabled || edgeChannelEnabled
? await readRequiredSecretFile(
process.env.DEVICE_IDENTIFIER_PEPPER_FILE,
"device_identifier_pepper_file_required",
)
: "",
managementToken: managementApiEnabled
? await readRequiredSecretFile(
process.env.DEVICE_MANAGEMENT_CORE_TOKEN_FILE,
"device_management_core_token_file_required",
)
: "",
edgeChannelCoreIdentity,
edgeChannelTrustRoot: edgeChannelEnabled
? await readRequiredDirectory(
process.env.DEVICE_EDGE_CHANNEL_TRUST_ROOT,
"device_edge_channel_trust_root_required",
)
: "",
edgeChannelMaxEdges: parseBoundedInt(
process.env.DEVICE_EDGE_CHANNEL_MAX_EDGES,
32,
1,
64,
),
edgeChannelReconcileIntervalMs: parseBoundedInt(
process.env.DEVICE_EDGE_CHANNEL_RECONCILE_INTERVAL_MS,
15_000,
1_000,
300_000,
),
};
}
async function readCoreIdentity(environment) {
const keyPath = requiredValue(
environment.DEVICE_EDGE_CHANNEL_CORE_KEY_FILE,
"device_edge_channel_core_key_file_required",
);
const certificatePath = requiredValue(
environment.DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE,
"device_edge_channel_core_certificate_file_required",
);
const [key, cert] = await Promise.all([
readBoundedRegularFile(keyPath, 32 * 1024),
readBoundedRegularFile(certificatePath, 32 * 1024),
]);
const privatePublic = createPublicKey(createPrivateKey(key)).export({
type: "spki",
format: "der",
});
const certificatePublic = new X509Certificate(cert).publicKey.export({
type: "spki",
format: "der",
});
if (
privatePublic.length !== certificatePublic.length
|| !timingSafeEqual(privatePublic, certificatePublic)
) {
throw new Error("device_edge_channel_core_identity_mismatch");
}
return Object.freeze({
identityRef: "workload:device-control-core",
key,
cert,
});
}
async function readBoundedRegularFile(path, maximumBytes) {
const state = await lstat(path);
if (
state.isSymbolicLink()
|| !state.isFile()
|| state.size < 1
|| state.size > maximumBytes
) {
throw new Error("device_edge_channel_core_identity_file_invalid");
}
return readFile(path);
}
async function readRequiredDirectory(path, errorCode) {
const normalized = requiredValue(path, errorCode);
const state = await lstat(normalized);
if (state.isSymbolicLink() || !state.isDirectory()) throw new Error(errorCode);
return normalized;
}
async function readRequiredSecretFile(path, errorCode) {
const normalized = requiredValue(path, errorCode);
const value = (await readFile(normalized, "utf8")).trim();
if (value.length < 32) throw new Error(errorCode);
return value;
}
function requiredValue(value, errorCode) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(errorCode);
}
return value.trim();
}
function parsePort(value, fallback) {
const parsed = Number(value || fallback);
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) {
throw new Error("device_control_port_invalid");
}
return parsed;
}
function parsePositiveInt(value, fallback) {
const parsed = Number(value || fallback);
if (!Number.isSafeInteger(parsed) || parsed < 1) {
throw new Error("device_positive_integer_invalid");
}
return parsed;
}
function parseBoundedInt(value, fallback, minimum, maximum) {
const parsed = Number(value ?? fallback);
if (
!Number.isSafeInteger(parsed)
|| parsed < minimum
|| parsed > maximum
) {
throw new Error("device_bounded_integer_invalid");
}
return parsed;
}
function parseBoolean(value, fallback) {
if (value === undefined || value === null || value === "") return fallback;
const normalized = String(value).trim().toLowerCase();
if (["1", "true", "yes", "on"].includes(normalized)) return true;
if (["0", "false", "no", "off"].includes(normalized)) return false;
throw new Error("device_boolean_invalid");
}
@@ -0,0 +1,299 @@
import { createHash, randomUUID } from "node:crypto";
import { assertProjectCapability } from "./project-management.mjs";
const SERVICE_PING_CATALOG = "arusnavi.b2.internal.v1:service-ping";
export async function planTypedServicePing(client, {
idempotencyKey,
requestDigest,
actor,
projectId,
deviceId,
expiresAt,
}) {
const commandKey = `service-ping-${createHash("sha256")
.update(`${actor.userRef}\0${idempotencyKey}`, "utf8")
.digest("hex").slice(0, 32)}`;
const project = await findProject(client, projectId);
const grants = await projectGrants(client, projectId);
assertProjectCapability(actor, grants, "command.plan");
assertProjectCapability(actor, grants, "command.dispatch");
const device = await findCommandableDevice(client, projectId, deviceId);
const existing = await client.query(
`select dc.*, di.display_name as device_name
from device_commands dc
join device_instances di on di.id = dc.device_id
where dc.project_id = $1 and dc.command_key = $2
for update of dc`,
[projectId, commandKey],
);
if (existing.rows[0]) {
const row = existing.rows[0];
if (
row.device_id !== deviceId
|| row.command_catalog_ref !== SERVICE_PING_CATALOG
|| row.command_type !== "service.ping"
|| row.parameters_digest !== requestDigest
) {
throw domainError("device_command_idempotency_conflict", 409);
}
return { replayed: true, commandId: row.id, command: commandView(row) };
}
const commandId = randomUUID();
const plannedAt = new Date();
await client.query(
`insert into device_commands (
id, owner_scope_id, project_id, device_id, command_key,
command_catalog_ref, command_type, risk_class,
parameters_digest, parameters_projection, lifecycle_state,
planned_by_ref, planned_at, expires_at
) values (
$1, $2, $3, $4, $5, $6, 'service.ping', 'low',
$7, $8::jsonb, 'queued', $9, $10, $11
)`,
[
commandId,
project.owner_scope_id,
projectId,
deviceId,
commandKey,
SERVICE_PING_CATALOG,
requestDigest,
JSON.stringify({ operation: "service.ping", profileRef: device.model_profile_ref }),
actor.userRef,
plannedAt,
expiresAt,
],
);
await insertEvent(client, commandId, deviceId, projectId, 1, null, "draft", actor.userRef, "operator_requested");
await insertEvent(client, commandId, deviceId, projectId, 2, "draft", "planned", actor.userRef, "typed_policy_approved");
await insertEvent(client, commandId, deviceId, projectId, 3, "planned", "queued", actor.userRef, "awaiting_tracker_session");
await audit(client, {
eventType: "command.queued",
actorRef: actor.userRef,
projectId,
deviceId,
payload: { commandRef: `command:${commandId}`, commandType: "service.ping", riskClass: "low" },
});
return {
replayed: false,
commandId,
command: commandView({
id: commandId,
device_id: deviceId,
device_name: device.display_name,
command_key: commandKey,
command_catalog_ref: SERVICE_PING_CATALOG,
command_type: "service.ping",
risk_class: "low",
lifecycle_state: "queued",
planned_at: plannedAt,
expires_at: expiresAt,
created_at: plannedAt,
updated_at: plannedAt,
}),
};
}
export async function dispatchTypedCommand(client, {
commandId,
transportMessageRef,
now,
}) {
const result = await client.query(
`select dc.*, di.display_name as device_name
from device_commands dc
join device_instances di on di.id = dc.device_id
where dc.id = $1
for update of dc`,
[commandId],
);
const row = result.rows[0];
if (!row) throw domainError("device_command_not_found", 404);
if (row.lifecycle_state !== "queued") return null;
if (new Date(row.expires_at).getTime() <= now.getTime()) {
await client.query(
`update device_commands set lifecycle_state = 'expired',
terminal_at = $2, terminal_reason_code = 'ttl_elapsed', updated_at = $2
where id = $1`,
[commandId, now],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 4, "queued", "expired", "workload:device-control-core", "ttl_elapsed");
return null;
}
await client.query(
`update device_commands set lifecycle_state = 'dispatched',
dispatched_at = $2, transport_message_ref = $3, updated_at = $2
where id = $1`,
[commandId, now, transportMessageRef],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 4, "queued", "dispatched", "workload:device-control-core", "tracker_session_allocated", transportMessageRef);
await audit(client, {
eventType: "command.dispatched",
actorRef: "workload:device-control-core",
projectId: row.project_id,
deviceId: row.device_id,
payload: { commandRef: `command:${commandId}`, commandType: row.command_type },
});
return commandView({ ...row, lifecycle_state: "dispatched", dispatched_at: now, transport_message_ref: transportMessageRef, updated_at: now });
}
export async function recordTypedCommandStatus(client, {
commandId,
transportMessageRef,
lifecycleState,
resultCode,
observedAt,
}) {
const result = await client.query(
`select * from device_commands where id = $1 for update`,
[commandId],
);
const row = result.rows[0];
if (!row) throw domainError("device_command_not_found", 404);
if (row.transport_message_ref !== transportMessageRef) {
throw domainError("device_command_transport_mismatch", 409);
}
if (["verified", "failed", "expired", "unknown"].includes(row.lifecycle_state)) {
return;
}
if (lifecycleState === "acknowledged" && row.lifecycle_state === "dispatched") {
await client.query(
`update device_commands set lifecycle_state = 'acknowledged',
acknowledged_at = $2, updated_at = $2 where id = $1`,
[commandId, observedAt],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 5, "dispatched", "acknowledged", "workload:device-edge", "protocol_reply_received", `result:${resultCode}`);
await client.query(
`update device_commands set lifecycle_state = 'verified',
terminal_at = $2, terminal_reason_code = 'protocol_reply_serv_ok',
updated_at = $2 where id = $1`,
[commandId, observedAt],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 6, "acknowledged", "verified", "workload:device-control-core", "protocol_reply_serv_ok", `result:${resultCode}`);
await audit(client, {
eventType: "command.verified",
actorRef: "workload:device-control-core",
projectId: row.project_id,
deviceId: row.device_id,
payload: { commandRef: `command:${commandId}`, commandType: row.command_type, resultCode },
});
return;
}
if (lifecycleState === "unknown" && row.lifecycle_state === "dispatched") {
await client.query(
`update device_commands set lifecycle_state = 'unknown', terminal_at = $2,
terminal_reason_code = $3, updated_at = $2 where id = $1`,
[commandId, observedAt, resultCode],
);
await insertEvent(client, commandId, row.device_id, row.project_id, 5, "dispatched", "unknown", "workload:device-edge", resultCode);
}
}
async function findProject(client, projectId) {
const result = await client.query(
`select id, owner_scope_id, lifecycle_state from device_projects where id = $1 for share`,
[projectId],
);
const row = result.rows[0];
if (!row) throw domainError("device_project_not_found", 404);
if (row.lifecycle_state !== "active") throw domainError("device_project_inactive", 409);
return row;
}
async function findCommandableDevice(client, projectId, deviceId) {
const result = await client.query(
`select di.id, di.display_name, di.model_profile_ref, di.lifecycle_state
from device_instances di
where di.project_id = $1 and di.id = $2
and exists (
select 1 from device_routes dr
where dr.project_id = di.project_id
and dr.model_profile_ref = di.model_profile_ref
and dr.direction = 'bidirectional'
and dr.lifecycle_state = 'active'
)
for share`,
[projectId, deviceId],
);
const row = result.rows[0];
if (!row) throw domainError("device_command_route_unavailable", 409);
if (row.model_profile_ref !== "arusnavi.b2.internal.v1") {
throw domainError("device_command_profile_unsupported", 409);
}
if (["suspended", "retired"].includes(row.lifecycle_state)) {
throw domainError("device_command_device_inactive", 409);
}
return row;
}
async function projectGrants(client, projectId) {
const result = await client.query(
`select id, principal_kind, principal_ref, project_role,
capability_allow, capability_deny, lifecycle_state
from device_project_grants where project_id = $1 for share`,
[projectId],
);
return result.rows.map((row) => ({
grantRef: `grant:${row.id}`,
principalKind: row.principal_kind,
principalRef: row.principal_ref,
projectRole: row.project_role,
capabilityAllow: row.capability_allow ?? [],
capabilityDeny: row.capability_deny ?? [],
lifecycleState: row.lifecycle_state,
}));
}
async function insertEvent(client, commandId, deviceId, projectId, sequence, from, to, actor, reason, evidence = null) {
await client.query(
`insert into device_command_events (
id, command_id, device_id, project_id, sequence_number,
from_state, to_state, actor_ref, reason_code, evidence_ref
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[randomUUID(), commandId, deviceId, projectId, sequence, from, to, actor, reason, evidence],
);
}
async function audit(client, { eventType, actorRef, projectId, deviceId, payload }) {
await client.query(
`insert into device_audit_events (
id, event_type, actor_ref, project_id, device_id, payload
) values ($1,$2,$3,$4,$5,$6::jsonb)`,
[randomUUID(), eventType, actorRef, projectId, deviceId, JSON.stringify(payload)],
);
}
function commandView(row) {
return Object.freeze({
commandRef: `command:${row.id}`,
deviceRef: `device:${row.device_id}`,
deviceName: row.device_name,
commandKey: row.command_key,
commandCatalogRef: row.command_catalog_ref,
commandType: row.command_type,
riskClass: row.risk_class,
lifecycleState: row.lifecycle_state,
plannedAt: iso(row.planned_at),
expiresAt: iso(row.expires_at),
dispatchedAt: iso(row.dispatched_at),
acknowledgedAt: iso(row.acknowledged_at),
terminalAt: iso(row.terminal_at),
terminalReasonCode: row.terminal_reason_code ?? null,
createdAt: iso(row.created_at),
updatedAt: iso(row.updated_at),
});
}
function iso(value) {
return value == null ? null : new Date(value).toISOString();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,149 @@
import { createHash, randomUUID } from "node:crypto";
export function createTypedCommandRuntime({ repository, now = () => new Date() } = {}) {
if (
!repository
|| typeof repository.planTypedServicePing !== "function"
|| typeof repository.dispatchTypedCommand !== "function"
|| typeof repository.recordTypedCommandStatus !== "function"
) {
throw new TypeError("device_typed_command_repository_required");
}
const credentials = new Map();
return Object.freeze({
async planServicePing({ idempotencyKey, actor, input }) {
const normalized = normalizeInput(input);
const expiresAt = new Date(now().getTime() + normalized.expiresInSeconds * 1000);
const requestDigest = `sha256:${createHash("sha256").update(JSON.stringify({
actorRef: actor.userRef,
projectId: normalized.projectId,
deviceId: normalized.deviceId,
operation: "service.ping",
expiresInSeconds: normalized.expiresInSeconds,
}), "utf8").digest("hex")}`;
const execution = await repository.planTypedServicePing({
idempotencyKey,
requestDigest,
actor,
projectId: normalized.projectId,
deviceId: normalized.deviceId,
expiresAt,
});
if (!execution.replayed && execution.command.lifecycleState === "queued") {
credentials.set(execution.commandId, Object.freeze({
deviceId: normalized.deviceId,
accessCode: normalized.accessCode,
expiresAt: new Date(execution.command.expiresAt).getTime(),
}));
}
return { replayed: execution.replayed, command: execution.command };
},
async offerForDevice(deviceRef) {
const deviceId = entityId(deviceRef, "device");
const at = now();
for (const [commandId, secret] of credentials) {
if (secret.expiresAt <= at.getTime()) {
await repository.dispatchTypedCommand({
commandId,
transportMessageRef: `edge-command:${randomUUID()}`,
now: at,
});
credentials.delete(commandId);
continue;
}
if (secret.deviceId !== deviceId) continue;
const transportMessageRef = `edge-command:${randomUUID()}`;
const dispatched = await repository.dispatchTypedCommand({
commandId,
transportMessageRef,
now: at,
});
if (!dispatched) {
credentials.delete(commandId);
continue;
}
return Object.freeze({
commandRef: `command:${commandId}`,
commandType: "service.ping",
accessCode: secret.accessCode,
transportMessageRef,
});
}
return null;
},
async recordStatus(status) {
const commandId = entityId(status?.commandRef, "command");
const normalized = normalizeStatus(status);
await repository.recordTypedCommandStatus({ commandId, ...normalized });
credentials.delete(commandId);
},
status() {
return Object.freeze({
commandTransport: "typed-service-ping-v1",
transientAuthorizations: credentials.size,
});
},
});
}
function normalizeInput(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw domainError("device_service_ping_input_invalid", 400);
}
const keys = Object.keys(input).sort().join(",");
if (keys !== "accessCode,deviceRef,expiresInSeconds,projectRef") {
throw domainError("device_service_ping_input_invalid", 400);
}
if (typeof input.accessCode !== "string" || !/^\d{6}$/.test(input.accessCode)) {
throw domainError("device_service_ping_access_code_invalid", 400);
}
const expiresInSeconds = Number(input.expiresInSeconds);
if (!Number.isSafeInteger(expiresInSeconds) || expiresInSeconds < 30 || expiresInSeconds > 1800) {
throw domainError("device_service_ping_ttl_invalid", 400);
}
return {
projectId: entityId(input.projectRef, "project"),
deviceId: entityId(input.deviceRef, "device"),
accessCode: input.accessCode,
expiresInSeconds,
};
}
function normalizeStatus(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_command_status_invalid");
}
if (!['acknowledged', 'unknown'].includes(value.lifecycleState)) {
throw new TypeError("device_command_status_invalid");
}
if (typeof value.transportMessageRef !== "string" || !/^edge-command:[0-9a-f-]{36}$/i.test(value.transportMessageRef)) {
throw new TypeError("device_command_status_invalid");
}
if (typeof value.resultCode !== "string" || !/^[a-z][a-z0-9._-]{1,63}$/.test(value.resultCode)) {
throw new TypeError("device_command_status_invalid");
}
const observedAt = new Date(value.observedAt);
if (Number.isNaN(observedAt.getTime())) throw new TypeError("device_command_status_invalid");
return {
transportMessageRef: value.transportMessageRef.toLowerCase(),
lifecycleState: value.lifecycleState,
resultCode: value.resultCode,
observedAt,
};
}
function entityId(value, prefix) {
const match = String(value || "").match(new RegExp(`^${prefix}:([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$`, "i"));
if (!match) throw domainError(`device_${prefix}_ref_invalid`, 400);
return match[1].toLowerCase();
}
function domainError(code, statusCode) {
const error = new Error(code);
error.statusCode = statusCode;
return error;
}
@@ -0,0 +1,589 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
DEVICE_ADAPTER_MESSAGE_SCHEMA,
DEVICE_DISCOVERY_SIGNAL_SCHEMA,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import { createControlCoreApp } from "../src/app.mjs";
const gatewayToken = "test-only-gateway-token-with-32-bytes";
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
const managementToken = "test-only-management-token-with-32-bytes";
const fakeImei = "000000000000001";
test("health reports database readiness and disabled command transport", async () => {
const runtime = await startTestServer({
repository: {
health: async () => "ready",
},
});
try {
const response = await fetch(`${runtime.baseUrl}/healthz`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
ok: true,
service: "nodedc-device-control-core",
database: "ready",
discoveryIngest: "disabled",
managementApi: "disabled",
edgeChannels: {
enabled: false,
configured: 0,
accepted: 0,
degraded: 0,
},
commandTransport: "disabled",
});
} finally {
await runtime.close();
}
});
test("management API is closed by default", async () => {
const runtime = await startTestServer({
repository: {
health: async () => "ready",
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
},
);
assert.equal(response.status, 404);
assert.equal((await response.json()).error, "device_management_api_disabled");
} finally {
await runtime.close();
}
});
test("management API cannot start without its repository boundary and strong token", () => {
assert.throws(
() => createControlCoreApp({
managementApiEnabled: true,
managementToken,
repository: { health: async () => "ready" },
}),
/device_management_repository_required/,
);
assert.throws(
() => createControlCoreApp({
managementApiEnabled: true,
managementToken: "short",
repository: {
health: async () => "ready",
executeManagementCommand: async () => ({}),
},
}),
/device_management_token_invalid/,
);
assert.throws(
() => createControlCoreApp({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => ({}),
},
}),
/device_identifier_pepper_invalid/,
);
});
test("management API requires service auth and an idempotency key", async () => {
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => {
throw new Error("must_not_execute");
},
},
});
try {
const unauthorized = await fetch(
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
},
);
assert.equal(unauthorized.status, 401);
const missingIdempotency = await fetch(
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
{
method: "POST",
headers: managementHeaders({ includeIdempotency: false }),
body: JSON.stringify(ownerScopeCommand()),
},
);
assert.equal(missingIdempotency.status, 400);
assert.equal(
(await missingIdempotency.json()).error,
"device_management_header_required",
);
} finally {
await runtime.close();
}
});
test("management API forwards only normalized actor and command data", async () => {
let executed;
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async (value) => {
executed = value;
return {
replayed: false,
result: {
created: true,
ownerScope: {
ownerScopeRef: "owner-scope:11111111-1111-4111-8111-111111111111",
scopeKind: "company",
ownerRef: "client:example",
displayName: "Example Company",
lifecycleState: "active",
},
},
};
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify(ownerScopeCommand()),
},
);
assert.equal(response.status, 200);
assert.equal(response.headers.get("idempotency-key"), "phase2-test-0001");
assert.equal(response.headers.get("idempotency-replayed"), "false");
assert.match(executed.requestDigest, /^sha256:[a-f0-9]{64}$/);
assert.equal(executed.commandKind, "owner_scope.ensure");
assert.deepEqual(executed.command, ownerScopeCommand());
assert.deepEqual(executed.actor, {
userRef: "user:engineer",
hubRole: "admin",
groupRefs: ["group:engineers", "group:operators"],
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
});
assert.equal((await response.json()).result.created, true);
} finally {
await runtime.close();
}
});
test("management API rejects unexpected fields before repository execution", async () => {
let executions = 0;
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => {
executions += 1;
return { replayed: false, result: {} };
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify({
...ownerScopeCommand(),
credential: "must-never-cross-boundary",
}),
},
);
assert.equal(response.status, 400);
assert.match(
(await response.json()).error,
/device_management_command_field_unexpected:credential/,
);
assert.equal(executions, 0);
} finally {
await runtime.close();
}
});
test("management API exposes a repository idempotency conflict without retrying", async () => {
let executions = 0;
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => {
executions += 1;
const error = new Error("device_idempotency_key_conflict");
error.statusCode = 409;
throw error;
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/owner-scopes:ensure`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify(ownerScopeCommand()),
},
);
assert.equal(response.status, 409);
assert.equal((await response.json()).error, "device_idempotency_key_conflict");
assert.equal(executions, 1);
} finally {
await runtime.close();
}
});
test("typed service ping accepts a transient access code and never echoes it", async () => {
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
let planned;
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => ({ replayed: false, result: {} }),
},
typedCommandRuntime: {
status: () => ({ commandTransport: "typed-service-ping-v1" }),
planServicePing: async (value) => {
planned = value;
return {
replayed: false,
command: {
commandRef: "command:33333333-3333-4333-8333-333333333333",
deviceRef,
commandType: "service.ping",
lifecycleState: "queued",
},
};
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/commands:service-ping`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify({
projectRef,
deviceRef,
accessCode: "654321",
expiresInSeconds: 300,
}),
},
);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.result.lifecycleState, "queued");
assert.equal(JSON.stringify(body).includes("654321"), false);
assert.equal(planned.input.accessCode, "654321");
assert.equal(planned.idempotencyKey, "phase2-test-0001");
} finally {
await runtime.close();
}
});
test("project query is service-authenticated and forwards only the trusted actor", async () => {
let queriedActor;
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => ({ replayed: false, result: {} }),
listAccessibleProjects: async (actor) => {
queriedActor = actor;
return [{ projectRef: "project:11111111-1111-4111-8111-111111111111" }];
},
},
});
try {
const unauthorized = await fetch(
`${runtime.baseUrl}/internal/v1/query/projects`,
);
assert.equal(unauthorized.status, 401);
const response = await fetch(
`${runtime.baseUrl}/internal/v1/query/projects`,
{ headers: managementHeaders() },
);
assert.equal(response.status, 200);
assert.equal((await response.json()).projects.length, 1);
assert.deepEqual(queriedActor, {
userRef: "user:engineer",
hubRole: "admin",
groupRefs: ["group:engineers", "group:operators"],
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
});
} finally {
await runtime.close();
}
});
test("project workspace query accepts only a canonical project path", async () => {
const projectId = "11111111-1111-4111-8111-111111111111";
let queried;
const runtime = await startTestServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => ({ replayed: false, result: {} }),
getProjectWorkspace: async (actor, id) => {
queried = { actor, id };
return { project: { projectRef: `project:${id}` }, devices: [] };
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/query/projects/${projectId}/workspace`,
{ headers: managementHeaders() },
);
assert.equal(response.status, 200);
assert.equal((await response.json()).workspace.devices.length, 0);
assert.equal(queried.id, projectId);
const invalid = await fetch(
`${runtime.baseUrl}/internal/v1/query/projects/not-a-project/workspace`,
{ headers: managementHeaders() },
);
assert.equal(invalid.status, 404);
} finally {
await runtime.close();
}
});
test("discovery ingest is closed by default", async () => {
const runtime = await startTestServer({
repository: {
health: async () => "ready",
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
},
);
assert.equal(response.status, 404);
assert.equal(
(await response.json()).error,
"device_discovery_ingest_disabled",
);
} finally {
await runtime.close();
}
});
test("authenticated ingest stores only digest and returns a masked view", async () => {
let stored;
const runtime = await startTestServer({
discoveryIngestEnabled: true,
gatewayToken,
identifierPepper,
repository: {
health: async () => "ready",
upsertQuarantineDiscovery: async (value) => {
stored = value;
return {
created: true,
value: {
...value.safeView,
discoveryRef: "discovery:test-001",
},
};
},
acceptAdapterMessage: async () => {
throw new Error("must_not_accept_message");
},
},
});
try {
const unauthorized = await fetch(
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(fakeSignal()),
},
);
assert.equal(unauthorized.status, 401);
const response = await fetch(
`${runtime.baseUrl}/internal/v1/device-discoveries:observe`,
{
method: "POST",
headers: {
Authorization: `Bearer ${gatewayToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(fakeSignal()),
},
);
assert.equal(response.status, 201);
const body = await response.json();
const serialized = JSON.stringify(body);
assert.equal(serialized.includes(fakeImei), false);
assert.equal(body.discovery.identifier.masked, "***********0001");
assert.match(stored.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
assert.equal(stored.sessionRef, "session:test-001");
assert.equal(stored.routeRef, null);
assert.equal(JSON.stringify(stored).includes(fakeImei), false);
} finally {
await runtime.close();
}
});
test("gateway message endpoint returns acceptance only after repository commit", async () => {
let stored;
const runtime = await startTestServer({
discoveryIngestEnabled: true,
gatewayToken,
identifierPepper,
repository: {
health: async () => "ready",
upsertQuarantineDiscovery: async () => {
throw new Error("must_not_observe_discovery");
},
acceptAdapterMessage: async (value) => {
stored = value;
return {
acceptance: {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: "acceptance:test-001",
idempotencyKey: value.safeView.idempotencyKey,
status: "accepted",
replayed: false,
acceptedAt: "2026-08-11T12:00:00.000Z",
},
claimedDeviceRef: "device:11111111-1111-4111-8111-111111111111",
};
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/gateway/messages:accept`,
{
method: "POST",
headers: {
Authorization: `Bearer ${gatewayToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(fakeAdapterMessage()),
},
);
assert.equal(response.status, 201);
const body = await response.json();
assert.equal(body.acceptance.status, "accepted");
assert.match(stored.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
assert.match(stored.requestDigest, /^sha256:[a-f0-9]{64}$/);
assert.equal(stored.safeView.identifier.masked, "***********0001");
assert.equal(JSON.stringify(stored).includes(fakeImei), false);
} finally {
await runtime.close();
}
});
function fakeSignal() {
return {
schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
sessionRef: "session:test-001",
modelProfileRef: "arusnavi.b2.internal.v1",
protocol: "INTERNAL",
observedAt: "2026-07-25T00:00:00.000Z",
identifier: { kind: "imei", value: fakeImei },
evidence: {
transport: "tcp",
bytesObserved: 128,
framingStatus: "verified",
specificationRef: "arusnavi.internal.framing.test-v1",
},
};
}
function fakeAdapterMessage() {
return {
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
edgeRef: "edge:test-001",
adapterRef: "arusnavi-b2",
protocolProfileRef: "arusnavi.b2.internal.v1",
protocol: "INTERNAL",
sessionRef: "session:test-001",
messageRef: "package:1:test",
messageType: "telemetry.package",
sequence: 1,
observedAt: "2026-08-11T12:00:00.000Z",
idempotencyKey: `sha256:${"a".repeat(64)}`,
identifier: { kind: "imei", value: fakeImei },
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
payload: {
packageNumber: 1,
packetCount: 1,
packageDigest: `sha256:${"b".repeat(64)}`,
},
};
}
function managementHeaders({ includeIdempotency = true } = {}) {
return {
Authorization: `Bearer ${managementToken}`,
"Content-Type": "application/json",
...(includeIdempotency ? { "Idempotency-Key": "phase2-test-0001" } : {}),
"X-NODEDC-User-Ref": "user:engineer",
"X-NODEDC-Hub-Role": "admin",
"X-NODEDC-Group-Refs": "group:operators,group:engineers",
"X-NODEDC-Owner-Scopes": "company=client:example",
};
}
function ownerScopeCommand() {
return {
scopeKind: "company",
ownerRef: "client:example",
displayName: "Example Company",
};
}
async function startTestServer(options) {
const server = createControlCoreApp({ identifierPepper, ...options });
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
return {
baseUrl: `http://127.0.0.1:${address.port}`,
close: () => new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
}),
};
}
@@ -0,0 +1,158 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
DEVICE_CONTROL_RESOURCE_COMMAND_KINDS,
normalizeControlResourceManagementCommand,
} from "../src/control-resource-management.mjs";
import {
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
normalizeDeviceManagementCommand,
} from "../src/management-command.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
const collectionRef = "collection:33333333-3333-4333-8333-333333333333";
const bindingRef = "binding:44444444-4444-4444-8444-444444444444";
const revisionRef =
"configuration-revision:55555555-5555-4555-8555-555555555555";
test("control resource commands join the strict idempotent surface", () => {
for (const kind of DEVICE_CONTROL_RESOURCE_COMMAND_KINDS) {
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
}
assert.equal(
normalizeDeviceManagementCommand(
"device_binding.ensure",
bindingInput(),
).projectId,
projectRef.slice("project:".length),
);
});
test("binding input is source-scoped and cannot claim external approval", () => {
const command = normalizeControlResourceManagementCommand(
"device_binding.ensure",
bindingInput(),
);
assert.deepEqual(command.source, {
kind: "collection",
id: collectionRef.slice("collection:".length),
});
assert.deepEqual(command.capabilities, ["inspect", "observe"]);
assert.equal("lifecycleState" in command, false);
assert.equal("externalApprovalRef" in command, false);
assert.throws(
() => normalizeControlResourceManagementCommand(
"device_binding.ensure",
{ ...bindingInput(), externalApprovalRef: "approval:forged" },
),
/device_management_command_field_unexpected:externalApprovalRef/,
);
assert.throws(
() => normalizeControlResourceManagementCommand(
"device_binding.ensure",
{ ...bindingInput(), targetRef: "ndc-credref:must-not-be-a-target" },
),
/device_binding_target_ref_invalid/,
);
});
test("binding revoke uses only project, binding and bounded reason refs", () => {
const command = normalizeControlResourceManagementCommand(
"device_binding.revoke",
{
projectRef,
bindingRef,
resolutionCode: "operator.unbound",
},
);
assert.equal(command.bindingId, bindingRef.slice("binding:".length));
assert.equal(command.resolutionCode, "operator.unbound");
});
test("configuration is canonical, bounded and secret-free before hashing", () => {
const first = normalizeControlResourceManagementCommand(
"device_configuration_revision.create",
{
projectRef,
deviceRef,
configuration: {
reporting_interval_seconds: 15,
motion: { enabled: true, threshold: 3.5 },
channels: ["gps", "voltage"],
},
changeSummary: "Pilot reporting profile",
},
);
const reordered = normalizeControlResourceManagementCommand(
"device_configuration_revision.create",
{
projectRef,
deviceRef,
configuration: {
channels: ["gps", "voltage"],
motion: { threshold: 3.5, enabled: true },
reporting_interval_seconds: 15,
},
changeSummary: "Pilot reporting profile",
},
);
assert.equal(first.configurationDigest, reordered.configurationDigest);
assert.equal(Object.isFrozen(first.configuration.motion), true);
assert.throws(
() => normalizeControlResourceManagementCommand(
"device_configuration_revision.create",
{
projectRef,
deviceRef,
configuration: { api_token: "forbidden" },
},
),
/forbidden_device_field/,
);
assert.throws(
() => normalizeControlResourceManagementCommand(
"device_configuration_revision.create",
{
projectRef,
deviceRef,
configuration: { tracker_imei: "000000000000001" },
},
),
/safe_projection_contains_unmasked_imei/,
);
});
test("desired configuration binds one exact immutable revision", () => {
const command = normalizeControlResourceManagementCommand(
"device_configuration_desired.set",
{
projectRef,
deviceRef,
configurationRevisionRef: revisionRef,
},
);
assert.equal(command.deviceId, deviceRef.slice("device:".length));
assert.equal(
command.configurationRevisionId,
revisionRef.slice("configuration-revision:".length),
);
assert.equal("applied" in command, false);
});
function bindingInput() {
return {
projectRef,
bindingKey: "robot2b-map",
displayName: "Robot2B map binding",
source: { kind: "collection", ref: collectionRef },
targetKind: "foundry.application",
targetRef: "foundry-application:robot2b-test",
capabilities: ["observe", "inspect"],
};
}
@@ -0,0 +1,104 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const schemaUrl = new URL(
"../migrations/010_device_control_resources.sql",
import.meta.url,
);
const commandsUrl = new URL(
"../migrations/011_device_control_resource_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
const appUrl = new URL("../src/app.mjs", import.meta.url);
test("control resource schema separates bindings, revisions and current state", async () => {
const sql = await readFile(schemaUrl, "utf8");
assert.match(sql, /create table if not exists device_resource_bindings/);
assert.match(sql, /pending_external_approval/);
assert.match(sql, /external_approval_digest/);
assert.match(sql, /device_binding_source_scope_mismatch/);
assert.match(sql, /create table if not exists device_configuration_revisions/);
assert.match(sql, /create table if not exists device_configuration_state/);
assert.match(sql, /device_configuration_revision_scope_mismatch/);
assert.match(sql, /unique \(device_id, revision_number\)/);
});
test("command ledger has honest ordered states without a transport API", async () => {
const sql = await readFile(schemaUrl, "utf8");
const app = await readFile(appUrl, "utf8");
assert.match(sql, /create table if not exists device_commands/);
assert.match(sql, /create table if not exists device_command_events/);
for (const state of [
"draft",
"planned",
"awaiting_confirmation",
"queued",
"dispatched",
"acknowledged",
"verified",
"failed",
"expired",
"unknown",
]) {
assert.match(sql, new RegExp(`'${state}'`));
}
assert.match(sql, /device_command_initial_event_invalid/);
assert.match(sql, /device_command_event_sequence_invalid/);
assert.match(sql, /device_command_event_transition_invalid/);
assert.match(sql, /device_command_event_projection_mismatch/);
assert.match(sql, /device_command_events_current_projection_guard/);
assert.match(sql, /device_command_current_projection_mismatch/);
assert.doesNotMatch(app, /device-commands:(?:plan|confirm|dispatch)/);
});
test("configuration, command history and audit are append-only", async () => {
const sql = await readFile(schemaUrl, "utf8");
for (const table of [
"device_configuration_revisions",
"device_command_events",
"device_audit_events",
]) {
assert.match(
sql,
new RegExp(`${table}_immutable_guard[\\s\\S]*before update or delete or truncate`),
);
}
assert.match(sql, /device_immutable_record_mutation_forbidden/);
assert.match(sql, /device_transfer_active_resource_binding/);
assert.match(sql, /device_transfer_applied_configuration/);
assert.match(sql, /device_transfer_nonterminal_command/);
});
test("control resource schema contains no seeded device or raw secret material", async () => {
const sql = await readFile(schemaUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|gelios|\bb2\b|imei/i);
assert.doesNotMatch(sql, /password\s+text|token\s+text|secret\s+text|raw_command|raw_packet/i);
});
test("commands extend receipts only after their schema", async () => {
const commands = await readFile(commandsUrl, "utf8");
const repository = await readFile(repositoryUrl, "utf8");
for (const kind of [
"device_binding.ensure",
"device_binding.revoke",
"device_configuration_revision.create",
"device_configuration_desired.set",
]) {
assert.match(commands, new RegExp(`'${kind.replace(".", "\\.")}'`));
}
const schemaIndex = repository.indexOf("010_device_control_resources.sql");
const commandsIndex = repository.indexOf(
"011_device_control_resource_commands.sql",
);
assert.notEqual(schemaIndex, -1);
assert.notEqual(commandsIndex, -1);
assert.ok(schemaIndex < commandsIndex);
});
@@ -0,0 +1,313 @@
import assert from "node:assert/strict";
import test from "node:test";
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
import { normalizeManagementActor } from "../src/project-management.mjs";
const now = new Date("2026-08-10T00:00:00.000Z");
const projectId = "11111111-1111-4111-8111-111111111111";
const ownerId = "22222222-2222-4222-8222-222222222222";
const deviceId = "33333333-3333-4333-8333-333333333333";
const collectionId = "44444444-4444-4444-8444-444444444444";
const bindingId = "55555555-5555-4555-8555-555555555555";
const revisionId = "66666666-6666-4666-8666-666666666666";
test("creates only a pending collection binding owned by the source project", async () => {
const actor = managementActor();
const command = normalizeDeviceManagementCommand("device_binding.ensure", {
projectRef: `project:${projectId}`,
bindingKey: "robot2b-map",
displayName: "Robot2B map binding",
source: { kind: "collection", ref: `collection:${collectionId}` },
targetKind: "foundry.application",
targetRef: "foundry-application:robot2b-test",
capabilities: ["observe", "inspect"],
});
const client = scriptedClient([
step("begin"),
receiptStep("receipt-binding"),
projectStep(),
grantsStep(actor),
step("from device_collections", {
rows: [{ id: collectionId, project_id: projectId, lifecycle_state: "active" }],
}),
step("insert into device_resource_bindings", {
rows: [bindingRow({ source_kind: "collection", device_id: null })],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_binding.ensure",
command,
digestCharacter: "a",
}));
assert.equal(result.result.binding.lifecycleState, "pending_external_approval");
assert.equal(result.result.binding.source.ref, `collection:${collectionId}`);
assert.equal("externalApprovalRef" in result.result.binding, false);
assertSafeProjection(result.result);
assert.equal(client.remaining(), 0);
});
test("creates an immutable configuration revision from the active profile schema", async () => {
const actor = managementActor();
const command = createConfigurationCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-configuration-revision"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("from device_model_profiles", {
rows: [{
profile_ref: "vendor.model.protocol.v1",
schema_artifact_ref: "schema:vendor.model.protocol.v1",
lifecycle_state: "active",
}],
}),
step("from device_configuration_revisions", {
rows: [{ next_revision: "1" }],
}),
step("insert into device_configuration_revisions", {
rows: [configurationRevisionRow({
configuration_digest: command.configurationDigest,
configuration: command.configuration,
})],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_configuration_revision.create",
command,
digestCharacter: "b",
}));
assert.equal(result.result.configurationRevision.revisionNumber, 1);
assert.deepEqual(result.result.configurationRevision.configuration, {
reporting_interval_seconds: 15,
});
assert.equal(
result.result.configurationRevision.schemaArtifactRef,
"schema:vendor.model.protocol.v1",
);
assertSafeProjection(result.result);
assert.equal(client.remaining(), 0);
});
test("sets desired configuration without claiming runtime apply", async () => {
const actor = managementActor();
const command = normalizeDeviceManagementCommand(
"device_configuration_desired.set",
{
projectRef: `project:${projectId}`,
deviceRef: `device:${deviceId}`,
configurationRevisionRef: `configuration-revision:${revisionId}`,
},
);
const client = scriptedClient([
step("begin"),
receiptStep("receipt-configuration-desired"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("from device_configuration_revisions", {
rows: [configurationRevisionRow()],
}),
step("from device_configuration_state", { rows: [] }),
step("insert into device_configuration_state", {
rows: [{
device_id: deviceId,
project_id: projectId,
desired_revision_id: revisionId,
applied_revision_id: null,
}],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_configuration_desired.set",
command,
digestCharacter: "c",
}));
assert.equal(result.result.changed, true);
assert.equal(
result.result.configurationState.desiredConfigurationRevisionRef,
`configuration-revision:${revisionId}`,
);
assert.equal(
result.result.configurationState.appliedConfigurationRevisionRef,
null,
);
assert.equal("applied" in result.result, false);
assert.equal(client.remaining(), 0);
});
function createConfigurationCommand() {
return normalizeDeviceManagementCommand(
"device_configuration_revision.create",
{
projectRef: `project:${projectId}`,
deviceRef: `device:${deviceId}`,
configuration: { reporting_interval_seconds: 15 },
changeSummary: "Pilot reporting profile",
},
);
}
function managementActor() {
return normalizeManagementActor({
userRef: "user:device-engineer",
hubRole: "member",
groupRefs: [],
ownerScopes: [],
});
}
function projectStep() {
return step("from device_projects p", {
rows: [{
id: projectId,
owner_scope_id: ownerId,
lifecycle_state: "active",
scope_kind: "company",
owner_ref: "client:example-company",
owner_display_name: "Example Company",
owner_lifecycle_state: "active",
}],
});
}
function grantsStep(actor) {
return step("from device_project_grants", {
rows: [{
id: "77777777-7777-4777-8777-777777777777",
principal_kind: "user",
principal_ref: actor.userRef,
project_role: "engineer",
capability_allow: [],
capability_deny: [],
lifecycle_state: "active",
}],
});
}
function deviceRow() {
return {
id: deviceId,
contour_id: null,
owner_scope_id: ownerId,
project_id: projectId,
model_profile_ref: "vendor.model.protocol.v1",
lifecycle_state: "claimed",
};
}
function bindingRow(overrides = {}) {
return {
id: bindingId,
owner_scope_id: ownerId,
project_id: projectId,
binding_key: "robot2b-map",
display_name: "Robot2B map binding",
source_kind: "device",
device_id: deviceId,
collection_id: collectionId,
target_kind: "foundry.application",
target_ref: "foundry-application:robot2b-test",
capabilities: ["inspect", "observe"],
lifecycle_state: "pending_external_approval",
source_approved_at: now,
created_at: now,
updated_at: now,
created: true,
...overrides,
};
}
function configurationRevisionRow(overrides = {}) {
return {
id: revisionId,
owner_scope_id: ownerId,
project_id: projectId,
device_id: deviceId,
revision_number: "1",
model_profile_ref: "vendor.model.protocol.v1",
schema_artifact_ref: "schema:vendor.model.protocol.v1",
configuration_digest: `sha256:${"d".repeat(64)}`,
configuration: { reporting_interval_seconds: 15 },
change_summary: "Pilot reporting profile",
created_at: now,
...overrides,
};
}
function receiptStep(id) {
return step("insert into device_management_command_receipts", {
rows: [{ id }],
});
}
function commandInput({ actor, commandKind, command, digestCharacter }) {
return {
idempotencyKey: `phase25-${commandKind.replaceAll(".", "-")}-0001`,
commandKind,
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
actor,
command,
};
}
function repositoryWithClient(client) {
return new PostgresDeviceRepository({
pool: {
query: async () => ({ rows: [] }),
connect: async () => client,
end: async () => undefined,
},
});
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
released: false,
async query(sql) {
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolveDeviceDatabaseUrl } from "../src/database-config.mjs";
test("builds the database URL from a file-backed password", async () => {
const password = "test-only-database-password-with-32-bytes";
const url = await resolveDeviceDatabaseUrl(
{
DEVICE_DATABASE_HOST: "device-postgres",
DEVICE_DATABASE_PORT: "5432",
DEVICE_DATABASE_NAME: "device_plane",
DEVICE_DATABASE_USER: "device_plane",
DEVICE_DATABASE_PASSWORD_FILE: "/run/test/postgres-password",
},
async (path, encoding) => {
assert.equal(path, "/run/test/postgres-password");
assert.equal(encoding, "utf8");
return `${password}\n`;
},
);
assert.equal(
url,
`postgresql://device_plane:${encodeURIComponent(password)}@device-postgres:5432/device_plane?sslmode=disable`,
);
});
test("rejects a short file-backed database password", async () => {
await assert.rejects(
resolveDeviceDatabaseUrl(
{
DEVICE_DATABASE_HOST: "device-postgres",
DEVICE_DATABASE_NAME: "device_plane",
DEVICE_DATABASE_USER: "device_plane",
DEVICE_DATABASE_PASSWORD_FILE: "/run/test/postgres-password",
},
async () => "too-short",
),
/device_database_password_invalid/,
);
});
test("keeps an explicit database URL as a compatibility-only boundary", async () => {
const explicit = "postgresql://local:test@127.0.0.1:5432/device_plane";
assert.equal(
await resolveDeviceDatabaseUrl({ DEVICE_DATABASE_URL: explicit }),
explicit,
);
});
@@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/013_device_edge_channels.sql",
import.meta.url,
);
test("Edge channel migration extends the canonical Edge without storing keys", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /alter table device_edges/);
assert.match(sql, /channel_endpoint text/);
assert.match(sql, /channel_generation_ref text/);
assert.match(sql, /channel_trust_bundle_ref text/);
assert.match(sql, /channel_certificate_identities jsonb/);
assert.match(sql, /channel_lifecycle_state in \('disabled', 'active', 'revoked'\)/);
assert.match(sql, /where channel_lifecycle_state = 'active'/);
assert.doesNotMatch(sql, /private[_ ]?key/i);
assert.doesNotMatch(sql, /password/i);
});
@@ -0,0 +1,24 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/015_device_integration_identity.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("integration identity is stored separately from display name and restricted identifiers", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /add column if not exists integration_device_id text/i);
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
});
test("integration identity migration follows registry profile commands", async () => {
const source = await readFile(repositoryUrl, "utf8");
assert.ok(
source.indexOf("014_device_registry_profile_commands.sql")
< source.indexOf("015_device_integration_identity.sql"),
);
});
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/007_device_lifecycle_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("lifecycle command migration extends the durable receipt allowlist", async () => {
const sql = await readFile(migrationUrl, "utf8");
for (const kind of [
"device.claim",
"device.transfer",
"discovery.reject",
"discovery.expire",
]) {
assert.match(sql, new RegExp(`'${kind.replace(".", "\\.")}'`));
}
});
test("lifecycle command migration contains no runtime entity or secret", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
assert.doesNotMatch(sql, /password|secret|credential|private_key/i);
});
test("repository applies lifecycle commands after ownership schema", async () => {
const source = await readFile(repositoryUrl, "utf8");
const lifecycleIndex = source.indexOf("006_device_lifecycle_ownership.sql");
const commandsIndex = source.indexOf("007_device_lifecycle_commands.sql");
assert.notEqual(lifecycleIndex, -1);
assert.notEqual(commandsIndex, -1);
assert.ok(lifecycleIndex < commandsIndex);
});
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/006_device_lifecycle_ownership.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("lifecycle migration separates direct ownership from legacy contours", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /alter column contour_id drop not null/);
assert.match(sql, /add column if not exists owner_scope_id uuid/);
assert.match(sql, /device_instances_project_owner_fk/);
assert.match(sql, /device_instances_ownership_mode_check/);
assert.match(sql, /references device_projects\(id, owner_scope_id\)/);
assert.match(sql, /\) not valid;/);
});
test("route-bound discovery and enrollment evidence are DB constrained", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /device_discoveries_route_context_fk/);
assert.match(sql, /device_discoveries_enrollment_context_fk/);
assert.match(sql, /device_enrollment_observed_discovery_fk/);
assert.match(sql, /device_enrollment_intents_active_identity_idx/);
assert.match(sql, /where lifecycle_state in \('pending', 'observed', 'claimed'\)/);
});
test("ownership history supports transfer without rewriting session provenance", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /create table if not exists device_ownership_transitions/);
assert.match(sql, /transition_kind in \('claim', 'transfer'\)/);
assert.match(sql, /device_ownership_single_claim_idx/);
assert.match(sql, /device_assert_session_current_project/);
assert.match(sql, /device_assert_enrollment_current_project/);
assert.match(sql, /drop constraint if exists device_sessions_device_id_project_id_fkey/);
});
test("lifecycle migration contains no tenant, device, route or credential seed", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
assert.doesNotMatch(sql, /155\.212\.|device\.nodedc\.ru|synology/i);
assert.doesNotMatch(sql, /password|secret|private_key|credential_ref/i);
});
test("repository applies lifecycle migration after registry commands", async () => {
const source = await readFile(repositoryUrl, "utf8");
const commandsIndex = source.indexOf("005_device_registry_commands.sql");
const lifecycleIndex = source.indexOf("006_device_lifecycle_ownership.sql");
assert.notEqual(commandsIndex, -1);
assert.notEqual(lifecycleIndex, -1);
assert.ok(commandsIndex < lifecycleIndex);
});
@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/005_device_registry_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("registry command migration extends the durable receipt allowlist", async () => {
const sql = await readFile(migrationUrl, "utf8");
for (const kind of [
"adapter_package.ensure",
"adapter_version.register",
"model_profile.register",
"edge.ensure",
"route.ensure",
"enrollment_intent.ensure",
]) {
assert.match(sql, new RegExp(`'${kind.replace(".", "\\.")}'`));
}
assert.doesNotMatch(sql, /session\.(ensure|create|upsert)/);
});
test("registry command migration contains no environment or device data", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
assert.doesNotMatch(sql, /password|secret|credential|private_key/i);
});
test("repository applies registry commands after registry schema", async () => {
const source = await readFile(repositoryUrl, "utf8");
const schemaIndex = source.indexOf("004_device_registry_foundation.sql");
const commandsIndex = source.indexOf("005_device_registry_commands.sql");
assert.notEqual(schemaIndex, -1);
assert.notEqual(commandsIndex, -1);
assert.ok(schemaIndex < commandsIndex);
});
@@ -0,0 +1,67 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/004_device_registry_foundation.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("registry migration defines generic catalog, edge and runtime boundaries", async () => {
const sql = await readFile(migrationUrl, "utf8");
for (const table of [
"device_adapter_packages",
"device_adapter_versions",
"device_edges",
"device_routes",
"device_sessions",
"device_enrollment_intents",
]) {
assert.match(sql, new RegExp(`create table if not exists ${table}`));
}
assert.match(sql, /add column if not exists adapter_version_id uuid/);
assert.match(sql, /content_digest ~ '\^sha256:\[a-f0-9\]\{64\}\$'/);
assert.match(sql, /expected_identifier_digest ~ '\^hmac-sha256:\[a-f0-9\]\{64\}\$'/);
});
test("registry migration enforces project, route, edge and device isolation", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(
sql,
/foreign key \(route_id, edge_id, project_id\)\s+references device_routes\(id, edge_id, project_id\)/,
);
assert.match(
sql,
/foreign key \(route_id, project_id, model_profile_ref\)\s+references device_routes\(id, project_id, model_profile_ref\)/,
);
assert.match(
sql,
/foreign key \(device_id, project_id\)\s+references device_instances\(id, project_id\)/,
);
assert.match(
sql,
/foreign key \(claimed_device_id, project_id\)\s+references device_instances\(id, project_id\)/,
);
});
test("registry migration stores no device, tenant, network or credential seed", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
assert.doesNotMatch(sql, /155\.212\.|device\.nodedc\.ru|synology/i);
assert.doesNotMatch(sql, /password|secret|private_key|credential_ref/i);
});
test("repository applies registry migration after management receipts", async () => {
const source = await readFile(repositoryUrl, "utf8");
const managementIndex = source.indexOf("003_device_management_commands.sql");
const registryIndex = source.indexOf("004_device_registry_foundation.sql");
assert.notEqual(managementIndex, -1);
assert.notEqual(registryIndex, -1);
assert.ok(managementIndex < registryIndex);
});
@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/014_device_registry_profile_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("registry profile command migration enables the bounded device update", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /'device\.update'/);
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|\bb2\b|imei|gelios/i);
assert.doesNotMatch(sql, /password|secret|private_key|private-key|token\s*=/i);
});
test("repository applies the profile command after edge channel schema", async () => {
const source = await readFile(repositoryUrl, "utf8");
const edgeChannelIndex = source.indexOf("013_device_edge_channels.sql");
const profileCommandIndex = source.indexOf("014_device_registry_profile_commands.sql");
assert.notEqual(edgeChannelIndex, -1);
assert.notEqual(profileCommandIndex, -1);
assert.ok(edgeChannelIndex < profileCommandIndex);
});
@@ -0,0 +1,199 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
import { observeQuarantineDiscovery } from "../src/discovery-repository.mjs";
const observedAt = "2026-08-10T00:00:00.000Z";
const projectId = "11111111-1111-4111-8111-111111111111";
const routeId = "22222222-2222-4222-8222-222222222222";
const enrollmentId = "33333333-3333-4333-8333-333333333333";
const discoveryId = "44444444-4444-4444-8444-444444444444";
const identifierDigest = `hmac-sha256:${"a".repeat(64)}`;
test("observation expires stale intents before matching an identity", async () => {
const source = await readFile(
new URL("../src/discovery-repository.mjs", import.meta.url),
"utf8",
);
assert.match(source, /expires_at <= \$6/);
assert.match(source, /expires_at is null or expires_at > \$6/);
assert.match(source, /resolution_code = 'deadline_elapsed'/);
});
test("legacy discovery remains unbound quarantine without a route reference", async () => {
const client = scriptedClient([
step("begin"),
step("insert into device_discoveries", {
rows: [discoveryRow({ project_id: null, route_id: null })],
}),
step("commit"),
]);
const result = await observeQuarantineDiscovery({
pool: poolWithClient(client),
identifierDigest,
safeView: safeView(),
sessionRef: "session:legacy-test",
});
assert.equal(result.created, true);
assert.equal("routeRef" in result.value, false);
assert.equal("enrollmentIntentRef" in result.value, false);
assertSafeProjection(result.value);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("route-bound discovery atomically observes only its matching enrollment", async () => {
const client = scriptedClient([
step("begin"),
step("from device_routes", {
rows: [{
id: routeId,
project_id: projectId,
model_profile_ref: "vendor.model.protocol.v1",
protocol: "GENERIC_TCP",
lifecycle_state: "active",
}],
}),
step("update device_enrollment_intents"),
step("from device_enrollment_intents", {
rows: [{
id: enrollmentId,
project_id: projectId,
route_id: routeId,
model_profile_ref: "vendor.model.protocol.v1",
lifecycle_state: "pending",
}],
}),
step("insert into device_discoveries", {
rows: [discoveryRow({
project_id: projectId,
route_id: routeId,
enrollment_intent_id: enrollmentId,
})],
}),
step("update device_enrollment_intents", {
rows: [{ id: enrollmentId }],
}),
step("commit"),
]);
const result = await observeQuarantineDiscovery({
pool: poolWithClient(client),
identifierDigest,
safeView: safeView(`route:${routeId}`),
sessionRef: "session:route-test",
routeRef: `route:${routeId}`,
});
assert.equal(result.value.routeRef, `route:${routeId}`);
assert.equal(
result.value.enrollmentIntentRef,
`enrollment-intent:${enrollmentId}`,
);
assertSafeProjection(result.value);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("inactive or mismatched routes fail before a discovery is stored", async () => {
const client = scriptedClient([
step("begin"),
step("from device_routes", {
rows: [{
id: routeId,
project_id: projectId,
model_profile_ref: "other.profile.v1",
protocol: "OTHER_TCP",
lifecycle_state: "active",
}],
}),
step("rollback"),
]);
await assert.rejects(
observeQuarantineDiscovery({
pool: poolWithClient(client),
identifierDigest,
safeView: safeView(`route:${routeId}`),
sessionRef: "session:mismatch-test",
routeRef: `route:${routeId}`,
}),
/device_discovery_route_profile_mismatch/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
function safeView(routeRef = null) {
return {
schemaVersion: "nodedc.device.discovery-view.v1",
...(routeRef ? { routeRef } : {}),
modelProfileRef: "vendor.model.protocol.v1",
protocol: "GENERIC_TCP",
observedAt,
lifecycleState: "quarantine",
identifier: { kind: "serial", masked: "********0001" },
evidence: {
transport: "tcp",
bytesObserved: 32,
framingStatus: "verified",
specificationRef: "vendor.protocol.v1",
},
commandTransport: "disabled",
};
}
function discoveryRow(overrides = {}) {
return {
id: discoveryId,
lifecycle_state: "quarantine",
model_profile_ref: "vendor.model.protocol.v1",
protocol: "GENERIC_TCP",
identifier_kind: "serial",
identifier_masked: "********0001",
first_observed_at: new Date(observedAt),
last_observed_at: new Date(observedAt),
evidence: safeView().evidence,
project_id: null,
route_id: null,
enrollment_intent_id: null,
created: true,
...overrides,
};
}
function poolWithClient(client) {
return { connect: async () => client };
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
released: false,
async query(sql) {
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,130 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
createDeviceEdgeChannelSupervisor,
} from "../src/edge-channel-supervisor.mjs";
test("supervisor reconciles one in-process client per active Edge", async () => {
let registrations = [registration("channel-generation:1")];
const clients = [];
const ingestCalls = [];
const supervisor = createDeviceEdgeChannelSupervisor({
repository: {
listActiveEdgeChannelRegistrations: async () => registrations,
},
gatewayIngest: {
observeDiscovery: async (...args) => ingestCalls.push(["discovery", ...args]),
acceptMessage: async (...args) => ingestCalls.push(["message", ...args]),
},
coreIdentity: {
identityRef: "workload:device-control-core",
key: "test-key",
cert: "test-cert",
},
readPeerTrust: async () => "test-edge-certificate",
clientFactory: (options) => {
const state = { started: 0, stopped: 0, options };
clients.push(state);
return {
async start() { state.started += 1; },
async stop() { state.stopped += 1; },
status: () => ({ channel: "accepted", lastErrorCode: null }),
};
},
reconcileIntervalMs: 300_000,
});
await supervisor.start();
assert.equal(clients.length, 1);
assert.equal(supervisor.status().accepted, 1);
assert.equal(clients[0].options.registration.channelGeneration, "channel-generation:1");
await clients[0].options.observeDiscovery({ signal: true });
await clients[0].options.acceptMessage({ message: true });
assert.deepEqual(ingestCalls, [
[
"discovery",
{ signal: true },
{ authenticatedEdgeRef: "edge:pilot" },
],
[
"message",
{ message: true },
{ authenticatedEdgeRef: "edge:pilot" },
],
]);
await supervisor.reconcile();
assert.equal(clients.length, 1);
registrations = [registration("channel-generation:2")];
await supervisor.reconcile();
assert.equal(clients.length, 2);
assert.equal(clients[0].stopped, 1);
assert.equal(clients[1].started, 1);
registrations = [];
await supervisor.reconcile();
assert.equal(clients[1].stopped, 1);
assert.equal(supervisor.status().configured, 0);
assert.equal(JSON.stringify(supervisor.status()).includes("155.212"), false);
await supervisor.stop();
});
test("supervisor keeps a failed trust enrollment isolated from other Edges", async () => {
const supervisor = createDeviceEdgeChannelSupervisor({
repository: {
listActiveEdgeChannelRegistrations: async () => [
registration("channel-generation:1", "edge:good"),
registration("channel-generation:1", "edge:bad"),
],
},
gatewayIngest: {
observeDiscovery: async () => ({}),
acceptMessage: async () => ({}),
},
coreIdentity: {
identityRef: "workload:device-control-core",
key: "test-key",
cert: "test-cert",
},
readPeerTrust: async ({ registration: value }) => {
if (value.edgeRegistrationId === "edge:bad") {
throw new Error("device_edge_channel_trust_bundle_identity_mismatch");
}
return "test-edge-certificate";
},
clientFactory: () => ({
start: async () => undefined,
stop: async () => undefined,
status: () => ({ channel: "accepted", lastErrorCode: null }),
}),
reconcileIntervalMs: 300_000,
});
await supervisor.start();
const status = supervisor.status();
assert.equal(status.configured, 2);
assert.equal(status.accepted, 1);
assert.equal(status.degraded, 1);
assert.equal(status.commandTransport, "disabled");
assert.equal(status.edges.find((item) => item.edgeRegistrationId === "edge:bad")
.lastErrorCode, "device_edge_channel_trust_bundle_identity_mismatch");
await supervisor.stop();
});
function registration(channelGeneration, edgeRegistrationId = "edge:pilot") {
return {
edgeRegistrationId,
endpoint: "https://155.212.211.15/",
servername: "155.212.211.15",
channelGeneration,
trustBundleRef: "edge-trust:moscow-edge",
certificateIdentities: [{
generationRef: "edge-identity:1",
fingerprint: "AA:".repeat(31) + "AA",
status: "active",
}],
lifecycleState: "active",
};
}
@@ -0,0 +1,143 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
DEVICE_ADAPTER_MESSAGE_SCHEMA,
DEVICE_DISCOVERY_SIGNAL_SCHEMA,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import { createDeviceGatewayIngest } from "../src/gateway-ingest.mjs";
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
const rawImei = "000000000000001";
test("shared gateway ingest masks identifiers for HTTP and Edge callers", async () => {
const stored = [];
const ingest = createDeviceGatewayIngest({
identifierPepper,
repository: {
async upsertQuarantineDiscovery(value) {
stored.push(value);
return {
created: true,
value: { ...value.safeView, discoveryRef: "discovery:test" },
};
},
async acceptAdapterMessage(value) {
stored.push(value);
return {
acceptance: {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: "acceptance:test",
idempotencyKey: value.safeView.idempotencyKey,
status: "accepted",
replayed: false,
acceptedAt: "2026-08-11T12:00:00.000Z",
},
claimedDeviceRef: "device:11111111-1111-4111-8111-111111111111",
};
},
},
});
const discovery = await ingest.observeDiscovery(discoverySignal());
const acceptance = await ingest.acceptMessage(adapterMessage());
assert.equal(discovery.value.identifier.masked, "***********0001");
assert.equal(acceptance.value.status, "accepted");
assert.equal(
acceptance.claimedDeviceRef,
"device:11111111-1111-4111-8111-111111111111",
);
assert.match(stored[0].identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
assert.match(stored[1].requestDigest, /^sha256:[a-f0-9]{64}$/);
assert.equal(JSON.stringify(stored).includes(rawImei), false);
});
test("authenticated Edge identity resolves the allowlisted project route", async () => {
const stored = [];
const resolutions = [];
const authenticatedEdgeRef = "edge:11111111-1111-4111-8111-111111111111";
const routeRef = "route:22222222-2222-4222-8222-222222222222";
const ingest = createDeviceGatewayIngest({
identifierPepper,
repository: {
async resolveInboundRoute(value) {
resolutions.push(value);
return routeRef;
},
async upsertQuarantineDiscovery(value) {
stored.push(value);
return {
created: false,
value: { ...value.safeView, discoveryRef: "discovery:test" },
};
},
async acceptAdapterMessage(value) {
stored.push(value);
return {
acceptance: {
schemaVersion: "nodedc.device-adapter-acceptance.v1",
acceptanceRef: "acceptance:test",
idempotencyKey: value.safeView.idempotencyKey,
status: "accepted",
replayed: false,
acceptedAt: "2026-08-11T12:00:00.000Z",
},
claimedDeviceRef: null,
};
},
},
});
await ingest.observeDiscovery(discoverySignal(), { authenticatedEdgeRef });
await ingest.acceptMessage(adapterMessage(), { authenticatedEdgeRef });
assert.equal(resolutions.length, 2);
assert.equal(resolutions[0].edgeRef, authenticatedEdgeRef);
assert.match(resolutions[0].identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
assert.equal(stored[0].safeView.routeRef, routeRef);
assert.equal(stored[1].safeView.routeRef, routeRef);
assert.equal(stored[1].safeView.edgeRef, authenticatedEdgeRef);
assert.notEqual(stored[1].safeView.edgeRef, adapterMessage().edgeRef);
assert.equal(JSON.stringify(resolutions).includes(rawImei), false);
});
function discoverySignal() {
return {
schemaVersion: DEVICE_DISCOVERY_SIGNAL_SCHEMA,
sessionRef: "session:test",
modelProfileRef: "arusnavi.b2.internal.v1",
protocol: "INTERNAL",
observedAt: "2026-08-11T12:00:00.000Z",
identifier: { kind: "imei", value: rawImei },
evidence: {
transport: "tcp",
bytesObserved: 16,
framingStatus: "verified",
specificationRef: "arusnavi.internal.framing.test-v1",
},
};
}
function adapterMessage() {
return {
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
edgeRef: "edge:test",
adapterRef: "arusnavi-b2",
protocolProfileRef: "arusnavi.b2.internal.v1",
protocol: "INTERNAL",
sessionRef: "session:test",
messageRef: "package:1:test",
messageType: "telemetry.package",
sequence: 1,
observedAt: "2026-08-11T12:00:00.000Z",
idempotencyKey: `sha256:${"a".repeat(64)}`,
identifier: { kind: "imei", value: rawImei },
payloadSchemaRef: "arusnavi.internal.package-metadata.v1",
payload: {
packageNumber: 1,
packetCount: 1,
packageDigest: `sha256:${"b".repeat(64)}`,
},
};
}
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/012_device_gateway_message_receipts.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("gateway receipts persist only typed bounded Core acceptance evidence", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /create table if not exists device_gateway_message_receipts/);
assert.match(sql, /unique \(idempotency_key\)/);
assert.match(sql, /unique \(edge_ref, session_ref, message_ref\)/);
assert.match(sql, /identifier_digest text not null/);
assert.match(sql, /identifier_masked text not null/);
assert.match(sql, /payload_schema_ref text not null/);
assert.match(sql, /payload jsonb not null/);
assert.match(sql, /device_gateway_message_receipts_immutable_guard/);
assert.match(sql, /execute function device_reject_immutable_mutation\(\)/);
assert.doesNotMatch(sql, /execute function reject_device_immutable_record_mutation\(\)/);
assert.doesNotMatch(sql, /raw_packet|raw_identifier|password|token|secret/i);
assert.doesNotMatch(sql, /insert\s+into|arusnavi|gelios|\bb2\b|imei/i);
});
test("gateway receipt migration follows the generic control resource schema", async () => {
const repository = await readFile(repositoryUrl, "utf8");
const controlResourceIndex = repository.indexOf(
"011_device_control_resource_commands.sql",
);
const gatewayReceiptIndex = repository.indexOf(
"012_device_gateway_message_receipts.sql",
);
assert.notEqual(controlResourceIndex, -1);
assert.notEqual(gatewayReceiptIndex, -1);
assert.ok(controlResourceIndex < gatewayReceiptIndex);
assert.doesNotMatch(repository, /arusnavi-b2-adapter/);
});
@@ -0,0 +1,166 @@
import assert from "node:assert/strict";
import test from "node:test";
import { acceptGatewayMessage } from "../src/gateway-message-repository.mjs";
const acceptedAt = new Date("2026-08-11T12:00:00.000Z");
const idempotencyKey = `sha256:${"a".repeat(64)}`;
const requestDigest = `sha256:${"b".repeat(64)}`;
test("commits a gateway receipt before returning Core acceptance", async () => {
const client = scriptedClient([
step("begin"),
step("insert into device_gateway_message_receipts", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
idempotency_key: idempotencyKey,
accepted_at: acceptedAt,
}],
}),
step("commit"),
]);
const result = await acceptGatewayMessage(messageInput(client));
assert.equal(result.acceptance.status, "accepted");
assert.equal(result.acceptance.replayed, false);
assert.equal(result.acceptance.idempotencyKey, idempotencyKey);
assert.equal(result.acceptance.acceptedAt, acceptedAt.toISOString());
assert.equal(result.claimedDeviceRef, null);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("replays one durable receipt for the same normalized request", async () => {
const client = scriptedClient([
step("begin"),
step("insert into device_gateway_message_receipts", { rows: [] }),
step("from device_gateway_message_receipts", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
idempotency_key: idempotencyKey,
request_digest: requestDigest,
accepted_at: acceptedAt,
}],
}),
step("commit"),
]);
const result = await acceptGatewayMessage(messageInput(client));
assert.equal(result.acceptance.status, "accepted");
assert.equal(result.acceptance.replayed, true);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("rejects idempotency reuse with different content", async () => {
const client = scriptedClient([
step("begin"),
step("insert into device_gateway_message_receipts", { rows: [] }),
step("from device_gateway_message_receipts", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
idempotency_key: idempotencyKey,
request_digest: `sha256:${"c".repeat(64)}`,
accepted_at: acceptedAt,
}],
}),
step("rollback"),
]);
await assert.rejects(
acceptGatewayMessage(messageInput(client)),
/device_gateway_idempotency_conflict/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("fails closed when a route does not match its Edge contract", async () => {
const routeId = "22222222-2222-4222-8222-222222222222";
const client = scriptedClient([
step("begin"),
step("from device_routes r", {
rows: [{
id: routeId,
project_id: "33333333-3333-4333-8333-333333333333",
edge_id: "44444444-4444-4444-8444-444444444444",
model_profile_ref: "generic.model.protocol.v1",
protocol: "GENERIC_TCP",
lifecycle_state: "active",
edge_lifecycle_state: "active",
profile_lifecycle_state: "active",
adapter_ref: "generic-adapter",
adapter_lifecycle_state: "active",
adapter_version_lifecycle_state: "active",
}],
}),
step("rollback"),
]);
const input = messageInput(client);
input.safeView.routeRef = `route:${routeId}`;
input.safeView.edgeRef = "edge:55555555-5555-4555-8555-555555555555";
await assert.rejects(
acceptGatewayMessage(input),
/device_gateway_route_contract_mismatch/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
function messageInput(client) {
return {
pool: {
connect: async () => client,
},
identifierDigest: `hmac-sha256:${"d".repeat(64)}`,
requestDigest,
safeView: {
edgeRef: "edge:test-001",
adapterRef: "generic-adapter",
protocolProfileRef: "generic.model.protocol.v1",
protocol: "GENERIC_TCP",
sessionRef: "session:test-001",
messageRef: "message:test-001",
messageType: "telemetry.sample",
sequence: 1,
idempotencyKey,
identifier: {
kind: "serial",
masked: "********0001",
},
payloadSchemaRef: "generic.telemetry.v1",
payload: { value: 1 },
observedAt: "2026-08-11T12:00:00.000Z",
},
};
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
released: false,
async query(sql) {
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
resolveInboundRoute,
} from "../src/inbound-route-repository.mjs";
const edgeRef = "edge:11111111-1111-4111-8111-111111111111";
const routeId = "22222222-2222-4222-8222-222222222222";
const identifierDigest = `hmac-sha256:${"a".repeat(64)}`;
test("inbound route resolution is scoped by authenticated Edge and enrollment", async () => {
const queries = [];
const client = {
async query(sql, values) {
queries.push({ sql, values });
return { rows: [{ id: routeId }] };
},
};
const result = await resolveInboundRoute(client, input());
assert.equal(result, `route:${routeId}`);
assert.equal(queries.length, 1);
assert.match(queries[0].sql, /device_enrollment_intents/);
assert.match(queries[0].sql, /r\.edge_id = \$1/);
assert.match(queries[0].sql, /ei\.expected_identifier_digest = \$5/);
assert.deepEqual(queries[0].values, [
edgeRef.slice("edge:".length),
"arusnavi.b2.internal.v1",
"INTERNAL",
"imei",
identifierDigest,
"2026-08-13T09:00:00.000Z",
]);
});
test("inbound route resolution leaves unknown identifiers quarantined", async () => {
const result = await resolveInboundRoute(
{ query: async () => ({ rows: [] }) },
input(),
);
assert.equal(result, null);
});
test("inbound route resolution fails closed on ambiguous ownership", async () => {
await assert.rejects(
() => resolveInboundRoute(
{ query: async () => ({ rows: [{ id: routeId }, { id: routeId }] }) },
input(),
),
(error) => {
assert.equal(error.message, "device_inbound_route_ambiguous");
assert.equal(error.statusCode, 409);
return true;
},
);
});
function input() {
return {
edgeRef,
modelProfileRef: "arusnavi.b2.internal.v1",
protocol: "INTERNAL",
identifierKind: "imei",
identifierDigest,
observedAt: "2026-08-13T09:00:00.000Z",
};
}
@@ -0,0 +1,322 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createControlCoreApp } from "../src/app.mjs";
const managementToken = "test-only-management-token-with-32-bytes";
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
test("management API forwards a normalized generic Edge registration", async () => {
let executed;
const runtime = await startServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async (input) => {
executed = input;
return { replayed: false, result: { created: true } };
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/edges:ensure`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify({
edgeKey: "generic-edge",
displayName: "Generic Edge",
deploymentRef: "deployment:device-edge/pilot",
}),
},
);
assert.equal(response.status, 200);
assert.equal(executed.commandKind, "edge.ensure");
assert.deepEqual(executed.command, {
edgeKey: "generic-edge",
displayName: "Generic Edge",
deploymentRef: "deployment:device-edge/pilot",
lifecycleState: "provisioning",
});
assert.equal(executed.actor.hubRole, "owner");
} finally {
await runtime.close();
}
});
test("management API exposes no user-owned session mutation", async () => {
let executions = 0;
const runtime = await startServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => {
executions += 1;
return { replayed: false, result: {} };
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/sessions:ensure`,
{
method: "POST",
headers: managementHeaders(),
body: "{}",
},
);
assert.equal(response.status, 404);
assert.equal(executions, 0);
} finally {
await runtime.close();
}
});
test("management API forwards claim as evidence references without identity input", async () => {
let executed;
const runtime = await startServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async (input) => {
executed = input;
return { replayed: false, result: { created: true } };
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/devices:claim`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify({
projectRef: "project:11111111-1111-4111-8111-111111111111",
enrollmentIntentRef:
"enrollment-intent:22222222-2222-4222-8222-222222222222",
discoveryRef: "discovery:33333333-3333-4333-8333-333333333333",
deviceKey: "pilot-device",
displayName: "Pilot device",
}),
},
);
assert.equal(response.status, 200);
assert.equal(executed.commandKind, "device.claim");
assert.equal(executed.command.deviceKey, "pilot-device");
assert.equal("identifier" in executed.command, false);
assert.equal("credentialRef" in executed.command, false);
} finally {
await runtime.close();
}
});
test("management API derives enrollment identity inside Core and never forwards raw IMEI", async () => {
let executed;
const runtime = await startServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async (input) => {
executed = input;
return {
replayed: false,
result: {
enrollmentIntent: {
identifier: {
kind: input.command.identifierKind,
masked: input.command.identifierMasked,
},
},
},
};
},
},
});
try {
const rawImei = "123456789012345";
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/enrollment-intents:ensure`,
{
method: "POST",
headers: {
...managementHeaders(),
"Idempotency-Key": "phase25-enrollment-0001",
},
body: JSON.stringify({
projectRef: "project:11111111-1111-4111-8111-111111111111",
enrollmentKey: "pilot-device",
routeRef: "route:22222222-2222-4222-8222-222222222222",
modelProfileRef: "arusnavi.b2.v1",
displayName: "Pilot device",
identifier: { kind: "imei", value: rawImei },
expiresAt: null,
}),
},
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(executed.command.identifierKind, "imei");
assert.equal(executed.command.identifierMasked, "***********2345");
assert.match(executed.command.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
assert.equal(JSON.stringify(executed).includes(rawImei), false);
assert.equal(JSON.stringify(body).includes(rawImei), false);
} finally {
await runtime.close();
}
});
test("management API rejects client-supplied enrollment digests", async () => {
let executions = 0;
const runtime = await startServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async () => {
executions += 1;
return { replayed: false, result: {} };
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/enrollment-intents:ensure`,
{
method: "POST",
headers: {
...managementHeaders(),
"Idempotency-Key": "phase25-enrollment-reject-0001",
},
body: JSON.stringify({
projectRef: "project:11111111-1111-4111-8111-111111111111",
enrollmentKey: "pilot-device",
routeRef: "route:22222222-2222-4222-8222-222222222222",
modelProfileRef: "arusnavi.b2.v1",
displayName: "Pilot device",
identifier: { kind: "imei", value: "123456789012345" },
identifierDigest: `hmac-sha256:${"a".repeat(64)}`,
}),
},
);
assert.equal(response.status, 400);
assert.equal(
(await response.json()).error,
"device_enrollment_input_field_unexpected",
);
assert.equal(executions, 0);
} finally {
await runtime.close();
}
});
test("management API accepts only a canonical credential reference", async () => {
let executed;
const runtime = await startServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async (input) => {
executed = input;
return {
replayed: false,
result: {
credentialBinding: {
credentialBindingRef:
"credential-binding:44444444-4444-4444-8444-444444444444",
},
},
};
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/device-credential-bindings:upsert`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify({
projectRef: "project:11111111-1111-4111-8111-111111111111",
deviceRef: "device:22222222-2222-4222-8222-222222222222",
purpose: "tracker.command",
credentialRef: {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
},
}),
},
);
assert.equal(response.status, 200);
assert.equal(
executed.commandKind,
"device_credential_binding.upsert",
);
assert.deepEqual(executed.command.credentialRef, {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
});
const rejected = await fetch(
`${runtime.baseUrl}/internal/v1/management/device-credential-bindings:upsert`,
{
method: "POST",
headers: {
...managementHeaders(),
"Idempotency-Key": "phase24-credential-invalid-0001",
},
body: JSON.stringify({
projectRef: "project:11111111-1111-4111-8111-111111111111",
deviceRef: "device:22222222-2222-4222-8222-222222222222",
purpose: "tracker.command",
credentialRef: {
owner: "device_core",
reference: "ndc-credref:pilot-command-0001",
},
}),
},
);
assert.equal(rejected.status, 400);
assert.equal(
(await rejected.json()).error,
"ndc_credential_reference_owner_invalid",
);
} finally {
await runtime.close();
}
});
async function startServer(options) {
const server = createControlCoreApp({ identifierPepper, ...options });
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
return {
baseUrl: `http://127.0.0.1:${address.port}`,
close: () => new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
}),
};
}
function managementHeaders() {
return {
Authorization: `Bearer ${managementToken}`,
"Content-Type": "application/json",
"Idempotency-Key": "phase23-edge-0001",
"X-NODEDC-User-Ref": "user:platform-owner",
"X-NODEDC-Hub-Role": "owner",
};
}
@@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const repositorySource = new URL(
"../src/infrastructure-repository.mjs",
import.meta.url,
);
test("catalog, Edge and route upserts enforce irreversible lifecycle transitions", async () => {
const source = await readFile(repositorySource, "utf8");
assert.match(source, /device_adapter_versions\.lifecycle_state = 'draft'[\s\S]*excluded\.lifecycle_state in \('active', 'retired'\)/);
assert.match(source, /device_model_profiles\.lifecycle_state = 'active'[\s\S]*excluded\.lifecycle_state = 'retired'/);
assert.match(source, /device_edges\.lifecycle_state = 'suspended'[\s\S]*excluded\.lifecycle_state in \('active', 'retired'\)/);
assert.match(source, /device_routes\.lifecycle_state = 'draft'[\s\S]*excluded\.lifecycle_state in \('active', 'retired'\)/);
assert.doesNotMatch(source, /device_(?:adapter_versions|model_profiles|edges|routes)\.lifecycle_state = 'retired'[\s\S]{0,160}excluded\.lifecycle_state = 'active'/);
});
test("legacy model adoption is a one-way exact-identity registry transition", async () => {
const source = await readFile(
new URL("../src/infrastructure-repository.mjs", import.meta.url),
"utf8",
);
assert.match(source, /profile ->> 'schemaVersion' = excluded\.schema_version/);
assert.match(source, /profile ->> 'profileRef' = excluded\.profile_ref/);
assert.match(source, /profile ->> 'vendor' = excluded\.vendor/);
assert.match(source, /profile ->> 'model' = excluded\.model/);
assert.match(source, /profile ->> 'deviceType' = excluded\.device_type/);
assert.match(source, /profile ->> 'protocol' = excluded\.protocol/);
assert.match(source, /adapter_version_id is null[\s\S]*schema_artifact_ref is null[\s\S]*profile_digest is null[\s\S]*cardinality\(device_model_profiles\.capabilities\) = 0[\s\S]*lifecycle_state = 'active'[\s\S]*excluded\.lifecycle_state = 'draft'/);
});
test("adopted rich profile lifecycle changes preserve exact registry identity", async () => {
const source = await readFile(
new URL("../src/infrastructure-repository.mjs", import.meta.url),
"utf8",
);
assert.match(source, /profile = excluded\.profile[\s\S]*or \([\s\S]*profile ->> 'schemaVersion' = excluded\.schema_version[\s\S]*profile ->> 'profileRef' = excluded\.profile_ref[\s\S]*profile ->> 'vendor' = excluded\.vendor[\s\S]*profile ->> 'model' = excluded\.model[\s\S]*profile ->> 'deviceType' = excluded\.device_type[\s\S]*profile ->> 'protocol' = excluded\.protocol/);
assert.match(source, /adapter_version_id = excluded\.adapter_version_id[\s\S]*schema_artifact_ref = excluded\.schema_artifact_ref[\s\S]*profile_digest = excluded\.profile_digest[\s\S]*capabilities = excluded\.capabilities/);
});
@@ -0,0 +1,210 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
assertPlatformCatalogAuthority,
DEVICE_INFRASTRUCTURE_COMMAND_KINDS,
normalizeInfrastructureManagementCommand,
} from "../src/infrastructure-management.mjs";
import {
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
normalizeDeviceManagementCommand,
} from "../src/management-command.mjs";
import { normalizeManagementActor } from "../src/project-management.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const packageRef = "adapter-package:22222222-2222-4222-8222-222222222222";
const versionRef = "adapter-version:33333333-3333-4333-8333-333333333333";
const edgeRef = "edge:44444444-4444-4444-8444-444444444444";
const routeRef = "route:55555555-5555-4555-8555-555555555555";
const digest = `sha256:${"a".repeat(64)}`;
const identifierDigest = `hmac-sha256:${"b".repeat(64)}`;
test("aggregates project and infrastructure commands without a session mutation", () => {
for (const kind of DEVICE_INFRASTRUCTURE_COMMAND_KINDS) {
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
}
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes("project.ensure"), true);
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes("session.ensure"), false);
assert.throws(
() => normalizeDeviceManagementCommand("session.ensure", {}),
/device_management_command_kind_invalid/,
);
});
test("normalizes immutable adapter version metadata and sorted capabilities", () => {
const command = normalizeInfrastructureManagementCommand(
"adapter_version.register",
{
adapterPackageRef: packageRef,
version: "1.2.3",
runtimePackageRef: "artifact:device-adapters/generic-1.2.3",
contentDigest: digest,
contractVersion: "nodedc.device-adapter.v1",
capabilities: ["telemetry.observe", "command.typed", "telemetry.observe"],
lifecycleState: "active",
},
);
assert.equal(command.adapterPackageId, packageRef.slice("adapter-package:".length));
assert.deepEqual(command.capabilities, ["command.typed", "telemetry.observe"]);
assert.equal(command.contentDigest, digest);
});
test("normalizes a generic model profile as artifact metadata, not executable payload", () => {
const command = normalizeInfrastructureManagementCommand(
"model_profile.register",
{
adapterVersionRef: versionRef,
profileRef: "vendor.model.protocol.v1",
schemaVersion: "nodedc.device-model-profile.v1",
vendor: "Example Vendor",
model: "Model One",
deviceType: "tracker",
protocol: "GENERIC_TCP",
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
profileDigest: digest,
capabilities: ["telemetry.observe"],
},
);
assert.equal(command.adapterVersionId, versionRef.slice("adapter-version:".length));
assert.equal(command.protocol, "GENERIC_TCP");
assert.equal(command.lifecycleState, "draft");
assert.equal("profile" in command, false);
assert.equal("source" in command, false);
});
test("route and enrollment commands resolve only scoped references", () => {
const route = normalizeInfrastructureManagementCommand("route.ensure", {
projectRef,
routeKey: "primary-ingress",
displayName: "Primary ingress",
edgeRef,
modelProfileRef: "vendor.model.protocol.v1",
listenerRef: "listener:generic-tcp-primary",
protocol: "GENERIC_TCP",
direction: "bidirectional",
});
const enrollment = normalizeInfrastructureManagementCommand(
"enrollment_intent.ensure",
{
projectRef,
enrollmentKey: "pilot-device",
routeRef,
modelProfileRef: "vendor.model.protocol.v1",
displayName: "Pilot device",
identifierKind: "serial",
identifierDigest,
identifierMasked: "********0001",
expiresAt: "2026-09-01T00:00:00.000Z",
},
);
assert.equal(route.projectId, projectRef.slice("project:".length));
assert.equal(route.edgeId, edgeRef.slice("edge:".length));
assert.equal(enrollment.routeId, routeRef.slice("route:".length));
assert.equal(enrollment.identifierDigest, identifierDigest);
});
test("enrollment contract rejects raw identifiers and credential-shaped fields", () => {
const base = {
projectRef,
enrollmentKey: "pilot-device",
routeRef,
modelProfileRef: "vendor.model.protocol.v1",
displayName: "Pilot device",
identifierKind: "imei",
identifierDigest,
identifierMasked: "***********0001",
};
assert.throws(
() => normalizeInfrastructureManagementCommand(
"enrollment_intent.ensure",
{ ...base, identifierMasked: "000000000000001" },
),
/safe_projection_contains_unmasked_imei/,
);
assert.throws(
() => normalizeInfrastructureManagementCommand(
"enrollment_intent.ensure",
{ ...base, credential: "forbidden" },
),
/device_management_command_field_unexpected:credential/,
);
});
test("shared catalog and Edge authority requires the Hub owner ceiling", () => {
assert.doesNotThrow(() => assertPlatformCatalogAuthority(actor("owner")));
assert.throws(
() => assertPlatformCatalogAuthority(actor("admin")),
/device_platform_catalog_access_denied/,
);
});
test("normalizes only a pinned Core-initiated public Edge channel", () => {
const command = normalizeInfrastructureManagementCommand("edge.ensure", {
edgeKey: "moscow-edge",
displayName: "Moscow Edge",
deploymentRef: "deployment:device-edge/moscow-1",
lifecycleState: "active",
channel: {
endpoint: "https://155.212.211.15/",
servername: "155.212.211.15",
generationRef: "channel-generation:1",
trustBundleRef: "edge-trust:moscow-edge",
certificateIdentities: [{
generationRef: "edge-identity:1",
fingerprint: "AA:".repeat(31) + "AA",
status: "active",
}],
lifecycleState: "active",
},
});
assert.equal(command.channel.endpoint, "https://155.212.211.15/");
assert.equal(command.channel.lifecycleState, "active");
assert.equal(command.channel.certificateIdentities.length, 1);
for (const endpoint of [
"https://127.0.0.1/",
"https://192.168.1.1/",
"https://155.212.211.15:8443/",
"https://155.212.211.15:9921/",
"http://155.212.211.15/",
]) {
assert.throws(
() => normalizeInfrastructureManagementCommand("edge.ensure", {
edgeKey: "bad-edge",
displayName: "Bad Edge",
channel: { ...command.channel, endpoint },
}),
/device_edge_channel_endpoint_invalid/,
);
}
assert.throws(
() => normalizeInfrastructureManagementCommand("edge.ensure", {
edgeKey: "bad-edge",
displayName: "Bad Edge",
channel: { ...command.channel, servername: "example.invalid" },
}),
/device_edge_channel_servername_mismatch/,
);
assert.throws(
() => normalizeInfrastructureManagementCommand("edge.ensure", {
edgeKey: "bad-edge",
displayName: "Bad Edge",
channel: { lifecycleState: "disabled", endpoint: command.channel.endpoint },
}),
/device_edge_channel_disabled_configuration_invalid/,
);
});
function actor(hubRole) {
return normalizeManagementActor({
userRef: "user:platform-admin",
hubRole,
groupRefs: [],
ownerScopes: [],
});
}
@@ -0,0 +1,516 @@
import assert from "node:assert/strict";
import test from "node:test";
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
import { normalizeManagementActor } from "../src/project-management.mjs";
const now = new Date("2026-08-10T00:00:00.000Z");
const projectId = "11111111-1111-4111-8111-111111111111";
const edgeId = "22222222-2222-4222-8222-222222222222";
const routeId = "33333333-3333-4333-8333-333333333333";
const adapterPackageId = "44444444-4444-4444-8444-444444444444";
const adapterVersionId = "55555555-5555-4555-8555-555555555555";
test("commits an owner-authorized generic Edge registration", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("edge.ensure", {
edgeKey: "generic-edge",
displayName: "Generic Edge",
deploymentRef: "deployment:device-edge/pilot",
});
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-edge" }],
}),
step("insert into device_edges", {
rows: [{
id: edgeId,
edge_key: command.edgeKey,
display_name: command.displayName,
deployment_ref: command.deploymentRef,
lifecycle_state: command.lifecycleState,
created_at: now,
updated_at: now,
created: true,
}],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "edge.ensure",
command,
digestCharacter: "a",
}));
assert.equal(result.replayed, false);
assert.equal(result.result.edge.edgeRef, `edge:${edgeId}`);
assert.equal(result.result.edge.lifecycleState, "provisioning");
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("adopts a legacy metadata-only model profile into the versioned registry", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("model_profile.register", {
adapterVersionRef: `adapter-version:${adapterVersionId}`,
profileRef: "vendor.model.protocol.v1",
schemaVersion: "nodedc.device-model-profile.v1",
vendor: "Example Vendor",
model: "Model One",
deviceType: "tracker",
protocol: "GENERIC_TCP",
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
profileDigest: `sha256:${"e".repeat(64)}`,
capabilities: ["telemetry.observe"],
});
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-profile" }],
}),
step("from device_adapter_versions av", {
rows: [{
id: adapterVersionId,
adapter_package_id: adapterPackageId,
version: "1.0.0",
runtime_package_ref: "artifact:device-adapters/vendor-model-1.0.0",
content_digest: `sha256:${"f".repeat(64)}`,
contract_version: "nodedc.device-adapter.v1",
capabilities: ["telemetry.observe"],
lifecycle_state: "active",
package_lifecycle_state: "active",
created_at: now,
updated_at: now,
}],
}),
step("from device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
adapter_version_id: null,
schema_artifact_ref: null,
profile_digest: null,
capabilities: [],
lifecycle_state: "active",
}],
}),
step("insert into device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
schema_version: command.schemaVersion,
vendor: command.vendor,
model: command.model,
device_type: command.deviceType,
protocol: command.protocol,
adapter_version_id: adapterVersionId,
schema_artifact_ref: command.schemaArtifactRef,
profile_digest: command.profileDigest,
capabilities: command.capabilities,
lifecycle_state: "draft",
created_at: now,
updated_at: now,
created: false,
}],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "model_profile.register",
command,
digestCharacter: "e",
}));
assert.equal(result.replayed, false);
assert.equal(result.result.created, false);
assert.equal(
result.result.modelProfile.adapterVersionRef,
`adapter-version:${adapterVersionId}`,
);
assert.equal(result.result.modelProfile.lifecycleState, "draft");
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("keeps non-legacy model profile identity conflicts fail-closed", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("model_profile.register", {
adapterVersionRef: `adapter-version:${adapterVersionId}`,
profileRef: "vendor.model.protocol.v1",
schemaVersion: "nodedc.device-model-profile.v1",
vendor: "Example Vendor",
model: "Model One",
deviceType: "tracker",
protocol: "GENERIC_TCP",
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
profileDigest: `sha256:${"e".repeat(64)}`,
capabilities: ["telemetry.observe"],
});
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-profile-conflict" }],
}),
step("from device_adapter_versions av", {
rows: [{
id: adapterVersionId,
adapter_package_id: adapterPackageId,
lifecycle_state: "active",
package_lifecycle_state: "active",
}],
}),
step("from device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
adapter_version_id: null,
schema_artifact_ref: "artifact:legacy-but-partial",
profile_digest: null,
capabilities: [],
lifecycle_state: "active",
}],
}),
step("insert into device_model_profiles"),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "model_profile.register",
command,
digestCharacter: "f",
})),
/device_model_profile_identity_conflict/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("activates an adopted rich legacy profile without replacing its immutable JSON", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("model_profile.register", {
adapterVersionRef: `adapter-version:${adapterVersionId}`,
profileRef: "vendor.model.protocol.v1",
schemaVersion: "nodedc.device-model-profile.v1",
vendor: "Example Vendor",
model: "Model One",
deviceType: "tracker",
protocol: "GENERIC_TCP",
schemaArtifactRef: "artifact:model-profiles/vendor-model-v1",
profileDigest: `sha256:${"e".repeat(64)}`,
capabilities: ["telemetry.observe"],
lifecycleState: "active",
});
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-profile-activation" }],
}),
step("from device_adapter_versions av", {
rows: [{
id: adapterVersionId,
adapter_package_id: adapterPackageId,
version: "1.0.0",
runtime_package_ref: "artifact:device-adapters/vendor-model-1.0.0",
content_digest: `sha256:${"f".repeat(64)}`,
contract_version: "nodedc.device-adapter.v1",
capabilities: ["telemetry.observe"],
lifecycle_state: "active",
package_lifecycle_state: "active",
created_at: now,
updated_at: now,
}],
}),
step("from device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
adapter_version_id: adapterVersionId,
schema_artifact_ref: command.schemaArtifactRef,
profile_digest: command.profileDigest,
capabilities: command.capabilities,
lifecycle_state: "draft",
}],
}),
step("insert into device_model_profiles", {
rows: [{
profile_ref: command.profileRef,
schema_version: command.schemaVersion,
vendor: command.vendor,
model: command.model,
device_type: command.deviceType,
protocol: command.protocol,
adapter_version_id: adapterVersionId,
schema_artifact_ref: command.schemaArtifactRef,
profile_digest: command.profileDigest,
capabilities: command.capabilities,
lifecycle_state: "active",
created_at: now,
updated_at: now,
created: false,
}],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "model_profile.register",
command,
digestCharacter: "d",
}));
assert.equal(result.result.created, false);
assert.equal(result.result.modelProfile.lifecycleState, "active");
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("denies project route mutation without an explicit project grant", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("route.ensure", {
projectRef: `project:${projectId}`,
routeKey: "generic-ingress",
displayName: "Generic ingress",
edgeRef: `edge:${edgeId}`,
modelProfileRef: "vendor.model.protocol.v1",
listenerRef: "listener:generic-tcp-primary",
protocol: "GENERIC_TCP",
});
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-route" }],
}),
step("from device_projects p", {
rows: [{
id: projectId,
lifecycle_state: "active",
owner_lifecycle_state: "active",
}],
}),
step("from device_project_grants", { rows: [] }),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "route.ensure",
command,
digestCharacter: "b",
})),
/device_project_capability_denied/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("stores enrollment digest but returns and audits only its masked projection", async () => {
const actor = managementActor("member");
const command = normalizeDeviceManagementCommand(
"enrollment_intent.ensure",
{
projectRef: `project:${projectId}`,
enrollmentKey: "pilot-device",
routeRef: `route:${routeId}`,
modelProfileRef: "vendor.model.protocol.v1",
displayName: "Pilot device",
identifierKind: "serial",
identifierDigest: `hmac-sha256:${"c".repeat(64)}`,
identifierMasked: "********0001",
expiresAt: "2026-09-01T00:00:00.000Z",
},
);
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-enrollment" }],
}),
step("from device_projects p", {
rows: [{
id: projectId,
lifecycle_state: "active",
owner_lifecycle_state: "active",
}],
}),
step("from device_project_grants", {
rows: [{
id: "44444444-4444-4444-8444-444444444444",
principal_kind: "user",
principal_ref: actor.userRef,
project_role: "engineer",
capability_allow: [],
capability_deny: [],
lifecycle_state: "active",
}],
}),
step("from device_routes", {
rows: [{
id: routeId,
project_id: projectId,
route_key: "generic-ingress",
display_name: "Generic ingress",
edge_id: edgeId,
model_profile_ref: command.modelProfileRef,
listener_ref: "listener:generic-tcp-primary",
protocol: "GENERIC_TCP",
direction: "telemetry",
lifecycle_state: "active",
created_at: now,
updated_at: now,
}],
}),
step("insert into device_enrollment_intents", {
rows: [{
id: "55555555-5555-4555-8555-555555555555",
project_id: projectId,
enrollment_key: command.enrollmentKey,
route_id: routeId,
model_profile_ref: command.modelProfileRef,
display_name: command.displayName,
expected_identifier_kind: command.identifierKind,
expected_identifier_masked: command.identifierMasked,
lifecycle_state: "pending",
expires_at: new Date(command.expiresAt),
claimed_device_id: null,
created_at: now,
updated_at: now,
created: true,
}],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "enrollment_intent.ensure",
command,
digestCharacter: "d",
}));
assertSafeProjection(result.result.enrollmentIntent);
assert.equal(
result.result.enrollmentIntent.identifier.masked,
command.identifierMasked,
);
assert.equal(JSON.stringify(result.result).includes(command.identifierDigest), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("lists only bounded active Edge channel registrations without secrets", async () => {
const client = scriptedClient([
step("begin transaction read only"),
step("from device_edges", {
rows: [{
id: edgeId,
channel_endpoint: "https://155.212.211.15/",
channel_servername: "155.212.211.15",
channel_generation_ref: "channel-generation:1",
channel_trust_bundle_ref: "edge-trust:moscow-edge",
channel_certificate_identities: [{
generationRef: "edge-identity:1",
fingerprint: "AA:".repeat(31) + "AA",
status: "active",
}],
channel_lifecycle_state: "active",
}],
}),
step("commit"),
]);
const repository = repositoryWithClient(client);
const registrations = await repository.listActiveEdgeChannelRegistrations(8);
assert.deepEqual(registrations[0], {
edgeRegistrationId: `edge:${edgeId}`,
endpoint: "https://155.212.211.15/",
servername: "155.212.211.15",
channelGeneration: "channel-generation:1",
trustBundleRef: "edge-trust:moscow-edge",
certificateIdentities: [{
generationRef: "edge-identity:1",
fingerprint: "AA:".repeat(31) + "AA",
status: "active",
}],
lifecycleState: "active",
});
assert.equal(JSON.stringify(registrations).includes("private"), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
function managementActor(hubRole) {
return normalizeManagementActor({
userRef: "user:platform-owner",
hubRole,
groupRefs: [],
ownerScopes: [],
});
}
function commandInput({ actor, commandKind, command, digestCharacter }) {
return {
idempotencyKey: `phase23-${commandKind.replaceAll(".", "-")}-0001`,
commandKind,
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
actor,
command,
};
}
function repositoryWithClient(client) {
return new PostgresDeviceRepository({
pool: {
query: async () => ({ rows: [] }),
connect: async () => client,
end: async () => undefined,
},
});
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
released: false,
async query(sql) {
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,144 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
DEVICE_LIFECYCLE_COMMAND_KINDS,
normalizeLifecycleManagementCommand,
} from "../src/lifecycle-management.mjs";
import {
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
normalizeDeviceManagementCommand,
} from "../src/management-command.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const targetProjectRef = "project:22222222-2222-4222-8222-222222222222";
const discoveryRef = "discovery:33333333-3333-4333-8333-333333333333";
const enrollmentIntentRef =
"enrollment-intent:44444444-4444-4444-8444-444444444444";
const deviceRef = "device:55555555-5555-4555-8555-555555555555";
test("lifecycle commands join the same strict management command surface", () => {
for (const kind of DEVICE_LIFECYCLE_COMMAND_KINDS) {
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
}
assert.equal(
normalizeDeviceManagementCommand("device.claim", claimInput()).projectId,
projectRef.slice("project:".length),
);
});
test("claim accepts only opaque evidence references and presentation fields", () => {
const command = normalizeLifecycleManagementCommand(
"device.claim",
claimInput(),
);
assert.equal(
command.enrollmentIntentId,
enrollmentIntentRef.slice("enrollment-intent:".length),
);
assert.equal(command.discoveryId, discoveryRef.slice("discovery:".length));
assert.equal(command.deviceKey, "pilot-device");
assert.equal("identifier" in command, false);
assert.equal("credentialRef" in command, false);
});
test("claim rejects raw identity and credential-shaped input", () => {
assert.throws(
() => normalizeLifecycleManagementCommand("device.claim", {
...claimInput(),
identifier: "000000000000001",
}),
/device_management_command_field_unexpected:identifier/,
);
assert.throws(
() => normalizeLifecycleManagementCommand("device.claim", {
...claimInput(),
credentialRef: "secret:test",
}),
/device_management_command_field_unexpected:credentialRef/,
);
});
test("device update keeps display name and integration identity as separate presentation fields", () => {
const command = normalizeLifecycleManagementCommand("device.update", {
projectRef,
deviceRef,
displayName: " Trike 8028 ",
integrationDeviceId: " 8028 ",
});
assert.equal(command.projectId, projectRef.slice("project:".length));
assert.equal(command.deviceId, deviceRef.slice("device:".length));
assert.equal(command.displayName, "Trike 8028");
assert.equal(command.integrationDeviceId, "8028");
assert.throws(
() => normalizeLifecycleManagementCommand("device.update", {
projectRef,
deviceRef,
displayName: "Trike 8028",
integrationDeviceId: "8028",
identifier: "000000000000001",
}),
/device_management_command_field_unexpected:identifier/,
);
});
test("legacy device update does not implicitly clear the integration identity", () => {
const command = normalizeLifecycleManagementCommand("device.update", {
projectRef,
deviceRef,
displayName: "Trike 8028",
});
assert.equal(command.integrationDeviceId, undefined);
});
test("transfer binds both project boundaries and rejects a no-op", () => {
const command = normalizeLifecycleManagementCommand("device.transfer", {
deviceRef,
sourceProjectRef: projectRef,
targetProjectRef,
targetDeviceKey: "transferred-device",
});
assert.equal(command.deviceId, deviceRef.slice("device:".length));
assert.notEqual(command.sourceProjectId, command.targetProjectId);
assert.throws(
() => normalizeLifecycleManagementCommand("device.transfer", {
deviceRef,
sourceProjectRef: projectRef,
targetProjectRef: projectRef,
targetDeviceKey: "same-project",
}),
/device_transfer_target_same_as_source/,
);
});
test("reject and expire require bounded machine-readable resolution codes", () => {
for (const kind of ["discovery.reject", "discovery.expire"]) {
const command = normalizeLifecycleManagementCommand(kind, {
projectRef,
discoveryRef,
resolutionCode: "operator.identity_mismatch",
});
assert.equal(command.resolutionCode, "operator.identity_mismatch");
}
assert.throws(
() => normalizeLifecycleManagementCommand("discovery.reject", {
projectRef,
discoveryRef,
resolutionCode: "free form reason is forbidden",
}),
/device_discovery_resolution_code_invalid/,
);
});
function claimInput() {
return {
projectRef,
enrollmentIntentRef,
discoveryRef,
deviceKey: "pilot-device",
displayName: "Pilot device",
};
}
@@ -0,0 +1,504 @@
import assert from "node:assert/strict";
import test from "node:test";
import { assertSafeProjection } from "../../../packages/device-protocol-contract/src/index.mjs";
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
import { normalizeManagementActor } from "../src/project-management.mjs";
const now = new Date("2026-08-10T00:00:00.000Z");
const sourceProjectId = "11111111-1111-4111-8111-111111111111";
const targetProjectId = "22222222-2222-4222-8222-222222222222";
const sourceOwnerId = "33333333-3333-4333-8333-333333333333";
const targetOwnerId = "44444444-4444-4444-8444-444444444444";
const enrollmentId = "55555555-5555-4555-8555-555555555555";
const discoveryId = "66666666-6666-4666-8666-666666666666";
const deviceId = "77777777-7777-4777-8777-777777777777";
const routeId = "88888888-8888-4888-8888-888888888888";
const identifierDigest = `hmac-sha256:${"a".repeat(64)}`;
test("claims only matching observed enrollment evidence into direct ownership", async () => {
const actor = managementActor("member");
const command = normalizeDeviceManagementCommand("device.claim", {
projectRef: `project:${sourceProjectId}`,
enrollmentIntentRef: `enrollment-intent:${enrollmentId}`,
discoveryRef: `discovery:${discoveryId}`,
deviceKey: "pilot-device",
displayName: "Pilot device",
});
const client = scriptedClient([
step("begin"),
receiptStep("receipt-claim"),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "engineer"),
step("from device_enrollment_intents", {
rows: [enrollmentRow()],
}),
step("from device_discoveries", {
rows: [discoveryRow()],
}),
step("insert into device_instances", {
rows: [deviceRow({
owner_scope_id: sourceOwnerId,
project_id: sourceProjectId,
device_key: command.deviceKey,
display_name: command.displayName,
})],
}),
step("insert into device_restricted_identifiers"),
step("update device_discoveries", { rows: [{ id: discoveryId }] }),
step("update device_enrollment_intents", { rows: [{ id: enrollmentId }] }),
step("insert into device_ownership_transitions"),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.claim",
command,
digestCharacter: "b",
}));
assert.equal(result.result.device.projectRef, `project:${sourceProjectId}`);
assert.equal(result.result.device.identifier.masked, "********0001");
assert.equal(JSON.stringify(result.result).includes(identifierDigest), false);
assertSafeProjection(result.result);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("project owner can update device name and integration identity independently", async () => {
const actor = managementActor("owner");
const command = normalizeDeviceManagementCommand("device.update", {
projectRef: `project:${sourceProjectId}`,
deviceRef: `device:${deviceId}`,
displayName: "Trike 8028",
integrationDeviceId: "8028",
});
const client = scriptedClient([
step("begin"),
receiptStep("receipt-device-update"),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
step("from device_instances", { rows: [deviceRow()] }),
step("update device_instances", {
rows: [deviceRow({
display_name: command.displayName,
integration_device_id: command.integrationDeviceId,
})],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.update",
command,
digestCharacter: "4",
}));
assert.equal(result.result.updated, true);
assert.equal(result.result.device.displayName, "Trike 8028");
assert.equal(result.result.device.integrationDeviceId, "8028");
assert.equal(result.result.device.projectRef, `project:${sourceProjectId}`);
assertSafeProjection(result.result);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("transfer requires explicit authority in the target project", async () => {
const actor = managementActor("owner");
const command = transferCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-transfer-denied"),
step("from device_instances", { rows: [deviceRow()] }),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
projectStep(targetProjectId, targetOwnerId),
step("from device_project_grants", { rows: [] }),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.transfer",
command,
digestCharacter: "c",
})),
/device_project_capability_denied/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("authorized transfer preserves history and detaches source collections", async () => {
const actor = managementActor("owner");
const command = transferCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-transfer"),
step("from device_instances", { rows: [deviceRow()] }),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: false }] }),
step("from device_resource_bindings", { rows: [{ active: false }] }),
step("from device_configuration_state", { rows: [] }),
step("from device_commands", { rows: [{ active: false }] }),
step("delete from device_configuration_state", { rows: [], rowCount: 0 }),
step("delete from device_collection_members", { rows: [], rowCount: 2 }),
step("update device_instances", {
rows: [deviceRow({
owner_scope_id: targetOwnerId,
project_id: targetProjectId,
device_key: command.targetDeviceKey,
})],
}),
step("update device_restricted_identifiers", { rows: [], rowCount: 1 }),
step("insert into device_ownership_transitions"),
step("insert into device_audit_events"),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.transfer",
command,
digestCharacter: "d",
}));
assert.equal(result.result.transferred, true);
assert.equal(result.result.device.projectRef, `project:${targetProjectId}`);
assert.equal(result.result.detachedCollectionCount, 2);
assert.equal(result.result.transferredIdentifierCount, 1);
assert.equal(result.result.clearedDesiredConfiguration, false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("transfer fails closed while a credential binding is active", async () => {
const actor = managementActor("owner");
const command = transferCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-transfer-credential-bound"),
step("from device_instances", { rows: [deviceRow()] }),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: true }] }),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.transfer",
command,
digestCharacter: "f",
})),
/device_transfer_active_credential_binding/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("transfer fails closed while a resource binding is pending approval", async () => {
const actor = managementActor("owner");
const command = transferCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-transfer-resource-bound"),
step("from device_instances", { rows: [deviceRow()] }),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: false }] }),
step("from device_resource_bindings", { rows: [{ active: true }] }),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.transfer",
command,
digestCharacter: "1",
})),
/device_transfer_active_resource_binding/,
);
assert.equal(client.remaining(), 0);
});
test("transfer fails closed with applied configuration", async () => {
const actor = managementActor("owner");
const command = transferCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-transfer-applied-config"),
step("from device_instances", { rows: [deviceRow()] }),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: false }] }),
step("from device_resource_bindings", { rows: [{ active: false }] }),
step("from device_configuration_state", {
rows: [{
desired_revision_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
applied_revision_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
}],
}),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.transfer",
command,
digestCharacter: "2",
})),
/device_transfer_applied_configuration/,
);
assert.equal(client.remaining(), 0);
});
test("transfer fails closed with a nonterminal command", async () => {
const actor = managementActor("owner");
const command = transferCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-transfer-command-active"),
step("from device_instances", { rows: [deviceRow()] }),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: false }] }),
step("from device_resource_bindings", { rows: [{ active: false }] }),
step("from device_configuration_state", { rows: [] }),
step("from device_commands", { rows: [{ active: true }] }),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.transfer",
command,
digestCharacter: "3",
})),
/device_transfer_nonterminal_command/,
);
assert.equal(client.remaining(), 0);
});
test("reject resolves both quarantine and enrollment without exposing a digest", async () => {
const actor = managementActor("member");
const command = normalizeDeviceManagementCommand("discovery.reject", {
projectRef: `project:${sourceProjectId}`,
discoveryRef: `discovery:${discoveryId}`,
resolutionCode: "operator.identity_mismatch",
});
const client = scriptedClient([
step("begin"),
receiptStep("receipt-reject"),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "engineer"),
step("from device_discoveries", { rows: [discoveryRow()] }),
step("from device_enrollment_intents", { rows: [enrollmentRow()] }),
step("update device_discoveries"),
step("update device_enrollment_intents"),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "discovery.reject",
command,
digestCharacter: "e",
}));
assert.equal(result.result.discovery.lifecycleState, "rejected");
assert.equal(JSON.stringify(result.result).includes(identifierDigest), false);
assertSafeProjection(result.result);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
function transferCommand() {
return normalizeDeviceManagementCommand("device.transfer", {
deviceRef: `device:${deviceId}`,
sourceProjectRef: `project:${sourceProjectId}`,
targetProjectRef: `project:${targetProjectId}`,
targetDeviceKey: "transferred-device",
});
}
function enrollmentRow() {
return {
id: enrollmentId,
project_id: sourceProjectId,
route_id: routeId,
model_profile_ref: "vendor.model.protocol.v1",
expected_identifier_kind: "serial",
expected_identifier_digest: identifierDigest,
expected_identifier_masked: "********0001",
lifecycle_state: "observed",
observed_discovery_id: discoveryId,
claimed_device_id: null,
};
}
function discoveryRow() {
return {
id: discoveryId,
project_id: sourceProjectId,
route_id: routeId,
enrollment_intent_id: enrollmentId,
model_profile_ref: "vendor.model.protocol.v1",
protocol: "GENERIC_TCP",
identifier_kind: "serial",
identifier_digest: identifierDigest,
identifier_masked: "********0001",
lifecycle_state: "quarantine",
claimed_device_id: null,
};
}
function deviceRow(overrides = {}) {
return {
id: deviceId,
contour_id: null,
owner_scope_id: sourceOwnerId,
project_id: sourceProjectId,
device_key: "pilot-device",
model_profile_ref: "vendor.model.protocol.v1",
display_name: "Pilot device",
integration_device_id: null,
identifier_kind: "serial",
identifier_masked: "********0001",
lifecycle_state: "claimed",
created_at: now,
updated_at: now,
...overrides,
};
}
function managementActor(hubRole) {
return normalizeManagementActor({
userRef: "user:lifecycle-operator",
hubRole,
groupRefs: [],
ownerScopes: [],
});
}
function projectStep(projectId, ownerScopeId) {
return step("from device_projects p", {
rows: [{
id: projectId,
owner_scope_id: ownerScopeId,
lifecycle_state: "active",
scope_kind: "company",
owner_ref: `client:${ownerScopeId}`,
owner_display_name: "Example Company",
owner_lifecycle_state: "active",
}],
});
}
function grantsStep(actor, projectRole) {
return step("from device_project_grants", {
rows: [{
id: "99999999-9999-4999-8999-999999999999",
principal_kind: "user",
principal_ref: actor.userRef,
project_role: projectRole,
capability_allow: [],
capability_deny: [],
lifecycle_state: "active",
}],
});
}
function receiptStep(id) {
return step("insert into device_management_command_receipts", {
rows: [{ id }],
});
}
function commandInput({ actor, commandKind, command, digestCharacter }) {
return {
idempotencyKey: `phase24-${commandKind.replaceAll(".", "-")}-0001`,
commandKind,
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
actor,
command,
};
}
function repositoryWithClient(client) {
return new PostgresDeviceRepository({
pool: {
query: async () => ({ rows: [] }),
connect: async () => client,
end: async () => undefined,
},
});
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
released: false,
async query(sql) {
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,39 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/003_device_management_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("management migration pins idempotency, audit and owner invariants", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /device_project_grants_owner_user_only/);
assert.match(sql, /project_role <> 'owner' or principal_kind = 'user'/);
assert.match(sql, /add column if not exists project_id uuid references device_projects\(id\)/);
assert.match(sql, /create table if not exists device_management_command_receipts/);
assert.match(sql, /unique \(actor_ref, command_kind, idempotency_key\)/);
assert.match(sql, /request_digest ~ '\^sha256:\[a-f0-9\]\{64\}\$'/);
assert.match(sql, /lifecycle_state in \('pending', 'completed'\)/);
});
test("management migration stores no tenant, device or credential seed", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|b2|imei/i);
assert.doesNotMatch(sql, /password|secret|credential_ref/i);
});
test("repository applies management migration after project access", async () => {
const source = await readFile(repositoryUrl, "utf8");
const projectAccessIndex = source.indexOf("002_device_project_access.sql");
const managementIndex = source.indexOf("003_device_management_commands.sql");
assert.notEqual(projectAccessIndex, -1);
assert.notEqual(managementIndex, -1);
assert.ok(projectAccessIndex < managementIndex);
});
@@ -0,0 +1,281 @@
import assert from "node:assert/strict";
import test from "node:test";
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
import {
normalizeManagementActor,
normalizeManagementCommand,
} from "../src/project-management.mjs";
const actor = normalizeManagementActor({
userRef: "user:engineer",
hubRole: "admin",
groupRefs: [],
ownerScopes: [{ scopeKind: "company", ownerRef: "client:example" }],
});
const command = normalizeManagementCommand("owner_scope.ensure", {
scopeKind: "company",
ownerRef: "client:example",
displayName: "Example Company",
});
const baseInput = {
idempotencyKey: "phase2-repository-0001",
commandKind: "owner_scope.ensure",
requestDigest: `sha256:${"a".repeat(64)}`,
actor,
command,
};
test("replays a completed command without executing the domain mutation", async () => {
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", { rows: [] }),
step("from device_management_command_receipts", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
request_digest: baseInput.requestDigest,
lifecycle_state: "completed",
response_body: { created: true, ownerScope: { ownerRef: "client:example" } },
}],
}),
step("from device_owner_scopes", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
scope_kind: "company",
owner_ref: "client:example",
display_name: "Example Company",
lifecycle_state: "active",
created_at: new Date("2026-08-10T00:00:00.000Z"),
updated_at: new Date("2026-08-10T00:00:00.000Z"),
}],
}),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(baseInput);
assert.equal(result.replayed, true);
assert.equal(result.result.ownerScope.ownerRef, "client:example");
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("rejects idempotency-key reuse with a different normalized request", async () => {
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", { rows: [] }),
step("from device_management_command_receipts", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
request_digest: `sha256:${"b".repeat(64)}`,
lifecycle_state: "completed",
response_body: { created: true },
}],
}),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(baseInput),
/device_idempotency_key_conflict/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("does not replay a completed project command after project access is revoked", async () => {
const collectionCommand = normalizeManagementCommand("collection.ensure", {
projectRef: "project:11111111-1111-4111-8111-111111111111",
collectionKey: "field-devices",
name: "Field Devices",
});
const input = {
idempotencyKey: "phase2-repository-collection-0001",
commandKind: "collection.ensure",
requestDigest: `sha256:${"c".repeat(64)}`,
actor,
command: collectionCommand,
};
const now = new Date("2026-08-10T00:00:00.000Z");
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", { rows: [] }),
step("from device_management_command_receipts", {
rows: [{
id: "22222222-2222-4222-8222-222222222222",
request_digest: input.requestDigest,
lifecycle_state: "completed",
response_body: { created: true },
}],
}),
step("from device_projects p", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
owner_scope_id: "33333333-3333-4333-8333-333333333333",
project_key: "field-devices",
name: "Field Devices",
description: null,
lifecycle_state: "active",
created_at: now,
updated_at: now,
scope_kind: "company",
owner_ref: "client:example",
owner_display_name: "Example Company",
owner_lifecycle_state: "active",
owner_created_at: now,
owner_updated_at: now,
}],
}),
step("from device_project_grants", { rows: [] }),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(input),
/device_project_capability_denied/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("refuses to revoke the last active project owner", async () => {
const ownerActor = normalizeManagementActor({
...actor,
hubRole: "owner",
});
const revokeOwner = normalizeManagementCommand("project_grant.upsert", {
projectRef: "project:11111111-1111-4111-8111-111111111111",
principalKind: "user",
principalRef: ownerActor.userRef,
projectRole: "owner",
lifecycleState: "revoked",
});
const input = {
idempotencyKey: "phase2-repository-owner-0001",
commandKind: "project_grant.upsert",
requestDigest: `sha256:${"d".repeat(64)}`,
actor: ownerActor,
command: revokeOwner,
};
const now = new Date("2026-08-10T00:00:00.000Z");
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-created" }],
}),
step("from device_projects p", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
owner_scope_id: "33333333-3333-4333-8333-333333333333",
project_key: "field-devices",
name: "Field Devices",
description: null,
lifecycle_state: "active",
created_at: now,
updated_at: now,
scope_kind: "company",
owner_ref: "client:example",
owner_display_name: "Example Company",
owner_lifecycle_state: "active",
owner_created_at: now,
owner_updated_at: now,
}],
}),
step("from device_project_grants", {
rows: [{
id: "44444444-4444-4444-8444-444444444444",
principal_kind: "user",
principal_ref: ownerActor.userRef,
project_role: "owner",
capability_allow: [],
capability_deny: [],
lifecycle_state: "active",
created_at: now,
updated_at: now,
}],
}),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(input),
/device_project_last_owner_required/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("commits an authorized generic owner-scope command and durable receipt", async () => {
const now = new Date("2026-08-10T00:00:00.000Z");
const client = scriptedClient([
step("begin"),
step("insert into device_management_command_receipts", {
rows: [{ id: "receipt-created" }],
}),
step("insert into device_owner_scopes", {
rows: [{
id: "11111111-1111-4111-8111-111111111111",
scope_kind: "company",
owner_ref: "client:example",
display_name: "Example Company",
lifecycle_state: "active",
created_at: now,
updated_at: now,
created: true,
}],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(baseInput);
assert.equal(result.replayed, false);
assert.equal(result.result.created, true);
assert.equal(result.result.ownerScope.ownerRef, "client:example");
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
function repositoryWithClient(client) {
return new PostgresDeviceRepository({
pool: {
query: async () => ({ rows: [] }),
connect: async () => client,
end: async () => undefined,
},
});
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
released: false,
async query(sql) {
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const appUrl = new URL("../src/app.mjs", import.meta.url);
const serverUrl = new URL("../src/server.mjs", import.meta.url);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
const managerComposeUrl = new URL("../../../docker-compose.device-manager.yml", import.meta.url);
test("management surface is internal, POST-only and disabled by default", async () => {
const source = await readFile(appUrl, "utf8");
assert.match(source, /managementApiEnabled = false/);
assert.match(source, /\/internal\/v1\/management\/owner-scopes:ensure/);
assert.match(source, /\/internal\/v1\/management\/projects:ensure/);
assert.match(source, /\/internal\/v1\/management\/collections:ensure/);
assert.match(source, /\/internal\/v1\/management\/project-grants:upsert/);
assert.match(source, /\/internal\/v1\/management\/device-bindings:ensure/);
assert.match(source, /\/internal\/v1\/management\/device-configuration-revisions:create/);
assert.match(source, /request\.method === "POST" && managementCommandKind/);
assert.doesNotMatch(source, /\/api\/public\/.*management/);
assert.doesNotMatch(source, /device-commands:(?:plan|confirm|dispatch)/);
});
test("management API is enabled only through a runner-owned file token", async () => {
const server = await readFile(serverUrl, "utf8");
const compose = await readFile(managerComposeUrl, "utf8");
assert.match(server, /DEVICE_MANAGEMENT_API_ENABLED/);
assert.match(server, /DEVICE_MANAGEMENT_CORE_TOKEN_FILE/);
assert.match(compose, /DEVICE_MANAGEMENT_API_ENABLED: "true"/);
assert.match(
compose,
/DEVICE_MANAGEMENT_CORE_TOKEN_FILE: \/run\/nodedc-secrets\/management-core-token/,
);
assert.match(
compose,
/source: \/volume1\/docker\/nodedc-device-plane\/secrets\/management-core-token/,
);
assert.doesNotMatch(compose, /DEVICE_MANAGEMENT_CORE_TOKEN:\s/);
});
test("repository pins idempotency, audit and last-owner checks inside one transaction", async () => {
const source = await readFile(repositoryUrl, "utf8");
assert.match(source, /await client\.query\("begin"\)/);
assert.match(source, /await client\.query\("commit"\)/);
assert.match(source, /await client\.query\("rollback"\)/);
assert.match(source, /device_idempotency_key_conflict/);
assert.match(source, /device_project_last_owner_required/);
assert.match(source, /for update of p/);
assert.match(source, /authorizeManagementReplay/);
assert.match(source, /insert into device_audit_events/);
assert.match(source, /update device_management_command_receipts/);
});
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/001_device_plane_foundation.sql",
import.meta.url,
);
test("foundation migration keeps restricted identifiers hashed and DB private", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.match(sql, /identifier_digest text not null/);
assert.match(sql, /identifier_masked text not null/);
assert.doesNotMatch(sql, /imei\s+text/i);
assert.doesNotMatch(sql, /password\s+text/i);
assert.doesNotMatch(sql, /raw_packet/i);
});
test("foundation migration has quarantine, contour, binding and audit tables", async () => {
const sql = await readFile(migrationUrl, "utf8");
for (const table of [
"device_model_profiles",
"device_contours",
"device_discoveries",
"device_instances",
"device_bindings",
"device_audit_events",
]) {
assert.match(sql, new RegExp(`create table if not exists ${table}`));
}
assert.match(sql, /default 'quarantine'/);
});
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const migrationUrl = new URL(
"../migrations/002_device_project_access.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("project access migration defines owner, project, collection and grant boundaries", async () => {
const sql = await readFile(migrationUrl, "utf8");
for (const table of [
"device_owner_scopes",
"device_projects",
"device_collections",
"device_collection_members",
"device_project_grants",
]) {
assert.match(sql, new RegExp(`create table if not exists ${table}`));
}
assert.match(sql, /scope_kind in \('company', 'personal'\)/);
assert.match(sql, /principal_kind in \('user', 'group'\)/);
assert.match(
sql,
/project_role in \('viewer', 'operator', 'engineer', 'admin', 'owner'\)/,
);
assert.match(sql, /unique \(scope_kind, owner_ref\)/);
assert.match(sql, /unique \(owner_scope_id, project_key\)/);
assert.match(sql, /unique \(project_id, principal_kind, principal_ref\)/);
assert.match(sql, /not \(capability_allow && capability_deny\)/);
assert.match(
sql,
/foreign key \(collection_id, project_id\)\s+references device_collections\(id, project_id\)/,
);
assert.match(
sql,
/foreign key \(device_id, project_id\)\s+references device_instances\(id, project_id\)/,
);
});
test("project access migration contains no tenant, device or credential seed", async () => {
const sql = await readFile(migrationUrl, "utf8");
assert.doesNotMatch(sql, /insert\s+into/i);
assert.doesNotMatch(sql, /dcctouch|arusnavi|b2|imei/i);
assert.doesNotMatch(sql, /password|secret|token|credential_ref/i);
});
test("repository applies project access migration after the foundation", async () => {
const source = await readFile(repositoryUrl, "utf8");
const foundationIndex = source.indexOf("001_device_plane_foundation.sql");
const projectAccessIndex = source.indexOf("002_device_project_access.sql");
assert.notEqual(foundationIndex, -1);
assert.notEqual(projectAccessIndex, -1);
assert.ok(foundationIndex < projectAccessIndex);
});
@@ -0,0 +1,293 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
assertActorCanManageOwnerScope,
assertGrantMutationAllowed,
assertProjectCapability,
normalizeManagementActor,
normalizeManagementCommand,
resolveProjectAccess,
} from "../src/project-management.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
test("normalizes strict generic management commands without seeded entities", () => {
const project = normalizeManagementCommand("project.ensure", {
scopeKind: "company",
ownerRef: "client:example",
projectKey: "field-devices",
name: "Field Devices",
description: "Generic project",
});
assert.deepEqual(project, {
scopeKind: "company",
ownerRef: "client:example",
projectKey: "field-devices",
name: "Field Devices",
description: "Generic project",
});
assert.throws(
() => normalizeManagementCommand("project.ensure", {
scopeKind: "company",
ownerRef: "client:example",
projectKey: "field-devices",
name: "Field Devices",
rawPayload: "forbidden",
}),
/device_management_command_field_unexpected:rawPayload/,
);
});
test("company scope requires an asserted scope and Hub admin ceiling", () => {
const scope = { scopeKind: "company", ownerRef: "client:example" };
assert.doesNotThrow(() => assertActorCanManageOwnerScope(actor({
hubRole: "admin",
ownerScopes: [scope],
}), scope));
assert.throws(
() => assertActorCanManageOwnerScope(actor({
hubRole: "viewer",
ownerScopes: [scope],
}), scope),
/device_owner_scope_access_denied/,
);
assert.throws(
() => assertActorCanManageOwnerScope(actor({ hubRole: "owner" }), scope),
/device_owner_scope_access_denied/,
);
});
test("personal scope is isolated to the matching Hub user", () => {
const scope = { scopeKind: "personal", ownerRef: "user:engineer" };
assert.doesNotThrow(() => assertActorCanManageOwnerScope(
actor({ hubRole: "owner" }),
scope,
));
assert.throws(
() => assertActorCanManageOwnerScope(
actor({ hubRole: "member" }),
scope,
),
/device_owner_scope_access_denied/,
);
assert.throws(
() => assertActorCanManageOwnerScope(
actor({ userRef: "user:other", hubRole: "owner" }),
scope,
),
/device_owner_scope_access_denied/,
);
});
test("Hub owner has no project access without an explicit project grant", () => {
const access = resolveProjectAccess({
actor: actor({ hubRole: "owner" }),
grants: [],
});
assert.equal(access.allowed, false);
assert.deepEqual(access.capabilities, []);
});
test("a direct user grant overrides broader group grants", () => {
const access = resolveProjectAccess({
actor: actor({ hubRole: "owner", groupRefs: ["group:admins"] }),
grants: [
grant({
grantRef: "grant:group-admin",
principalKind: "group",
principalRef: "group:admins",
projectRole: "admin",
}),
grant({
grantRef: "grant:direct-viewer",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "viewer",
}),
],
});
assert.equal(access.projectRole, "viewer");
assert.equal(access.capabilities.includes("access.manage"), false);
});
test("matching group grants combine bounded operator and engineer capabilities", () => {
const access = resolveProjectAccess({
actor: actor({
hubRole: "member",
groupRefs: ["group:operators", "group:engineers"],
}),
grants: [
grant({
grantRef: "grant:operator",
principalKind: "group",
principalRef: "group:operators",
projectRole: "operator",
}),
grant({
grantRef: "grant:engineer",
principalKind: "group",
principalRef: "group:engineers",
projectRole: "engineer",
}),
],
});
assert.equal(access.capabilities.includes("device.enroll"), true);
assert.equal(access.capabilities.includes("command.dispatch"), true);
assert.equal(access.capabilities.includes("access.manage"), false);
});
test("configuration mutation belongs to engineer and admin, not operator", () => {
const engineer = resolveProjectAccess({
actor: actor({ groupRefs: ["group:engineers"] }),
grants: [grant({
principalKind: "group",
principalRef: "group:engineers",
projectRole: "engineer",
})],
});
const operator = resolveProjectAccess({
actor: actor({ groupRefs: ["group:operators"] }),
grants: [grant({
principalKind: "group",
principalRef: "group:operators",
projectRole: "operator",
})],
});
assert.equal(engineer.capabilities.includes("configuration.manage"), true);
assert.equal(operator.capabilities.includes("configuration.manage"), false);
});
test("Hub ceiling and explicit deny prevent privilege escalation", () => {
const ownerGrant = grant({
grantRef: "grant:owner",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "owner",
capabilityDeny: ["credential.manage"],
});
const hubAdmin = resolveProjectAccess({
actor: actor({ hubRole: "admin" }),
grants: [ownerGrant],
});
const hubOwner = resolveProjectAccess({
actor: actor({ hubRole: "owner" }),
grants: [ownerGrant],
});
assert.equal(hubAdmin.capabilities.includes("device.transfer"), false);
assert.equal(hubOwner.capabilities.includes("device.transfer"), true);
assert.equal(hubOwner.capabilities.includes("credential.manage"), false);
});
test("owner grant mutations require both Hub and project ownership authority", () => {
const grants = [grant({
grantRef: "grant:owner",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "owner",
})];
const ownerCommand = normalizeManagementCommand("project_grant.upsert", {
projectRef,
principalKind: "user",
principalRef: "user:second-owner",
projectRole: "owner",
});
assert.throws(
() => assertGrantMutationAllowed(
actor({ hubRole: "admin" }),
grants,
ownerCommand,
),
/device_project_owner_transfer_denied/,
);
assert.doesNotThrow(() => assertGrantMutationAllowed(
actor({ hubRole: "owner" }),
grants,
ownerCommand,
));
});
test("grant normalization rejects group owners and capability overlap", () => {
assert.throws(
() => normalizeManagementCommand("project_grant.upsert", {
projectRef,
principalKind: "group",
principalRef: "group:owners",
projectRole: "owner",
}),
/device_project_owner_must_be_user/,
);
assert.throws(
() => normalizeManagementCommand("project_grant.upsert", {
projectRef,
principalKind: "user",
principalRef: "user:operator",
projectRole: "operator",
capabilityAllow: ["command.dispatch"],
capabilityDeny: ["command.dispatch"],
}),
/device_project_capability_overlap/,
);
});
test("capability checks fail closed for inactive or unrelated grants", () => {
assert.throws(
() => assertProjectCapability(
actor({ hubRole: "owner" }),
[grant({ lifecycleState: "revoked" })],
"project.read",
),
/device_project_capability_denied/,
);
});
test("denying project.read collapses every derived capability", () => {
const access = resolveProjectAccess({
actor: actor({ hubRole: "owner" }),
grants: [grant({
projectRole: "owner",
capabilityDeny: ["project.read"],
})],
});
assert.equal(access.allowed, false);
assert.deepEqual(access.capabilities, []);
assert.throws(
() => assertProjectCapability(
actor({ hubRole: "owner" }),
[grant({
projectRole: "owner",
capabilityDeny: ["project.read"],
})],
"access.manage",
),
/device_project_capability_denied/,
);
});
function actor(overrides = {}) {
return normalizeManagementActor({
userRef: "user:engineer",
hubRole: "member",
groupRefs: [],
ownerScopes: [],
...overrides,
});
}
function grant(overrides = {}) {
return {
grantRef: "grant:default",
principalKind: "user",
principalRef: "user:engineer",
projectRole: "viewer",
capabilityAllow: [],
capabilityDeny: [],
lifecycleState: "active",
...overrides,
};
}
@@ -0,0 +1,391 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import {
getDeviceProjectWorkspace,
listAccessibleDeviceProjects,
} from "../src/project-query-repository.mjs";
import { normalizeManagementActor } from "../src/project-management.mjs";
const projectId = "11111111-1111-4111-8111-111111111111";
const actor = normalizeManagementActor({
userRef: "user:device-admin",
hubRole: "admin",
groupRefs: ["group:engineers"],
ownerScopes: [],
});
const timestamp = "2026-08-10T00:00:00.000Z";
test("project list applies direct-grant precedence and returns bounded summaries", async () => {
const client = {
async query(sql) {
assert.match(sql, /from device_projects p/);
return {
rows: [
projectGrantRow({
grant_id: "22222222-2222-4222-8222-222222222222",
principal_kind: "group",
principal_ref: "group:engineers",
project_role: "engineer",
}),
projectGrantRow({
grant_id: "33333333-3333-4333-8333-333333333333",
principal_kind: "user",
principal_ref: "user:device-admin",
project_role: "viewer",
}),
],
};
},
};
const projects = await listAccessibleDeviceProjects(client, actor);
assert.equal(projects.length, 1);
assert.equal(projects[0].access.projectRole, "viewer");
assert.equal(projects[0].access.capabilities.includes("collection.manage"), false);
assert.deepEqual(projects[0].counts, {
devices: 3,
collections: 2,
discoveries: 1,
});
});
test("project workspace returns only masked identity projections", async () => {
const client = workspaceClient();
const workspace = await getDeviceProjectWorkspace(client, actor, projectId);
assert.equal(workspace.project.projectRef, `project:${projectId}`);
assert.equal(workspace.devices[0].identifier.masked, "***********0001");
assert.equal(workspace.discoveries[0].identifier.masked, "***********0001");
assert.equal(workspace.enrollments[0].expectedIdentifier.masked, "***********0001");
assert.equal(
workspace.enrollments[0].enrollmentIntentRef,
"enrollment-intent:77777777-7777-4777-8777-777777777777",
);
assert.equal(workspace.adapterPackages[0].packageKey, "generic-tracker");
assert.equal(workspace.modelProfiles[0].modelProfileRef, "vendor.model.v1");
assert.equal(workspace.routes[0].activeSessionCount, 1);
assert.equal(workspace.sessions[0].frameCount, 12);
assert.equal(workspace.bindings[0].lifecycleState, "pending_external_approval");
assert.equal(workspace.configurationRevisions[0].revisionNumber, 1);
assert.equal(workspace.commands[0].lifecycleState, "acknowledged");
assert.equal(workspace.auditEvents[0].eventType, "device.observed");
assert.equal(workspace.grants[0].principalRef, "user:device-admin");
assert.equal(workspace.policies.commandTransport, "disabled");
const serialized = JSON.stringify(workspace);
assert.equal(serialized.includes("hmac-sha256"), false);
assert.equal(serialized.includes("ndc-credref"), false);
assert.equal(serialized.includes("transport-message-secret"), false);
assert.equal(serialized.includes("external-approval-proof"), false);
assert.equal(serialized.includes("raw-audit-payload"), false);
});
test("project workspace authorization remains compatible with read-only transactions", async () => {
const queries = [];
const client = workspaceClient({ queries });
await getDeviceProjectWorkspace(client, actor, projectId);
assert.ok(queries.length > 0);
assert.equal(
queries.some((sql) => /\bfor\s+(?:no\s+key\s+)?(?:update|share)\b/i.test(sql)),
false,
);
});
test("project read source never selects identifier digests or credential refs", async () => {
const source = await readFile(
new URL("../src/project-query-repository.mjs", import.meta.url),
"utf8",
);
assert.doesNotMatch(
source,
/\b(?:identifier_digest|expected_identifier_digest|credential_ref|parameters_digest|parameters_projection|transport_message_ref|external_approval_ref|external_approval_digest)\b/,
);
assert.doesNotMatch(source, /\b(?:dae\.payload|dcr\.configuration)\b/);
});
function workspaceClient({ queries = [] } = {}) {
let grantReads = 0;
return {
async query(sql) {
queries.push(sql);
if (/from device_projects p/.test(sql)) {
return { rows: [projectGrantRow()] };
}
if (/from device_project_grants/.test(sql)) {
grantReads += 1;
return { rows: [storedGrantRow()] };
}
if (/from device_adapter_packages ap/.test(sql)) {
return { rows: [{
id: "99999999-9999-4999-8999-999999999999",
package_key: "generic-tracker",
display_name: "Generic tracker",
publisher_ref: "publisher:nodedc",
lifecycle_state: "active",
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_adapter_versions av/.test(sql)) {
return { rows: [{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
adapter_package_id: "99999999-9999-4999-8999-999999999999",
version: "1.0.0",
runtime_package_ref: "artifact:generic-tracker:1.0.0",
content_digest: `sha256:${"a".repeat(64)}`,
contract_version: "device-adapter.v1",
capabilities: ["telemetry"],
lifecycle_state: "active",
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_model_profiles dmp/.test(sql)) {
return { rows: [{
profile_ref: "vendor.model.v1",
adapter_version_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
schema_version: "1.0.0",
vendor: "Vendor",
model: "Model",
device_type: "tracker",
protocol: "INTERNAL",
schema_artifact_ref: "schema:vendor.model.v1",
profile_digest: `sha256:${"b".repeat(64)}`,
capabilities: ["telemetry"],
lifecycle_state: "active",
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_edges de/.test(sql)) {
return { rows: [{
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
edge_key: "edge-one",
display_name: "Edge one",
deployment_ref: "deployment:edge-one",
lifecycle_state: "active",
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_routes dr/.test(sql)) {
return { rows: [{
id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
route_key: "route-one",
display_name: "Route one",
edge_id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
edge_name: "Edge one",
model_profile_ref: "vendor.model.v1",
profile_vendor: "Vendor",
profile_model: "Model",
listener_ref: "listener:generic",
protocol: "INTERNAL",
direction: "bidirectional",
lifecycle_state: "active",
session_count: "1",
active_session_count: "1",
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/select ds\.id, ds\.route_id, dr\.display_name/.test(sql)) {
return { rows: [{
id: "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
route_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
route_name: "Route one",
device_id: "44444444-4444-4444-8444-444444444444",
device_name: "Pilot device",
protocol: "INTERNAL",
lifecycle_state: "online",
connected_at: timestamp,
last_seen_at: timestamp,
disconnected_at: null,
close_reason_code: null,
frame_count: "12",
byte_count: "1024",
}] };
}
if (/from device_resource_bindings drb/.test(sql)) {
return { rows: [{
id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
binding_key: "foundry-map",
display_name: "Foundry map",
source_kind: "collection",
device_id: null,
collection_id: "55555555-5555-4555-8555-555555555555",
source_name: "Pilot fleet",
target_kind: "foundry.application",
target_ref: "application:pilot-map",
capabilities: ["observe"],
lifecycle_state: "pending_external_approval",
source_approved_at: timestamp,
external_approval_ref: "external-approval-proof",
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_configuration_revisions dcr/.test(sql)) {
return { rows: [{
id: "ffffffff-ffff-4fff-8fff-ffffffffffff",
device_id: "44444444-4444-4444-8444-444444444444",
device_name: "Pilot device",
revision_number: "1",
model_profile_ref: "vendor.model.v1",
schema_artifact_ref: "schema:vendor.model.v1",
configuration_digest: `sha256:${"c".repeat(64)}`,
configuration: { raw: "must-not-leak" },
change_summary: "Pilot configuration",
created_at: timestamp,
}] };
}
if (/from device_configuration_state dcs/.test(sql)) {
return { rows: [{
device_id: "44444444-4444-4444-8444-444444444444",
device_name: "Pilot device",
desired_revision_id: "ffffffff-ffff-4fff-8fff-ffffffffffff",
applied_revision_id: null,
applied_at: null,
updated_at: timestamp,
}] };
}
if (/from device_commands dc/.test(sql)) {
return { rows: [{
id: "12121212-1212-4121-8121-121212121212",
device_id: "44444444-4444-4444-8444-444444444444",
device_name: "Pilot device",
command_key: "safe-ping",
command_catalog_ref: "catalog:safe-ping:v1",
command_type: "device.ping",
risk_class: "low",
lifecycle_state: "acknowledged",
planned_at: timestamp,
expires_at: "2026-08-11T00:00:00.000Z",
confirmed_at: timestamp,
dispatched_at: timestamp,
acknowledged_at: timestamp,
terminal_at: null,
terminal_reason_code: null,
transport_message_ref: "transport-message-secret",
parameters_projection: { raw: "must-not-leak" },
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_audit_events dae/.test(sql)) {
return { rows: [{
id: "13131313-1313-4131-8131-131313131313",
event_type: "device.observed",
actor_ref: "user:device-admin",
device_id: "44444444-4444-4444-8444-444444444444",
discovery_id: "66666666-6666-4666-8666-666666666666",
occurred_at: timestamp,
payload: { raw: "raw-audit-payload" },
}] };
}
if (/from device_instances di\n left join lateral/.test(sql)) {
return { rows: [{
id: "44444444-4444-4444-8444-444444444444",
device_key: "pilot-device",
display_name: "Pilot device",
model_profile_ref: "vendor.model.v1",
lifecycle_state: "online",
identifier_kind: "imei",
identifier_masked: "***********0001",
session_state: "online",
last_seen_at: timestamp,
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_collections dc/.test(sql)) {
return { rows: [{
id: "55555555-5555-4555-8555-555555555555",
collection_key: "pilot-fleet",
name: "Pilot fleet",
description: null,
lifecycle_state: "active",
member_count: "1",
created_at: timestamp,
updated_at: timestamp,
}] };
}
if (/from device_discoveries dd/.test(sql)) {
return { rows: [{
id: "66666666-6666-4666-8666-666666666666",
identifier_kind: "imei",
identifier_masked: "***********0001",
model_profile_ref: "vendor.model.v1",
protocol: "INTERNAL",
lifecycle_state: "quarantine",
first_observed_at: timestamp,
last_observed_at: timestamp,
enrollment_intent_id: "77777777-7777-4777-8777-777777777777",
claimed_device_id: null,
}] };
}
if (/from device_enrollment_intents dei/.test(sql)) {
return { rows: [{
id: "77777777-7777-4777-8777-777777777777",
enrollment_key: "pilot-enrollment",
display_name: "Pilot device",
model_profile_ref: "vendor.model.v1",
expected_identifier_kind: "imei",
expected_identifier_masked: "***********0001",
lifecycle_state: "observed",
observed_discovery_id: "66666666-6666-4666-8666-666666666666",
claimed_device_id: null,
expires_at: null,
created_at: timestamp,
updated_at: timestamp,
}] };
}
throw new Error(`unexpected_query:${sql}`);
},
get grantReads() {
return grantReads;
},
};
}
function projectGrantRow(overrides = {}) {
return {
id: projectId,
project_key: "pilot-project",
name: "Pilot project",
description: null,
lifecycle_state: "active",
owner_scope_id: "88888888-8888-4888-8888-888888888888",
scope_kind: "company",
owner_ref: "client:dctouch",
owner_display_name: "DCTOUCH",
owner_lifecycle_state: "active",
grant_id: "33333333-3333-4333-8333-333333333333",
principal_kind: "user",
principal_ref: "user:device-admin",
project_role: "viewer",
capability_allow: [],
capability_deny: [],
grant_lifecycle_state: "active",
device_count: "3",
collection_count: "2",
discovery_count: "1",
created_at: timestamp,
updated_at: timestamp,
...overrides,
};
}
function storedGrantRow() {
return {
id: "33333333-3333-4333-8333-333333333333",
principal_kind: "user",
principal_ref: "user:device-admin",
project_role: "admin",
capability_allow: [],
capability_deny: [],
lifecycle_state: "active",
};
}
@@ -0,0 +1,157 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS,
normalizeSensitiveReferenceManagementCommand,
} from "../src/sensitive-reference-management.mjs";
import {
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
normalizeDeviceManagementCommand,
} from "../src/management-command.mjs";
import {
normalizeNdcCredentialReference as normalizeRuntimeCredentialReference,
} from "../src/credential-reference.mjs";
import {
normalizeNdcCredentialReference as normalizePlatformCredentialReference,
} from "../../../../platform/packages/external-provider-contract/src/credential-reference.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
test("credential binding commands share the strict management surface", () => {
for (const kind of DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS) {
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
}
assert.equal(
normalizeDeviceManagementCommand(
"device_credential_binding.upsert",
upsertInput(),
).projectId,
projectRef.slice("project:".length),
);
});
test("credential binding accepts only the platform canonical opaque ref", () => {
const command = normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
upsertInput(),
);
assert.deepEqual(command.credentialRef, {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
});
assert.equal(Object.isFrozen(command.credentialRef), true);
assert.throws(
() => normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
{
...upsertInput(),
credentialRef: {
owner: "device_core",
reference: "ndc-credref:pilot-command-0001",
},
},
),
/ndc_credential_reference_owner_invalid/,
);
assert.throws(
() => normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
{
...upsertInput(),
credentialRef: {
owner: "ndc_l2_credentials",
reference: "Bearer plaintext-is-forbidden",
},
},
),
/ndc_credential_reference_value_invalid/,
);
});
test("runtime credential reference adapter matches the platform contract", () => {
const accepted = [
{
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
},
{
owner: "ndc_l2_credentials",
reference: "ndc-credref:A1234567",
},
];
for (const input of accepted) {
assert.deepEqual(
normalizeRuntimeCredentialReference(input),
normalizePlatformCredentialReference(input),
);
}
const rejected = [
null,
[],
{ owner: "device_core", reference: "ndc-credref:pilot-command-0001" },
{ owner: "ndc_l2_credentials", reference: "secret:test" },
{
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
token: "forbidden",
},
];
for (const input of rejected) {
let runtimeError;
let platformError;
try {
normalizeRuntimeCredentialReference(input);
} catch (error) {
runtimeError = error;
}
try {
normalizePlatformCredentialReference(input);
} catch (error) {
platformError = error;
}
assert.equal(runtimeError?.message, platformError?.message);
}
});
test("credential binding rejects raw secret-shaped fields", () => {
for (const field of ["password", "token", "secretValue", "endpoint"]) {
assert.throws(
() => normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
{ ...upsertInput(), [field]: "forbidden" },
),
new RegExp(`device_management_command_field_unexpected:${field}`),
);
}
});
test("credential revoke has no credential reference input", () => {
const command = normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.revoke",
{
projectRef,
deviceRef,
purpose: "tracker.command",
resolutionCode: "operator.rotation",
},
);
assert.equal(command.resolutionCode, "operator.rotation");
assert.equal("credentialRef" in command, false);
});
function upsertInput() {
return {
projectRef,
deviceRef,
purpose: "tracker.command",
credentialRef: {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
},
};
}
@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const schemaUrl = new URL(
"../migrations/008_device_sensitive_references.sql",
import.meta.url,
);
const commandsUrl = new URL(
"../migrations/009_device_sensitive_reference_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("sensitive reference schema stores only digest, mask and canonical refs", async () => {
const sql = await readFile(schemaUrl, "utf8");
assert.match(sql, /create table if not exists device_restricted_identifiers/);
assert.match(sql, /identifier_digest text not null/);
assert.match(sql, /identifier_masked text not null/);
assert.match(sql, /device_restricted_identifiers_active_identity_idx/);
assert.match(sql, /device_restricted_identifiers_primary_idx/);
assert.match(sql, /device_identifier_ownership_mismatch/);
assert.match(sql, /device_active_identifier_ownership_mismatch/);
assert.match(sql, /deferrable initially deferred/);
assert.match(sql, /create table if not exists device_credential_bindings/);
assert.match(sql, /credential_owner = 'ndc_l2_credentials'/);
assert.match(sql, /\^ndc-credref:/);
assert.match(sql, /device_credential_binding_ownership_mismatch/);
assert.match(sql, /device_transfer_active_credential_binding/);
assert.match(sql, /owner_scope_id is null or credential_ref is null/);
assert.doesNotMatch(sql, /imei\s+text|serial\s+text|password\s+text|token\s+text/i);
assert.doesNotMatch(sql, /insert\s+into/i);
});
test("credential commands extend durable receipts after their schema", async () => {
const commands = await readFile(commandsUrl, "utf8");
const repository = await readFile(repositoryUrl, "utf8");
assert.match(commands, /'device_credential_binding\.upsert'/);
assert.match(commands, /'device_credential_binding\.revoke'/);
const schemaIndex = repository.indexOf("008_device_sensitive_references.sql");
const commandsIndex = repository.indexOf(
"009_device_sensitive_reference_commands.sql",
);
assert.notEqual(schemaIndex, -1);
assert.notEqual(commandsIndex, -1);
assert.ok(schemaIndex < commandsIndex);
});
@@ -0,0 +1,271 @@
import assert from "node:assert/strict";
import test from "node:test";
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
import { normalizeManagementActor } from "../src/project-management.mjs";
const now = new Date("2026-08-10T00:00:00.000Z");
const projectId = "11111111-1111-4111-8111-111111111111";
const ownerId = "22222222-2222-4222-8222-222222222222";
const deviceId = "33333333-3333-4333-8333-333333333333";
const bindingId = "44444444-4444-4444-8444-444444444444";
const canonicalRef = "ndc-credref:pilot-command-0001";
test("creates a canonical binding without returning or auditing its reference", async () => {
const actor = managementActor();
const command = upsertCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-credential-upsert"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("from device_credential_bindings", { rows: [] }),
step("insert into device_credential_bindings", {
rows: [bindingRow()],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_credential_binding.upsert",
command,
digestCharacter: "a",
}));
assert.equal(result.result.created, true);
assert.equal(result.result.rotated, false);
assert.equal(
result.result.credentialBinding.credentialBindingRef,
`credential-binding:${bindingId}`,
);
assert.equal(JSON.stringify(result.result).includes(canonicalRef), false);
const auditCall = client.calls.find((call) =>
String(call.sql).includes("insert into device_audit_events")
);
assert.ok(auditCall);
assert.equal(JSON.stringify(auditCall.params).includes(canonicalRef), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("revokes by device and purpose without accepting a credential ref", async () => {
const actor = managementActor();
const command = normalizeDeviceManagementCommand(
"device_credential_binding.revoke",
{
projectRef: `project:${projectId}`,
deviceRef: `device:${deviceId}`,
purpose: "tracker.command",
resolutionCode: "operator.rotation",
},
);
const client = scriptedClient([
step("begin"),
receiptStep("receipt-credential-revoke"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("update device_credential_bindings", {
rows: [bindingRow({ lifecycle_state: "revoked" })],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_credential_binding.revoke",
command,
digestCharacter: "b",
}));
assert.equal(result.result.revoked, true);
assert.equal(result.result.credentialBinding.lifecycleState, "revoked");
assert.equal("credentialRef" in command, false);
assert.equal(JSON.stringify(result.result).includes(canonicalRef), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("rotates an active binding atomically and keeps both refs out of audit", async () => {
const actor = managementActor();
const command = upsertCommand();
const oldRef = "ndc-credref:pilot-command-old-0001";
const client = scriptedClient([
step("begin"),
receiptStep("receipt-credential-rotate"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("from device_credential_bindings", {
rows: [bindingRow({ credential_ref: oldRef })],
}),
step("update device_credential_bindings"),
step("insert into device_credential_bindings", {
rows: [bindingRow({
id: "66666666-6666-4666-8666-666666666666",
})],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_credential_binding.upsert",
command,
digestCharacter: "c",
}));
assert.equal(result.result.created, true);
assert.equal(result.result.rotated, true);
const auditCall = client.calls.find((call) =>
String(call.sql).includes("insert into device_audit_events")
);
assert.ok(auditCall);
assert.equal(JSON.stringify(auditCall.params).includes(oldRef), false);
assert.equal(JSON.stringify(auditCall.params).includes(canonicalRef), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
function upsertCommand() {
return normalizeDeviceManagementCommand(
"device_credential_binding.upsert",
{
projectRef: `project:${projectId}`,
deviceRef: `device:${deviceId}`,
purpose: "tracker.command",
credentialRef: {
owner: "ndc_l2_credentials",
reference: canonicalRef,
},
},
);
}
function managementActor() {
return normalizeManagementActor({
userRef: "user:credential-operator",
hubRole: "admin",
groupRefs: [],
ownerScopes: [],
});
}
function projectStep() {
return step("from device_projects p", {
rows: [{
id: projectId,
owner_scope_id: ownerId,
lifecycle_state: "active",
scope_kind: "company",
owner_ref: "client:example-company",
owner_display_name: "Example Company",
owner_lifecycle_state: "active",
}],
});
}
function grantsStep(actor) {
return step("from device_project_grants", {
rows: [{
id: "55555555-5555-4555-8555-555555555555",
principal_kind: "user",
principal_ref: actor.userRef,
project_role: "admin",
capability_allow: [],
capability_deny: [],
lifecycle_state: "active",
}],
});
}
function deviceRow() {
return {
id: deviceId,
owner_scope_id: ownerId,
project_id: projectId,
lifecycle_state: "claimed",
};
}
function bindingRow(overrides = {}) {
return {
id: bindingId,
device_id: deviceId,
owner_scope_id: ownerId,
project_id: projectId,
purpose: "tracker.command",
credential_owner: "ndc_l2_credentials",
lifecycle_state: "active",
created_at: now,
updated_at: now,
...overrides,
};
}
function receiptStep(id) {
return step("insert into device_management_command_receipts", {
rows: [{ id }],
});
}
function commandInput({ actor, commandKind, command, digestCharacter }) {
return {
idempotencyKey: `phase24-${commandKind.replaceAll(".", "-")}-0001`,
commandKind,
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
actor,
command,
};
}
function repositoryWithClient(client) {
return new PostgresDeviceRepository({
pool: {
query: async () => ({ rows: [] }),
connect: async () => client,
end: async () => undefined,
},
});
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
calls: [],
released: false,
async query(sql, params = []) {
this.calls.push({ sql, params });
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,106 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createTypedCommandRuntime } from "../src/typed-command-runtime.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
test("keeps the B2 access code transient and emits only a typed offer", async () => {
const planned = [];
const dispatched = [];
const runtime = createTypedCommandRuntime({
now: () => new Date("2026-08-12T18:00:00.000Z"),
repository: {
async planTypedServicePing(value) {
planned.push(value);
return {
replayed: false,
commandId: "33333333-3333-4333-8333-333333333333",
command: {
lifecycleState: "queued",
expiresAt: "2026-08-12T18:05:00.000Z",
},
};
},
async dispatchTypedCommand(value) {
dispatched.push(value);
return { lifecycleState: "dispatched" };
},
async recordTypedCommandStatus() {},
},
});
await runtime.planServicePing({
idempotencyKey: "idem-00000001",
actor: { userRef: "user:test" },
input: { projectRef, deviceRef, accessCode: "123456", expiresInSeconds: 300 },
});
assert.equal(JSON.stringify(planned).includes("123456"), false);
const offer = await runtime.offerForDevice(deviceRef);
assert.equal(offer.commandType, "service.ping");
assert.equal(offer.accessCode, "123456");
assert.match(offer.transportMessageRef, /^edge-command:/);
assert.equal(dispatched.length, 1);
});
test("expires a transient authorization through the durable ledger", async () => {
let current = new Date("2026-08-12T18:00:00.000Z");
const dispatches = [];
const runtime = createTypedCommandRuntime({
now: () => current,
repository: {
async planTypedServicePing() {
return {
replayed: false,
commandId: "33333333-3333-4333-8333-333333333333",
command: {
lifecycleState: "queued",
expiresAt: "2026-08-12T18:00:30.000Z",
},
};
},
async dispatchTypedCommand(value) {
dispatches.push(value);
return null;
},
async recordTypedCommandStatus() {},
},
});
await runtime.planServicePing({
idempotencyKey: "idem-00000002",
actor: { userRef: "user:test" },
input: { projectRef, deviceRef, accessCode: "123456", expiresInSeconds: 30 },
});
current = new Date("2026-08-12T18:00:31.000Z");
assert.equal(await runtime.offerForDevice(deviceRef), null);
assert.equal(dispatches.length, 1);
assert.equal(runtime.status().transientAuthorizations, 0);
});
test("does not recreate a transient authorization on an idempotent replay", async () => {
const runtime = createTypedCommandRuntime({
repository: {
async planTypedServicePing() {
return {
replayed: true,
commandId: "33333333-3333-4333-8333-333333333333",
command: {
lifecycleState: "queued",
expiresAt: "2026-08-12T18:05:00.000Z",
},
};
},
async dispatchTypedCommand() {
throw new Error("must_not_dispatch_replayed_secret");
},
async recordTypedCommandStatus() {},
},
});
await runtime.planServicePing({
idempotencyKey: "idem-00000003",
actor: { userRef: "user:test" },
input: { projectRef, deviceRef, accessCode: "654321", expiresInSeconds: 300 },
});
assert.equal(runtime.status().transientAuthorizations, 0);
assert.equal(await runtime.offerForDevice(deviceRef), null);
});