Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/registry-property-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adcp/sdk': minor
---

Allow registry saveProperty/saveProperties writes to include full property identity facts: property_type, identifiers, and tags.
18 changes: 10 additions & 8 deletions bin/adcp-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,9 @@ SAVE COMMANDS (requires --auth):
Save or update a community brand
save-brand <domain> <name> @manifest.json
Save brand with manifest from file
save-property <domain> [agent-url] [payload-json]
save-property <domain> [payload-json]
Save or update a hosted property
save-property <domain> [agent-url] @property.json
save-property <domain> @property.json
Save property with full payload from file

LIST & SEARCH:
Expand Down Expand Up @@ -296,7 +296,7 @@ EXAMPLES:

# Save a property
adcp registry save-property example.com --auth sk_your_key
adcp registry save-property example.com https://agent.example.com --auth sk_your_key`);
adcp registry save-property example.com '{"properties":[{"property_type":"website","name":"Example","identifiers":[{"type":"domain","value":"example.com"}],"tags":["news"]}]}' --auth sk_your_key`);
}

/**
Expand Down Expand Up @@ -458,15 +458,17 @@ async function handleRegistryCommand(args) {
const domain = positional[1];
if (!domain) {
console.error('Error: domain is required\n');
console.error('Usage: adcp registry save-property <domain> [agent-url] [payload-json]\n');
console.error('Usage: adcp registry save-property <domain> [payload-json]\n');
return 2;
}
const extraArg = positional[2];
if (extraArg && !extraArg.startsWith('{') && !extraArg.startsWith('@')) {
console.error('Error: save-property no longer accepts an agent URL; pass identity facts as payload JSON\n');
console.error('Usage: adcp registry save-property <domain> [payload-json]\n');
return 2;
}
const maybeAgentUrl = positional[2];
const hasAgentUrl = maybeAgentUrl && !maybeAgentUrl.startsWith('{') && !maybeAgentUrl.startsWith('@');
const extraArg = hasAgentUrl ? positional[3] : positional[2];
const payload = {
publisher_domain: domain,
...(hasAgentUrl ? { authorized_agents: [{ url: maybeAgentUrl }] } : {}),
...(extraArg ? parsePayload(extraArg) : {}),
};
const result = await client.saveProperty(payload);
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type {
SaveBrandLogoResponse,
UploadBrandLogoInput,
UploadBrandLogoResponse,
SavePropertyIdentity,
SavePropertyRequest,
SavePropertyResponse,
BrandRegistryItem,
Expand Down
4 changes: 3 additions & 1 deletion src/lib/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
SaveBrandLogoResponse,
UploadBrandLogoInput,
UploadBrandLogoResponse,
SavePropertyIdentity,
SavePropertyRequest,
SavePropertyResponse,
ClaimHostedPropertyDomainResponse,
Expand Down Expand Up @@ -104,6 +105,7 @@ export type {
SaveBrandLogoResponse,
UploadBrandLogoInput,
UploadBrandLogoResponse,
SavePropertyIdentity,
SavePropertyRequest,
SavePropertyResponse,
ClaimHostedPropertyDomainResponse,
Expand Down Expand Up @@ -611,7 +613,7 @@ export class RegistryClient {
if (!this.apiKey) throw new Error('apiKey is required for save operations');
const payload: SavePropertyRequest = {
...property,
authorized_agents: property.authorized_agents ?? [],
authorized_agents: [],
};
return this.post(`${this.baseUrl}/api/properties/save`, payload);
}
Expand Down
59 changes: 57 additions & 2 deletions src/lib/registry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export type {
BrandRegistryItem,
ResolvedProperty,
PropertyIdentifier,
PropertyRegistryItem,
ValidationResult,
RegistryError,
PublisherPropertySelector,
Expand Down Expand Up @@ -57,6 +56,7 @@ export type { paths, operations, components } from './types.generated';

import type {
ResolvedBrand as GeneratedResolvedBrand,
PropertyRegistryItem as GeneratedPropertyRegistryItem,
operations,
CommunityMirrorListResponse,
CommunityMirrorSummary,
Expand All @@ -66,6 +66,7 @@ import type {
CommunityMirrorPublishRequest,
CommunityMirrorDeleteResponse,
} from './types.generated';
import type { PropertyIdentifierType, PropertyType } from '../discovery/types';
import type { MediaChannel, ProductFormatDeclaration } from '../types/tools.generated';

/**
Expand Down Expand Up @@ -118,8 +119,62 @@ export type SaveBrandRequest = NonNullable<operations['saveBrand']['requestBody'
/** Response from POST /api/brands/save (200) */
export type SaveBrandResponse = operations['saveBrand']['responses']['200']['content']['application/json'];

type RegistrySavePropertyRequest = NonNullable<
operations['saveProperty']['requestBody']
>['content']['application/json'];

type SavePropertyIdentityBase = {
/** Human-readable property name. */
name: string;
/** Register this property by known identifiers such as domain, bundle id, or app-store id. */
identifiers?: { type: PropertyIdentifierType; value: string }[];
/** Tags used by downstream `by_tag` property selection. */
tags?: string[];
};

/** Property identity accepted by POST /api/properties/save. */
export type SavePropertyIdentity = SavePropertyIdentityBase &
(
| {
/** Preferred field name, aligned with adagents.json property declarations. */
property_type: PropertyType;
/** Deprecated alias accepted for backwards compatibility. */
type?: PropertyType | string;
}
| {
/** Deprecated alias for `property_type`; accepted for backwards compatibility. */
type: PropertyType | string;
property_type?: PropertyType;
}
);

/** Property identity facts returned by registry property read/list APIs. */
export type RegistryPropertyIdentity = {
/** Stable property identifier when the registry has assigned one. */
id?: string;
/** Preferred field name, aligned with adagents.json property declarations. */
property_type?: PropertyType;
/** Legacy read/write alias for `property_type`. */
type?: PropertyType | string;
/** Human-readable property name. */
name?: string;
/** Known identifiers such as domain, bundle id, or app-store id. */
identifiers?: { type: PropertyIdentifierType; value: string }[];
/** Tags used by downstream `by_tag` property selection. */
tags?: string[];
};

/** Property registry list item, including identity facts when returned by the registry. */
export type PropertyRegistryItem = GeneratedPropertyRegistryItem & {
properties?: RegistryPropertyIdentity[];
};

/** Request body for POST /api/properties/save */
export type SavePropertyRequest = NonNullable<operations['saveProperty']['requestBody']>['content']['application/json'];
export type SavePropertyRequest = Omit<RegistrySavePropertyRequest, 'authorized_agents' | 'properties'> & {
/** Ignored by the registry client; identity-only property saves always write `authorized_agents: []`. */
authorized_agents?: [];
properties?: SavePropertyIdentity[];
};

/** Response from POST /api/properties/save (200) */
export type SavePropertyResponse = operations['saveProperty']['responses']['200']['content']['application/json'];
Expand Down
58 changes: 44 additions & 14 deletions test/lib/registry-cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,17 +430,11 @@ describe('CLI registry command', () => {
});
output = captureOutput();

const code = await handleRegistryCommand([
'save-property',
'example.com',
'https://agent.example.com',
'--auth',
'sk_test',
]);
const code = await handleRegistryCommand(['save-property', 'example.com', '--auth', 'sk_test']);

assert.strictEqual(code, 0);
assert.strictEqual(capturedBody.publisher_domain, 'example.com');
assert.deepStrictEqual(capturedBody.authorized_agents, [{ url: 'https://agent.example.com' }]);
assert.deepStrictEqual(capturedBody.authorized_agents, []);
assert.ok(output.stdout.includes('Saved successfully'));
assert.ok(output.stdout.includes('prop_456'));
});
Expand All @@ -456,7 +450,6 @@ describe('CLI registry command', () => {
const code = await handleRegistryCommand([
'save-property',
'example.com',
'https://agent.example.com',
'{"contact":{"email":"admin@example.com"}}',
'--auth',
'sk_test',
Expand All @@ -467,6 +460,35 @@ describe('CLI registry command', () => {
assert.strictEqual(capturedBody.contact.email, 'admin@example.com');
});

test('saves a property with identity facts payload JSON', async () => {
let capturedBody;
restoreFetch = mockFetch(async (_url, opts) => {
capturedBody = JSON.parse(opts.body);
return new Response(JSON.stringify(SAVE_RESULT), { status: 200 });
});
output = captureOutput();

const code = await handleRegistryCommand([
'save-property',
'example.com',
'{"properties":[{"property_type":"website","name":"Example","identifiers":[{"type":"domain","value":"example.com"}],"tags":["news"]}]}',
'--auth',
'sk_test',
]);

assert.strictEqual(code, 0);
assert.strictEqual(capturedBody.publisher_domain, 'example.com');
assert.deepStrictEqual(capturedBody.authorized_agents, []);
assert.deepStrictEqual(capturedBody.properties, [
{
property_type: 'website',
name: 'Example',
identifiers: [{ type: 'domain', value: 'example.com' }],
tags: ['news'],
},
]);
});

test('saves a property without an authorized agent URL', async () => {
let capturedBody;
restoreFetch = mockFetch(async (_url, opts) => {
Expand Down Expand Up @@ -508,18 +530,26 @@ describe('CLI registry command', () => {
restoreFetch = mockFetch(async () => new Response(JSON.stringify(SAVE_RESULT), { status: 200 }));
output = captureOutput();

const code = await handleRegistryCommand(['save-property', 'example.com', '--auth', 'sk_test', '--json']);

assert.strictEqual(code, 0);
const parsed = JSON.parse(output.stdout);
assert.strictEqual(parsed.id, 'prop_456');
});

test('returns exit code 2 for legacy authorized agent URL positional', async () => {
output = captureOutput();

const code = await handleRegistryCommand([
'save-property',
'example.com',
'https://agent.example.com',
'--auth',
'sk_test',
'--json',
]);

assert.strictEqual(code, 0);
const parsed = JSON.parse(output.stdout);
assert.strictEqual(parsed.id, 'prop_456');
assert.strictEqual(code, 2);
assert.ok(output.stderr.includes('no longer accepts an agent URL'));
});

test('returns exit code 2 when domain is missing', async () => {
Expand All @@ -535,7 +565,7 @@ describe('CLI registry command', () => {
output = captureOutput();

try {
const code = await handleRegistryCommand(['save-property', 'example.com', 'https://agent.example.com']);
const code = await handleRegistryCommand(['save-property', 'example.com']);
assert.strictEqual(code, 1);
assert.ok(output.stderr.includes('apiKey is required'));
} finally {
Expand Down
Loading
Loading