feat(device-core): add restricted identity references

This commit is contained in:
Codex
2026-08-10 18:31:30 +03:00
parent fceaca9546
commit 422ddb020f
20 changed files with 1413 additions and 7 deletions
@@ -25,6 +25,7 @@ export const DEVICE_BINDING_CAPABILITIES = Object.freeze([
const OPAQUE_REF_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const IMEI_RE = /^\d{15}$/;
const DIGEST_RE = /^hmac-sha256:[a-f0-9]{64}$/;
const IDENTIFIER_KIND_RE = /^[a-z][a-z0-9._:-]{1,63}$/;
const forbiddenKeyFragments = Object.freeze([
"password",
"secret",
@@ -88,7 +89,7 @@ export function toSafeDiscoveryView(signal, options = {}) {
protocol: normalized.protocol,
observedAt: normalized.observedAt,
lifecycleState: normalized.lifecycleState,
identifier: Object.freeze({
identifier: normalizeRestrictedIdentifierProjection({
kind: normalized.identifier.kind,
masked: maskRestrictedIdentifier(normalized.identifier),
}),
@@ -116,6 +117,55 @@ export function assertIdentifierDigest(value) {
return value;
}
export function normalizeRestrictedIdentifierProjection(input) {
assertPlainObject(input, "restricted_identifier_projection");
const allowedKeys = new Set(["kind", "masked"]);
for (const key of Object.keys(input)) {
if (!allowedKeys.has(key)) {
throw new TypeError(
`restricted_identifier_projection_field_unexpected:${key}`,
);
}
}
if (typeof input.kind !== "string" || !IDENTIFIER_KIND_RE.test(input.kind)) {
throw new TypeError("restricted_identifier_projection_kind_invalid");
}
if (
typeof input.masked !== "string"
|| input.masked.length < 5
|| input.masked.length > 128
|| !input.masked.includes("*")
|| /\u0000|[\u0001-\u001f\u007f]/.test(input.masked)
|| /\b\d{15}\b/.test(input.masked)
) {
throw new TypeError("restricted_identifier_projection_mask_invalid");
}
return Object.freeze({
kind: input.kind,
masked: input.masked,
});
}
export function normalizeRestrictedIdentifierRecord(input) {
assertPlainObject(input, "restricted_identifier_record");
const allowedKeys = new Set(["kind", "digest", "masked"]);
for (const key of Object.keys(input)) {
if (!allowedKeys.has(key)) {
throw new TypeError(
`restricted_identifier_record_field_unexpected:${key}`,
);
}
}
const projection = normalizeRestrictedIdentifierProjection({
kind: input.kind,
masked: input.masked,
});
return Object.freeze({
...projection,
digest: assertIdentifierDigest(input.digest),
});
}
export function normalizeDevicePlaneBinding(input) {
assertPlainObject(input, "device_plane_binding");
rejectForbiddenKeys(input);
@@ -9,6 +9,8 @@ import {
hashRestrictedIdentifier,
normalizeDevicePlaneBinding,
normalizeDiscoverySignal,
normalizeRestrictedIdentifierProjection,
normalizeRestrictedIdentifierRecord,
toSafeDiscoveryView,
} from "../src/index.mjs";
@@ -82,6 +84,32 @@ test("identifier hashing requires a strong process-only pepper", () => {
);
});
test("restricted identifier records keep digest internal and expose only a mask", () => {
const record = normalizeRestrictedIdentifierRecord({
kind: "vendor.serial",
digest: `hmac-sha256:${"a".repeat(64)}`,
masked: "********ABCD",
});
const projection = normalizeRestrictedIdentifierProjection({
kind: record.kind,
masked: record.masked,
});
assert.deepEqual(projection, {
kind: "vendor.serial",
masked: "********ABCD",
});
assert.equal("digest" in projection, false);
assertSafeProjection({ identifier: projection });
assert.throws(
() => normalizeRestrictedIdentifierProjection({
kind: "vendor.serial",
masked: "SERIAL-PLAINTEXT",
}),
/restricted_identifier_projection_mask_invalid/,
);
});
test("rejects unverified framing and command-shaped discovery input", () => {
assert.throws(
() => normalizeDiscoverySignal({
@@ -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;
@@ -27,6 +27,14 @@ const managementRoutes = new Map([
["/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",
],
]);
export function createControlCoreApp({
@@ -1,5 +1,8 @@
import { randomUUID } from "node:crypto";
import {
normalizeRestrictedIdentifierProjection,
} from "../../../packages/device-protocol-contract/src/index.mjs";
import { isLifecycleManagementCommand } from "./lifecycle-management.mjs";
import {
assertProjectCapability,
@@ -123,6 +126,32 @@ async function claimDevice(client, actor, command) {
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',
@@ -193,6 +222,7 @@ async function claimDevice(client, actor, command) {
enrollmentIntentRef: `enrollment-intent:${enrollment.id}`,
discoveryRef: `discovery:${discovery.id}`,
ownershipTransitionRef: `ownership-transition:${transitionId}`,
identifierRef: `identifier:${identifierId}`,
modelProfileRef: device.model_profile_ref,
},
});
@@ -202,6 +232,7 @@ async function claimDevice(client, actor, command) {
enrollmentIntentRef: `enrollment-intent:${enrollment.id}`,
discoveryRef: `discovery:${discovery.id}`,
ownershipTransitionRef: `ownership-transition:${transitionId}`,
identifierRef: `identifier:${identifierId}`,
};
}
@@ -329,6 +360,17 @@ async function transferDevice(client, actor, command) {
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 detached = await client.query(
`delete from device_collection_members
where device_id = $1 and project_id = $2`,
@@ -354,6 +396,18 @@ async function transferDevice(client, actor, command) {
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 (
@@ -382,6 +436,7 @@ async function transferDevice(client, actor, command) {
sourceProjectRef: toProjectRef(sourceProject.id),
targetProjectRef: toProjectRef(targetProject.id),
detachedCollectionCount: Number(detached.rowCount || 0),
transferredIdentifierCount: Number(movedIdentifiers.rowCount || 0),
};
await addAudit(client, {
eventType: "device.transferred_out",
@@ -403,10 +458,11 @@ async function transferDevice(client, actor, command) {
sourceProjectRef: toProjectRef(sourceProject.id),
ownershipTransitionRef: `ownership-transition:${transitionId}`,
detachedCollectionCount: Number(detached.rowCount || 0),
transferredIdentifierCount: Number(movedIdentifiers.rowCount || 0),
};
}
async function findProjectWithCapability(client, actor, projectId, capability) {
export async function findProjectWithCapability(client, actor, projectId, capability) {
const result = await client.query(
`select p.id, p.owner_scope_id, p.lifecycle_state,
os.scope_kind, os.owner_ref, os.display_name as owner_display_name,
@@ -554,10 +610,10 @@ function deviceView(row, project) {
},
modelProfileRef: row.model_profile_ref,
displayName: row.display_name,
identifier: {
identifier: normalizeRestrictedIdentifierProjection({
kind: row.identifier_kind,
masked: row.identifier_masked,
},
}),
lifecycleState: row.lifecycle_state,
createdAt: toIso(row.created_at),
updatedAt: toIso(row.updated_at),
@@ -12,14 +12,23 @@ 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,
]);
export function normalizeDeviceManagementCommand(kind, input) {
if (isSensitiveReferenceManagementCommand(kind)) {
return normalizeSensitiveReferenceManagementCommand(kind, input);
}
if (isLifecycleManagementCommand(kind)) {
return normalizeLifecycleManagementCommand(kind, input);
}
@@ -17,6 +17,13 @@ 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,
@@ -34,6 +41,8 @@ const migrationFiles = [
"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",
];
export class PostgresDeviceRepository {
@@ -217,6 +226,13 @@ async function completeManagementReceipt(client, receiptId, result) {
}
async function applyManagementCommand(client, { commandKind, actor, command }) {
if (isSensitiveReferenceManagementCommand(commandKind)) {
return applySensitiveReferenceManagementCommand(client, {
commandKind,
actor,
command,
});
}
if (isLifecycleManagementCommand(commandKind)) {
return applyLifecycleManagementCommand(client, {
commandKind,
@@ -247,6 +263,13 @@ async function applyManagementCommand(client, { commandKind, actor, command }) {
}
async function authorizeManagementReplay(client, { commandKind, actor, command }) {
if (isSensitiveReferenceManagementCommand(commandKind)) {
return authorizeSensitiveReferenceManagementReplay(client, {
commandKind,
actor,
command,
});
}
if (isLifecycleManagementCommand(commandKind)) {
return authorizeLifecycleManagementReplay(client, {
commandKind,
@@ -0,0 +1,97 @@
import {
normalizeNdcCredentialReference,
} from "../../../../packages/external-provider-contract/src/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;
}
@@ -116,6 +116,84 @@ test("management API forwards claim as evidence references without identity inpu
}
});
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(options);
await new Promise((resolve, reject) => {
@@ -45,6 +45,7 @@ test("claims only matching observed enrollment evidence into direct ownership",
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"),
@@ -109,6 +110,7 @@ test("authorized transfer preserves history and detaches source collections", as
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: false }] }),
step("delete from device_collection_members", { rows: [], rowCount: 2 }),
step("update device_instances", {
rows: [deviceRow({
@@ -117,6 +119,7 @@ test("authorized transfer preserves history and detaches source collections", as
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"),
@@ -135,6 +138,37 @@ test("authorized transfer preserves history and detaches source collections", as
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(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);
});
@@ -0,0 +1,105 @@
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";
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("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,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);
}
@@ -1,6 +1,12 @@
import { EXTERNAL_PROVIDER_CONTRACT_VERSION } from "./contract-version.mjs";
import { isNdcCredentialReferenceValue } from "./credential-reference.mjs";
export { EXTERNAL_PROVIDER_CONTRACT_VERSION } from "./contract-version.mjs";
export {
NDC_CREDENTIAL_REFERENCE_OWNER,
isNdcCredentialReferenceValue,
normalizeNdcCredentialReference,
} from "./credential-reference.mjs";
export { validateIntakeBatch } from "./intake-batch.mjs";
export {
DATA_PRODUCT_GEOMETRY_TYPES,
@@ -199,6 +205,12 @@ export function validateConnectionProfile(value) {
if (value?.credentialRef?.owner && value.credentialRef.owner !== "ndc_l2_credentials") {
errors.push("credentialRef.owner_must_be_ndc_l2_credentials");
}
if (
typeof value?.credentialRef?.reference === "string"
&& !isNdcCredentialReferenceValue(value.credentialRef.reference)
) {
errors.push("credentialRef.reference_must_be_canonical_ndc_ref");
}
if (containsSecretLikeMaterial(value)) errors.push("profile_must_not_contain_secret_material");
if (value?.scope !== undefined) {
if (!isPlainObject(value.scope)) {
@@ -1,4 +1,6 @@
import { createHash } from "node:crypto";
import { isNdcCredentialReferenceValue } from "./credential-reference.mjs";
import { validateProviderPackage } from "./provider-package.mjs";
import {
compileTelemetryFieldProjection,
@@ -20,7 +22,6 @@ export const L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS = Object.freeze([
const HASH = /^(?:sha256:)?[a-f0-9]{64}$/;
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const CREDENTIAL_REF = /^ndc-credref:[A-Za-z0-9._:-]{8,255}$/;
const COMPILE_OPTION_KEYS = new Set(["telemetryFieldRegistry"]);
const RECEIPT_OPTION_KEYS = new Set(["graphRevision", "graphDigest", "materializedStepIds"]);
const STEP_RUNTIME_KINDS = Object.freeze({
@@ -478,7 +479,7 @@ function validateConnectionInstance(providerPackage, value) {
if (typeof value.connectionId !== "string" || !IDENTIFIER.test(value.connectionId)) {
throw new Error("l2_execution_plan_connection_id_invalid");
}
if (!CREDENTIAL_REF.test(String(value.credentialRefs?.provider?.reference || ""))
if (!isNdcCredentialReferenceValue(value.credentialRefs?.provider?.reference)
|| value.credentialRefs?.provider?.owner !== "ndc_l2_credentials") {
throw new Error("l2_execution_plan_provider_credential_ref_invalid");
}
@@ -4,6 +4,7 @@ export const L2_CONNECTION_INSTANCE_SCHEMA_VERSION = "nodedc.l2-connection-insta
export const SEMANTIC_MAPPING_SCHEMA_VERSION = "nodedc.semantic-mapping/v1";
import { SECRET_LIKE_VALUE as SECRET_VALUE } from "./sensitive-field-policy.mjs";
import { isNdcCredentialReferenceValue } from "./credential-reference.mjs";
import { isBoundedTelemetryReadings } from "./telemetry-readings.mjs";
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
@@ -1319,7 +1320,7 @@ function requiredString(value, path, errors) {
}
function requiredOpaqueReference(value, path, errors) {
if (typeof value !== "string" || !/^ndc-credref:[A-Za-z0-9][A-Za-z0-9._:-]{7,240}$/.test(value)) errors.push(`${path}_invalid`);
if (!isNdcCredentialReferenceValue(value)) errors.push(`${path}_invalid`);
}
function validateProviderBaseUrl(value, path, errors) {
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
import {
EXTERNAL_PROVIDER_CONTRACT_VERSION,
FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION,
NDC_CREDENTIAL_REFERENCE_OWNER,
assertValid,
isNdcCredentialReferenceValue,
normalizeNdcCredentialReference,
validateCollectionProfile,
validateConnectionProfile,
validateDataProduct,
@@ -19,6 +22,29 @@ assert.equal(validateConnectionProfile({
...geliosPositionsCurrentExample.connection,
credentialRef: { ...geliosPositionsCurrentExample.connection.credentialRef, owner: "engine" },
}).errors.includes("credentialRef.owner_must_be_ndc_l2_credentials"), true);
assert.deepEqual(normalizeNdcCredentialReference({
owner: NDC_CREDENTIAL_REFERENCE_OWNER,
reference: "ndc-credref:provider-example-0001",
}), {
owner: "ndc_l2_credentials",
reference: "ndc-credref:provider-example-0001",
});
assert.equal(isNdcCredentialReferenceValue("ndc-credref:provider-example-0001"), true);
assert.equal(isNdcCredentialReferenceValue("secret://provider-example"), false);
assert.equal(validateConnectionProfile({
...geliosPositionsCurrentExample.connection,
credentialRef: {
owner: "ndc_l2_credentials",
reference: "provider-example-0001",
},
}).errors.includes("credentialRef.reference_must_be_canonical_ndc_ref"), true);
assert.throws(
() => normalizeNdcCredentialReference({
owner: "device_core",
reference: "ndc-credref:provider-example-0001",
}),
/ndc_credential_reference_owner_invalid/,
);
assert.equal(validateCollectionProfile(geliosPositionsCurrentExample.collectionProfile).ok, true);
assert.equal(validateDataProduct(geliosPositionsCurrentExample.dataProduct).ok, true);
assert.equal(validateFoundryBinding(geliosPositionsCurrentExample.foundryBinding).ok, true);