@pdpp/cli 0.1.0-beta.7 → 0.1.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/package.json +1 -1
- package/src/index.js +51 -0
- package/src/owner-agent/command.js +368 -0
- package/src/owner-agent/control.js +138 -0
- package/src/owner-agent/credential-store.js +126 -0
- package/src/owner-agent/device-flow.js +145 -0
- package/src/owner-agent/discovery.js +233 -0
- package/src/owner-agent/errors.js +13 -0
- package/src/owner-agent/lifecycle.js +126 -0
- package/src/owner-agent/setup.js +378 -0
- package/src/read/commands.js +250 -0
- package/src/ref/auth.js +179 -0
- package/src/ref/commands/call.js +168 -0
- package/src/ref/commands/connectors.js +44 -4
- package/src/ref/commands/event-subscriptions.js +190 -0
- package/src/ref/commands/grant.js +3 -1
- package/src/ref/commands/run.js +3 -1
- package/src/ref/commands/trace.js +3 -1
- package/src/ref/output.js +44 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Introspection and RFC 7592 client-delete revocation for owner-agent
|
|
2
|
+
// credentials. These preserve the existing reference behavior so a revoked
|
|
3
|
+
// owner-agent credential stops working and an active one can be confirmed
|
|
4
|
+
// without printing the bearer.
|
|
5
|
+
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
|
|
8
|
+
import { OwnerAgentError } from './errors.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Introspect a stored owner-agent credential. Returns the non-secret subset of
|
|
12
|
+
* the introspection response (`active`, `token_kind`/`pdpp_token_kind`, `sub`,
|
|
13
|
+
* `client_id`, `exp`, `scope`). Never returns the bearer.
|
|
14
|
+
*/
|
|
15
|
+
export async function introspectOwnerAgentCredential({ fetchFn, record }) {
|
|
16
|
+
if (!record?.introspection_endpoint) {
|
|
17
|
+
throw new OwnerAgentError('introspection_unavailable', 'Stored credential has no introspection endpoint.');
|
|
18
|
+
}
|
|
19
|
+
const token = getOwnerAgentAccessToken(record);
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new OwnerAgentError('credential_invalid', 'Stored credential is missing an access token.');
|
|
22
|
+
}
|
|
23
|
+
const body = new URLSearchParams();
|
|
24
|
+
body.set('token', token);
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetchFn(record.introspection_endpoint, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
Accept: 'application/json',
|
|
31
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
},
|
|
34
|
+
body: body.toString(),
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new OwnerAgentError('request_failed', `Introspection request failed: ${error.message}.`);
|
|
38
|
+
}
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new OwnerAgentError('introspection_failed', `Introspection failed with HTTP ${response.status}.`);
|
|
41
|
+
}
|
|
42
|
+
let json;
|
|
43
|
+
try {
|
|
44
|
+
json = await response.json();
|
|
45
|
+
} catch {
|
|
46
|
+
throw new OwnerAgentError('introspection_failed', 'Introspection response was not valid JSON.');
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
active: Boolean(json.active),
|
|
50
|
+
token_kind: json.pdpp_token_kind ?? json.token_kind ?? null,
|
|
51
|
+
sub: json.sub ?? null,
|
|
52
|
+
client_id: json.client_id ?? null,
|
|
53
|
+
exp: json.exp ?? null,
|
|
54
|
+
scope: json.scope ?? null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Revoke an owner-agent credential via RFC 7592 client delete. The reference
|
|
60
|
+
* implementation authenticates this route with the owner session for the
|
|
61
|
+
* approving owner, not with a registration access token.
|
|
62
|
+
*/
|
|
63
|
+
export async function revokeOwnerAgentCredential({ fetchFn, record, ownerSessionCookie }) {
|
|
64
|
+
const uri = record?.registration_client_uri;
|
|
65
|
+
if (!uri) {
|
|
66
|
+
throw new OwnerAgentError(
|
|
67
|
+
'revocation_unavailable',
|
|
68
|
+
'Stored credential has no RFC 7592 registration handle (registration_client_uri). ' +
|
|
69
|
+
'Revoke it from the owner dashboard instead.'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (!ownerSessionCookie) {
|
|
73
|
+
throw new OwnerAgentError(
|
|
74
|
+
'owner_session_required',
|
|
75
|
+
'Revocation requires an owner session. Run `pdpp ref login <authorization-server>` first, or set PDPP_OWNER_SESSION_COOKIE.',
|
|
76
|
+
5
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
let response;
|
|
80
|
+
try {
|
|
81
|
+
response = await fetchFn(uri, {
|
|
82
|
+
method: 'DELETE',
|
|
83
|
+
headers: { Cookie: normalizeOwnerSessionCookie(ownerSessionCookie), Accept: 'application/json' },
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new OwnerAgentError('request_failed', `Revocation request failed: ${error.message}.`);
|
|
87
|
+
}
|
|
88
|
+
// RFC 7592 specifies 204 No Content on successful delete.
|
|
89
|
+
if (response.status === 204 || response.status === 200) {
|
|
90
|
+
return { revoked: true };
|
|
91
|
+
}
|
|
92
|
+
if (response.status === 401 || response.status === 403) {
|
|
93
|
+
throw new OwnerAgentError('revocation_unauthorized', `Revocation rejected (HTTP ${response.status}).`, 4);
|
|
94
|
+
}
|
|
95
|
+
if (response.status === 404) {
|
|
96
|
+
// Already gone is an acceptable terminal state for revocation.
|
|
97
|
+
return { revoked: true, already_absent: true };
|
|
98
|
+
}
|
|
99
|
+
throw new OwnerAgentError('revocation_failed', `Revocation failed with HTTP ${response.status}.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getOwnerAgentAccessToken(record) {
|
|
103
|
+
return record?.access_token ?? record?.credential?.access_token ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function readCredentialRecord(targetPath) {
|
|
107
|
+
let raw;
|
|
108
|
+
try {
|
|
109
|
+
raw = await readFile(targetPath, 'utf8');
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error?.code === 'ENOENT') {
|
|
112
|
+
throw new OwnerAgentError('not_onboarded', `No owner-agent credential found at ${targetPath}.`, 5);
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
} catch {
|
|
119
|
+
throw new OwnerAgentError('credential_invalid', `Owner-agent credential at ${targetPath} is not valid JSON.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeOwnerSessionCookie(value) {
|
|
124
|
+
const raw = String(value || '').trim();
|
|
125
|
+
return raw.includes('=') ? raw : `pdpp_owner_session=${raw}`;
|
|
126
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// Owner-agent connection setup planning for the `pdpp owner-agent setup` and
|
|
2
|
+
// `pdpp owner-agent connectors` subcommands.
|
|
3
|
+
//
|
|
4
|
+
// A trusted local owner agent (or a human at the CLI) needs the SAME
|
|
5
|
+
// non-secret setup plan and next-step contract that the console add-connection
|
|
6
|
+
// flow and the owner-agent REST route surface. This module is a thin consumer
|
|
7
|
+
// of the server's `GET /v1/owner/connector-templates` and
|
|
8
|
+
// `POST /v1/owner/connections/intents` routes — it does not re-classify
|
|
9
|
+
// connectors, invent modalities, or maintain a supported-connector list. The
|
|
10
|
+
// server's connection setup planner is the single source of truth; the CLI only
|
|
11
|
+
// formats what the planner returns.
|
|
12
|
+
//
|
|
13
|
+
// Secret boundary (design.md Decision 5, "agent help is allowed; agent-held
|
|
14
|
+
// secrets are not"): the owner bearer is read from the stored credential and
|
|
15
|
+
// sent ONLY as an `Authorization: Bearer` header. It is never printed. The
|
|
16
|
+
// route response carries no provider credentials, owner cookies, browser
|
|
17
|
+
// cookies, or grant-scoped MCP bearers; it may carry an owner-openable
|
|
18
|
+
// enrollment code and route names, which are setup material, not secrets.
|
|
19
|
+
|
|
20
|
+
import { OwnerAgentError } from './errors.js';
|
|
21
|
+
import { getOwnerAgentAccessToken } from './lifecycle.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Request an owner-mediated connection setup plan from the reference's
|
|
25
|
+
* owner-agent intent route. Sends the owner bearer only as an Authorization
|
|
26
|
+
* header and returns the parsed (non-secret) intent body.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} args
|
|
29
|
+
* @param {typeof fetch} args.fetchFn
|
|
30
|
+
* @param {object} args.record stored owner-agent credential record
|
|
31
|
+
* @param {string} args.connectorId connector id/key the owner wants to add
|
|
32
|
+
* @param {string|null} [args.displayName] optional owner-meaningful label
|
|
33
|
+
* @returns {Promise<object>} the parsed `owner_connection_intent` body
|
|
34
|
+
*/
|
|
35
|
+
export async function requestConnectionSetupPlan({ fetchFn, record, connectorId, displayName }) {
|
|
36
|
+
const token = getOwnerAgentAccessToken(record);
|
|
37
|
+
if (!token) {
|
|
38
|
+
throw new OwnerAgentError('credential_invalid', 'Stored credential is missing an access token.');
|
|
39
|
+
}
|
|
40
|
+
const resource = typeof record?.resource === 'string' ? record.resource.replace(/\/$/, '') : null;
|
|
41
|
+
if (!resource) {
|
|
42
|
+
throw new OwnerAgentError(
|
|
43
|
+
'credential_invalid',
|
|
44
|
+
'Stored credential has no resource origin; re-run `pdpp owner-agent onboard`.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const trimmedConnector = typeof connectorId === 'string' ? connectorId.trim() : '';
|
|
48
|
+
if (!trimmedConnector) {
|
|
49
|
+
throw new OwnerAgentError(
|
|
50
|
+
'invalid_request',
|
|
51
|
+
'Usage: pdpp owner-agent setup <connector-id> [--display-name <name>]',
|
|
52
|
+
64
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const body = { connector_id: trimmedConnector };
|
|
57
|
+
const trimmedDisplayName = typeof displayName === 'string' ? displayName.trim() : '';
|
|
58
|
+
if (trimmedDisplayName) {
|
|
59
|
+
body.display_name = trimmedDisplayName;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const url = `${resource}/v1/owner/connections/intents`;
|
|
63
|
+
let response;
|
|
64
|
+
try {
|
|
65
|
+
response = await fetchFn(url, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
Accept: 'application/json',
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
Authorization: `Bearer ${token}`,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new OwnerAgentError('setup_failed', `Failed to request setup plan from ${url}: ${error.message}.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (response.status === 401 || response.status === 403) {
|
|
79
|
+
throw new OwnerAgentError(
|
|
80
|
+
'setup_unauthorized',
|
|
81
|
+
`Owner-agent setup is not authorized (HTTP ${response.status}). The credential may be revoked or inactive; run \`pdpp owner-agent status\`.`,
|
|
82
|
+
4
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let json = null;
|
|
87
|
+
try {
|
|
88
|
+
json = await response.json();
|
|
89
|
+
} catch {
|
|
90
|
+
json = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const code = json?.error?.code ?? json?.error ?? `http_${response.status}`;
|
|
95
|
+
const message = json?.error?.message ?? json?.message ?? null;
|
|
96
|
+
const detail = typeof message === 'string' && message.trim() ? `: ${message.trim()}` : '';
|
|
97
|
+
throw new OwnerAgentError('setup_failed', `Setup plan request failed (${code})${detail}.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!json || typeof json !== 'object') {
|
|
101
|
+
throw new OwnerAgentError('setup_failed', `Response from ${url} was not a valid setup plan.`);
|
|
102
|
+
}
|
|
103
|
+
return json;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function requestConnectorTemplates({ fetchFn, record }) {
|
|
107
|
+
const token = getOwnerAgentAccessToken(record);
|
|
108
|
+
if (!token) {
|
|
109
|
+
throw new OwnerAgentError('credential_invalid', 'Stored credential is missing an access token.');
|
|
110
|
+
}
|
|
111
|
+
const resource = typeof record?.resource === 'string' ? record.resource.replace(/\/$/, '') : null;
|
|
112
|
+
if (!resource) {
|
|
113
|
+
throw new OwnerAgentError(
|
|
114
|
+
'credential_invalid',
|
|
115
|
+
'Stored credential has no resource origin; re-run `pdpp owner-agent onboard`.'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const url = `${resource}/v1/owner/connector-templates`;
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await fetchFn(url, {
|
|
122
|
+
headers: {
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
Authorization: `Bearer ${token}`,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new OwnerAgentError('templates_failed', `Failed to request connector templates from ${url}: ${error.message}.`);
|
|
129
|
+
}
|
|
130
|
+
if (response.status === 401 || response.status === 403) {
|
|
131
|
+
throw new OwnerAgentError(
|
|
132
|
+
'setup_unauthorized',
|
|
133
|
+
`Owner-agent connector discovery is not authorized (HTTP ${response.status}). The credential may be revoked or inactive; run \`pdpp owner-agent status\`.`,
|
|
134
|
+
4
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
let json = null;
|
|
138
|
+
try {
|
|
139
|
+
json = await response.json();
|
|
140
|
+
} catch {
|
|
141
|
+
json = null;
|
|
142
|
+
}
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const code = json?.error?.code ?? json?.error ?? `http_${response.status}`;
|
|
145
|
+
throw new OwnerAgentError('templates_failed', `Connector template request failed (${code}).`);
|
|
146
|
+
}
|
|
147
|
+
return Array.isArray(json?.data) ? json.data : [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Maps planner support state + next step to a concise, honest status label. The
|
|
151
|
+
// support state is the source of truth; next_step.kind only explains the owner's
|
|
152
|
+
// next action.
|
|
153
|
+
function describeSetupStatus(supportState, kind) {
|
|
154
|
+
switch (supportState) {
|
|
155
|
+
case 'supported':
|
|
156
|
+
return { label: 'supported', summary: 'This setup path can start now.' };
|
|
157
|
+
case 'proof_gated':
|
|
158
|
+
return { label: 'proof-gated', summary: 'A setup path exists, but support is not flipped without live proof.' };
|
|
159
|
+
case 'needs_deployment_config':
|
|
160
|
+
return { label: 'deployment-blocked', summary: 'An instance-level prerequisite is missing.' };
|
|
161
|
+
case 'unsupported':
|
|
162
|
+
return { label: 'unsupported', summary: 'No reference setup path for this connector yet.' };
|
|
163
|
+
default:
|
|
164
|
+
return { label: kind ? `next-step:${kind}` : 'unknown', summary: 'See the next-step details below.' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Field names that carry owner-openable setup material the agent may surface
|
|
169
|
+
// (codes, route names, URLs, expiries). Everything else in `next_step` is
|
|
170
|
+
// rendered generically. No field here is a provider/credential secret: the
|
|
171
|
+
// route never returns those.
|
|
172
|
+
const NEXT_STEP_DETAIL_KEYS = [
|
|
173
|
+
['enroll_endpoint', 'enroll endpoint'],
|
|
174
|
+
['enrollment_code', 'enrollment code'],
|
|
175
|
+
['local_binding_name', 'local binding name'],
|
|
176
|
+
['capture_endpoint', 'capture endpoint'],
|
|
177
|
+
['authorization_url', 'authorization url'],
|
|
178
|
+
['runbook_path', 'runbook'],
|
|
179
|
+
['expires_at', 'expires'],
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Format an `owner_connection_intent` setup plan into a non-secret,
|
|
184
|
+
* token-efficient text report. Returns the string the command writes to stdout.
|
|
185
|
+
* Renders the support label, modality, connection-active state, the primary
|
|
186
|
+
* next step with its reason, and any owner-openable next-step details.
|
|
187
|
+
*/
|
|
188
|
+
export function formatConnectionSetupPlan(plan) {
|
|
189
|
+
const connectorKey = plan?.connector_key ?? plan?.connector_id ?? '(unknown connector)';
|
|
190
|
+
const connectorModality = plan?.connector_modality ?? 'unknown';
|
|
191
|
+
const setupModality = plan?.setup_modality ?? 'unknown';
|
|
192
|
+
const supportState = plan?.support_state ?? null;
|
|
193
|
+
const nextStep = plan?.next_step && typeof plan.next_step === 'object' ? plan.next_step : {};
|
|
194
|
+
const kind = typeof nextStep.kind === 'string' ? nextStep.kind : null;
|
|
195
|
+
const status = describeSetupStatus(supportState, kind);
|
|
196
|
+
const deployment = plan?.deployment_readiness && typeof plan.deployment_readiness === 'object'
|
|
197
|
+
? plan.deployment_readiness
|
|
198
|
+
: null;
|
|
199
|
+
|
|
200
|
+
const lines = [];
|
|
201
|
+
lines.push(`Connection setup plan for ${connectorKey} (non-secret):`);
|
|
202
|
+
lines.push(` status: ${status.label} — ${status.summary}`);
|
|
203
|
+
lines.push(` setup modality: ${setupModality}`);
|
|
204
|
+
lines.push(` connector modality: ${connectorModality}`);
|
|
205
|
+
if (typeof plan?.validation === 'string') {
|
|
206
|
+
lines.push(
|
|
207
|
+
` credential validation: ${plan.validation}${
|
|
208
|
+
plan.validation === 'synchronous'
|
|
209
|
+
? ' (the credential is checked and the account identity echoed before storing)'
|
|
210
|
+
: ' (the connection activates when the first sync accepts records)'
|
|
211
|
+
}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
lines.push(` connection active: ${plan?.connection_active === true ? 'yes' : 'no (materializes when the owner step completes)'}`);
|
|
215
|
+
if (deployment?.state && deployment.state !== 'not_applicable') {
|
|
216
|
+
lines.push(` deployment readiness: ${deployment.state}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
lines.push(` Next step: ${kind ?? '(none)'}`);
|
|
220
|
+
if (typeof nextStep.reason === 'string' && nextStep.reason.trim()) {
|
|
221
|
+
lines.push(` ${nextStep.reason.trim()}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const detailLines = [];
|
|
225
|
+
for (const [key, label] of NEXT_STEP_DETAIL_KEYS) {
|
|
226
|
+
const value = nextStep[key];
|
|
227
|
+
if (typeof value === 'string' && value.trim()) {
|
|
228
|
+
detailLines.push(` ${label}: ${value.trim()}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (detailLines.length > 0) {
|
|
232
|
+
lines.push('');
|
|
233
|
+
lines.push(' Details:');
|
|
234
|
+
lines.push(...detailLines);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const blockers = Array.isArray(deployment?.blockers) ? deployment.blockers : [];
|
|
238
|
+
if (blockers.length > 0) {
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push(' Deployment blockers:');
|
|
241
|
+
for (const blocker of blockers) {
|
|
242
|
+
const label = typeof blocker?.label === 'string' && blocker.label.trim() ? blocker.label.trim() : blocker?.key;
|
|
243
|
+
if (typeof label === 'string' && label.trim()) {
|
|
244
|
+
lines.push(` ${label.trim()}${blocker?.secret === true ? ' (secret)' : ''}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push(' Note: provider secrets are captured only through owner-mediated flows;');
|
|
251
|
+
lines.push(' this plan and the owner bearer are never exposed to /mcp or grant-scoped reads.');
|
|
252
|
+
return `${lines.join('\n')}\n`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function templateSupportStatus(template) {
|
|
256
|
+
const setupPlan = template?.setup_plan && typeof template.setup_plan === 'object' ? template.setup_plan : null;
|
|
257
|
+
if (typeof setupPlan?.support_state === 'string') {
|
|
258
|
+
return setupPlan.support_state;
|
|
259
|
+
}
|
|
260
|
+
const actions = Array.isArray(template?.supported_actions) ? template.supported_actions : [];
|
|
261
|
+
const initiate = actions.find((action) => action?.family === 'initiate_connection');
|
|
262
|
+
return typeof initiate?.status === 'string' ? initiate.status : 'unknown';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function templateNextStep(template) {
|
|
266
|
+
const setupPlan = template?.setup_plan && typeof template.setup_plan === 'object' ? template.setup_plan : null;
|
|
267
|
+
if (typeof setupPlan?.next_step_kind === 'string') {
|
|
268
|
+
return setupPlan.next_step_kind;
|
|
269
|
+
}
|
|
270
|
+
const actions = Array.isArray(template?.supported_actions) ? template.supported_actions : [];
|
|
271
|
+
const initiate = actions.find((action) => action?.family === 'initiate_connection');
|
|
272
|
+
return typeof initiate?.reason === 'string' ? initiate.reason : 'unknown';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function templateMatches(template, query) {
|
|
276
|
+
const needle = query.trim().toLowerCase();
|
|
277
|
+
if (!needle) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
return [
|
|
281
|
+
template?.display_name,
|
|
282
|
+
template?.connector_id,
|
|
283
|
+
template?.connector_key,
|
|
284
|
+
template?.connector_modality,
|
|
285
|
+
templateSupportStatus(template),
|
|
286
|
+
templateNextStep(template),
|
|
287
|
+
]
|
|
288
|
+
.filter((value) => typeof value === 'string')
|
|
289
|
+
.join(' ')
|
|
290
|
+
.toLowerCase()
|
|
291
|
+
.includes(needle);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function sortedTemplates(templates) {
|
|
295
|
+
return [...templates].sort((left, right) => {
|
|
296
|
+
const leftName = left?.display_name ?? left?.connector_key ?? '';
|
|
297
|
+
const rightName = right?.display_name ?? right?.connector_key ?? '';
|
|
298
|
+
return String(leftName).localeCompare(String(rightName));
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function findConnectorTemplates(templates, query) {
|
|
303
|
+
return sortedTemplates(templates).filter((template) => templateMatches(template, query));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function formatConnectorTemplates(templates, { query = '' } = {}) {
|
|
307
|
+
const rows = findConnectorTemplates(templates, query);
|
|
308
|
+
const lines = [];
|
|
309
|
+
lines.push(query ? `Connector setup catalog matching "${query}" (non-secret):` : 'Connector setup catalog (non-secret):');
|
|
310
|
+
if (rows.length === 0) {
|
|
311
|
+
lines.push(' (no matching connector templates)');
|
|
312
|
+
return `${lines.join('\n')}\n`;
|
|
313
|
+
}
|
|
314
|
+
for (const template of rows) {
|
|
315
|
+
const connectorKey = template?.connector_key ?? template?.connector_id ?? '(unknown)';
|
|
316
|
+
const displayName = template?.display_name ?? connectorKey;
|
|
317
|
+
const status = templateSupportStatus(template);
|
|
318
|
+
const next = templateNextStep(template);
|
|
319
|
+
const connectionCount = Number.isFinite(template?.connection_count) ? template.connection_count : 0;
|
|
320
|
+
lines.push(` - ${displayName} connector=${connectorKey} status=${status} connections=${connectionCount}`);
|
|
321
|
+
lines.push(` next: ${next}`);
|
|
322
|
+
lines.push(` explain: pdpp owner-agent connectors explain ${connectorKey}`);
|
|
323
|
+
}
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push(' Start setup with: pdpp owner-agent setup <connector-id> [--display-name <name>]');
|
|
326
|
+
lines.push(' Note: setup may mint short-lived enrollment material; explain/list/search are read-only.');
|
|
327
|
+
return `${lines.join('\n')}\n`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function formatConnectorTemplateExplain(template) {
|
|
331
|
+
if (!template) {
|
|
332
|
+
throw new OwnerAgentError('not_found', 'No matching connector template found.', 1);
|
|
333
|
+
}
|
|
334
|
+
const connectorKey = template?.connector_key ?? template?.connector_id ?? '(unknown)';
|
|
335
|
+
const displayName = template?.display_name ?? connectorKey;
|
|
336
|
+
const setupPlan = template?.setup_plan && typeof template.setup_plan === 'object' ? template.setup_plan : {};
|
|
337
|
+
const lines = [];
|
|
338
|
+
lines.push(`Connector setup preview for ${displayName} (${connectorKey}) — non-secret, read-only:`);
|
|
339
|
+
lines.push(` status: ${templateSupportStatus(template)}`);
|
|
340
|
+
lines.push(` connector modality: ${template?.connector_modality ?? 'unknown'}`);
|
|
341
|
+
lines.push(` setup modality: ${setupPlan.setup_modality ?? 'unknown'}`);
|
|
342
|
+
if (typeof setupPlan.validation === 'string') {
|
|
343
|
+
lines.push(` credential validation: ${setupPlan.validation}`);
|
|
344
|
+
}
|
|
345
|
+
lines.push(` next step: ${templateNextStep(template)}`);
|
|
346
|
+
if (setupPlan.proof_gate) {
|
|
347
|
+
lines.push(` proof gate: ${setupPlan.proof_gate}`);
|
|
348
|
+
}
|
|
349
|
+
if (setupPlan.runbook_path) {
|
|
350
|
+
lines.push(` runbook: ${setupPlan.runbook_path}`);
|
|
351
|
+
}
|
|
352
|
+
const blockers = Array.isArray(setupPlan.deployment_readiness?.blockers)
|
|
353
|
+
? setupPlan.deployment_readiness.blockers
|
|
354
|
+
: [];
|
|
355
|
+
if (blockers.length > 0) {
|
|
356
|
+
lines.push(' deployment blockers:');
|
|
357
|
+
for (const blocker of blockers) {
|
|
358
|
+
const label = typeof blocker?.label === 'string' ? blocker.label : blocker?.key;
|
|
359
|
+
if (label) {
|
|
360
|
+
lines.push(` ${label}${blocker?.secret === true ? ' (secret)' : ''}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const connections = Array.isArray(template?.connections) ? template.connections : [];
|
|
365
|
+
lines.push(` existing connections: ${connections.length}`);
|
|
366
|
+
for (const connection of connections) {
|
|
367
|
+
const connectionId = connection?.connection_id ?? connection?.connector_instance_id ?? '(no connection_id)';
|
|
368
|
+
const display = typeof connection?.display_name === 'string' && connection.display_name.trim()
|
|
369
|
+
? ` "${connection.display_name.trim()}"`
|
|
370
|
+
: '';
|
|
371
|
+
lines.push(` - ${connectionId}${display}`);
|
|
372
|
+
}
|
|
373
|
+
lines.push('');
|
|
374
|
+
lines.push(` Start setup: pdpp owner-agent setup ${connectorKey} --display-name "<name>"`);
|
|
375
|
+
lines.push(' Repeat setup with another display name to add another account when this connector supports it.');
|
|
376
|
+
lines.push(' This preview did not mint enrollment codes, provider secrets, owner cookies, or grant-scoped MCP bearers.');
|
|
377
|
+
return `${lines.join('\n')}\n`;
|
|
378
|
+
}
|