@pdpp/cli 0.0.0 → 0.1.0-beta.2
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 +8 -5
- package/package.json +2 -1
- package/src/cache-layout.js +22 -0
- package/src/connect/flow.js +314 -0
- package/src/index.js +12 -21
- package/src/package-info.d.ts +20 -0
- package/src/package-info.js +4 -0
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@ Command-line tools for PDPP providers.
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
This package is the public npm home for the `pdpp` command. The
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
This package is the public npm home for the `pdpp` command. The beta CLI supports
|
|
8
|
+
`pdpp connect <provider-url>` for delegated access: the agent runs the command,
|
|
9
|
+
the owner approves scoped access in the browser, and the CLI stores scoped client
|
|
10
|
+
credentials in the project-local `.pdpp/` cache without asking for an owner
|
|
11
|
+
bearer token.
|
|
11
12
|
|
|
12
13
|
## Install
|
|
13
14
|
|
|
@@ -23,7 +24,9 @@ publication.
|
|
|
23
24
|
The intended npm scope is `@pdpp`, owned by the durable PDPP/Vana project
|
|
24
25
|
organization rather than an individual maintainer. Normal publication is handled
|
|
25
26
|
by semantic-release from GitHub Actions using npm trusted publishing/OIDC and
|
|
26
|
-
registry provenance.
|
|
27
|
+
registry provenance when the source repository is public. npm does not support
|
|
28
|
+
provenance for packages published from private GitHub repositories, so
|
|
29
|
+
`publishConfig.provenance` stays disabled until this repository is public.
|
|
27
30
|
|
|
28
31
|
After the package exists on npm, configure the trusted publisher with npm CLI
|
|
29
32
|
11.5.1+:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pdpp/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Command-line tools for PDPP providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./src/index.js",
|
|
11
|
+
"./cache-layout": "./src/cache-layout.js",
|
|
11
12
|
"./package-info": "./src/package-info.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mkdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function getPdppCacheLayout(cacheRoot = '.pdpp') {
|
|
5
|
+
return {
|
|
6
|
+
root: cacheRoot,
|
|
7
|
+
clientsDir: join(cacheRoot, 'clients'),
|
|
8
|
+
grantsDir: join(cacheRoot, 'grants'),
|
|
9
|
+
secretsDir: join(cacheRoot, 'secrets'),
|
|
10
|
+
accessFile: join(cacheRoot, 'agent-access.json'),
|
|
11
|
+
secretFile: (name) => join(cacheRoot, 'secrets', `${name}.secret`),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function writePdppSecretFile(path, value) {
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
17
|
+
writeFileSync(path, value, { mode: 0o600 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getFileMode(path) {
|
|
21
|
+
return statSync(path).mode & 0o777;
|
|
22
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const PROTECTED_RESOURCE_METADATA_PATH = '/.well-known/oauth-protected-resource';
|
|
5
|
+
const AUTHORIZATION_SERVER_METADATA_PATH = '/.well-known/oauth-authorization-server';
|
|
6
|
+
const DEFAULT_SCOPE = 'pdpp:read';
|
|
7
|
+
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
8
|
+
const DEFAULT_POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export class ConnectError extends Error {
|
|
11
|
+
constructor(code, message, exitCode = 69) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'ConnectError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.exitCode = exitCode;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function connectProvider(providerUrl, options = {}) {
|
|
20
|
+
const normalizedProviderUrl = normalizeProviderUrl(providerUrl);
|
|
21
|
+
if (!normalizedProviderUrl) {
|
|
22
|
+
throw new ConnectError('invalid_provider_url', `Invalid provider URL: ${providerUrl}`, 64);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
26
|
+
if (typeof fetchFn !== 'function') {
|
|
27
|
+
throw new ConnectError('fetch_unavailable', 'This Node runtime does not provide fetch().');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const io = options.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
31
|
+
const cacheRoot = options.cacheRoot ?? '.pdpp';
|
|
32
|
+
const scope = options.scope ?? DEFAULT_SCOPE;
|
|
33
|
+
const resourceMetadata = await discoverProtectedResourceMetadata(normalizedProviderUrl, fetchFn);
|
|
34
|
+
const cliDiscovery = resourceMetadata.pdpp_agent_discovery?.cli;
|
|
35
|
+
if (cliDiscovery?.no_owner_token === false) {
|
|
36
|
+
const policy = cliDiscovery.no_owner_token_policy ? ` Policy: ${cliDiscovery.no_owner_token_policy}.` : '';
|
|
37
|
+
throw new ConnectError(
|
|
38
|
+
'connect_not_supported',
|
|
39
|
+
`Provider metadata does not advertise a complete no-owner-token connect flow.${policy}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const authorizationServerUrl = selectAuthorizationServer(resourceMetadata, normalizedProviderUrl);
|
|
43
|
+
if (!authorizationServerUrl) {
|
|
44
|
+
throw new ConnectError('metadata_failure', 'Protected-resource metadata did not include a valid authorization server.');
|
|
45
|
+
}
|
|
46
|
+
const authorizationMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, fetchFn);
|
|
47
|
+
const connectEndpoint = findAgentConnectEndpoint(resourceMetadata, authorizationMetadata);
|
|
48
|
+
|
|
49
|
+
if (!connectEndpoint) {
|
|
50
|
+
throw new ConnectError(
|
|
51
|
+
'connect_not_supported',
|
|
52
|
+
'Provider metadata does not advertise a no-owner-token agent connect endpoint yet. Backend must expose pdpp_agent_discovery.agent_connect_endpoint or agent_connect_endpoint before this command can complete.'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const start = await postJson(fetchFn, connectEndpoint, {
|
|
57
|
+
resource: normalizedProviderUrl,
|
|
58
|
+
scope,
|
|
59
|
+
client_name: 'PDPP CLI',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const approvalUrl = start.approval_url ?? start.verification_uri_complete ?? start.verification_uri;
|
|
63
|
+
const pollUrl = start.poll_url ?? start.token_url ?? start.device_poll_endpoint ?? start.completion_endpoint;
|
|
64
|
+
if (!approvalUrl || !pollUrl) {
|
|
65
|
+
throw new ConnectError(
|
|
66
|
+
'connect_contract_invalid',
|
|
67
|
+
'Agent connect start response must include approval_url and a polling token URL.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
io.stdout.write(`Open this URL to approve access:\n${approvalUrl}\n`);
|
|
72
|
+
if (start.user_code) {
|
|
73
|
+
io.stdout.write(`Enter code: ${start.user_code}\n`);
|
|
74
|
+
}
|
|
75
|
+
io.stdout.write('Waiting for approval...\n');
|
|
76
|
+
|
|
77
|
+
const credential = await pollForCredential(fetchFn, pollUrl, {
|
|
78
|
+
intervalMs: Number(start.interval_ms ?? start.interval ?? options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS),
|
|
79
|
+
timeoutMs: options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
|
|
80
|
+
sleep: options.sleep,
|
|
81
|
+
now: options.now,
|
|
82
|
+
pollingCode: start.polling_code,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await verifySchema(fetchFn, normalizedProviderUrl, credential.access_token);
|
|
86
|
+
const cacheFile = await storeCredential(cacheRoot, normalizedProviderUrl, {
|
|
87
|
+
provider_url: normalizedProviderUrl,
|
|
88
|
+
authorization_server: authorizationServerUrl,
|
|
89
|
+
scope,
|
|
90
|
+
credential,
|
|
91
|
+
created_at: new Date((options.now?.() ?? Date.now())).toISOString(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
io.stdout.write(
|
|
95
|
+
`Connected to ${normalizedProviderUrl}\nStored scoped credentials in ${cacheFile}\nVerified /v1/schema\n`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
providerUrl: normalizedProviderUrl,
|
|
100
|
+
authorizationServerUrl,
|
|
101
|
+
cacheFile,
|
|
102
|
+
scope,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function normalizeProviderUrl(value) {
|
|
107
|
+
try {
|
|
108
|
+
const parsed = new URL(value.includes('://') ? value : `https://${value}`);
|
|
109
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
parsed.username = '';
|
|
113
|
+
parsed.password = '';
|
|
114
|
+
parsed.hash = '';
|
|
115
|
+
parsed.search = '';
|
|
116
|
+
parsed.pathname = trimTrailingSlash(parsed.pathname);
|
|
117
|
+
if (parsed.pathname === '/') {
|
|
118
|
+
parsed.pathname = '';
|
|
119
|
+
}
|
|
120
|
+
return parsed.toString().replace(/\/$/, '');
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function discoverProtectedResourceMetadata(providerUrl, fetchFn = globalThis.fetch) {
|
|
127
|
+
const metadataUrl = new URL(PROTECTED_RESOURCE_METADATA_PATH, providerUrl).toString();
|
|
128
|
+
const metadata = await getJson(fetchFn, metadataUrl, 'metadata_failure');
|
|
129
|
+
if (metadata.resource && normalizeProviderUrl(metadata.resource) !== providerUrl) {
|
|
130
|
+
throw new ConnectError(
|
|
131
|
+
'metadata_failure',
|
|
132
|
+
`Protected-resource metadata resource mismatch: expected ${providerUrl}.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return metadata;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function discoverAuthorizationServerMetadata(issuerUrl, fetchFn = globalThis.fetch) {
|
|
139
|
+
const metadataUrl = new URL(AUTHORIZATION_SERVER_METADATA_PATH, issuerUrl).toString();
|
|
140
|
+
const metadata = await getJson(fetchFn, metadataUrl, 'metadata_failure');
|
|
141
|
+
if (metadata.issuer && normalizeProviderUrl(metadata.issuer) !== normalizeProviderUrl(issuerUrl)) {
|
|
142
|
+
throw new ConnectError('metadata_failure', 'Authorization-server metadata issuer mismatch.');
|
|
143
|
+
}
|
|
144
|
+
return metadata;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function selectAuthorizationServer(resourceMetadata, providerUrl) {
|
|
148
|
+
const servers = resourceMetadata.authorization_servers;
|
|
149
|
+
const selected = Array.isArray(servers) ? servers[0] : resourceMetadata.authorization_server;
|
|
150
|
+
return normalizeProviderUrl(selected ?? providerUrl);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findAgentConnectEndpoint(resourceMetadata, authorizationMetadata) {
|
|
154
|
+
const endpoint =
|
|
155
|
+
authorizationMetadata.agent_connect_endpoint ??
|
|
156
|
+
authorizationMetadata.pdpp_agent_connect_endpoint ??
|
|
157
|
+
authorizationMetadata.pdpp_agent_discovery?.agent_connect_endpoint ??
|
|
158
|
+
resourceMetadata.agent_connect_endpoint ??
|
|
159
|
+
resourceMetadata.pdpp_agent_discovery?.agent_connect_endpoint;
|
|
160
|
+
if (!endpoint) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
return new URL(endpoint, authorizationMetadata.issuer ?? resourceMetadata.resource).toString();
|
|
165
|
+
} catch {
|
|
166
|
+
throw new ConnectError('metadata_failure', 'Agent connect endpoint in provider metadata is not a valid URL.');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function pollForCredential(fetchFn, pollUrl, options) {
|
|
171
|
+
const startedAt = options.now?.() ?? Date.now();
|
|
172
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
173
|
+
|
|
174
|
+
while ((options.now?.() ?? Date.now()) - startedAt <= options.timeoutMs) {
|
|
175
|
+
const result = await postJson(fetchFn, pollUrl, options.pollingCode ? { polling_code: options.pollingCode } : {});
|
|
176
|
+
const errorStatus = result.error?.code ?? result.error ?? result.code;
|
|
177
|
+
if (errorStatus === 'access_denied') {
|
|
178
|
+
throw new ConnectError('approval_denied', 'Owner denied the delegated access request.');
|
|
179
|
+
}
|
|
180
|
+
if (errorStatus === 'expired_token') {
|
|
181
|
+
throw new ConnectError('approval_expired', 'Delegated access approval expired. Run connect again.');
|
|
182
|
+
}
|
|
183
|
+
if (errorStatus === 'insufficient_scope') {
|
|
184
|
+
throw new ConnectError('insufficient_scope', 'Approved grant did not include the required PDPP scope.');
|
|
185
|
+
}
|
|
186
|
+
const status = result.status ?? (result.access_token ? 'approved' : 'pending');
|
|
187
|
+
|
|
188
|
+
if (status === 'approved') {
|
|
189
|
+
const credential = result.credential ?? result;
|
|
190
|
+
if (!credential.access_token) {
|
|
191
|
+
throw new ConnectError('token_verification_failed', 'Approved connect response did not include an access token.');
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
access_token: credential.access_token,
|
|
195
|
+
token_type: credential.token_type ?? 'Bearer',
|
|
196
|
+
expires_at: credential.expires_at,
|
|
197
|
+
scope: credential.scope,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (status === 'pending' || status === 'authorization_pending') {
|
|
202
|
+
await sleep(options.intervalMs);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (status === 'denied' || status === 'access_denied') {
|
|
207
|
+
throw new ConnectError('approval_denied', 'Owner denied the delegated access request.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (status === 'expired' || status === 'expired_token') {
|
|
211
|
+
throw new ConnectError('approval_expired', 'Delegated access approval expired. Run connect again.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (status === 'insufficient_scope') {
|
|
215
|
+
throw new ConnectError('insufficient_scope', 'Approved grant did not include the required PDPP scope.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
throw new ConnectError('connect_contract_invalid', `Unexpected connect polling status: ${status}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
throw new ConnectError('approval_expired', 'Timed out waiting for delegated access approval.');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function verifySchema(fetchFn, providerUrl, accessToken) {
|
|
225
|
+
const response = await fetchFn(new URL('/v1/schema', providerUrl), {
|
|
226
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
227
|
+
});
|
|
228
|
+
if (response.status === 403) {
|
|
229
|
+
throw new ConnectError('insufficient_scope', 'Grant cannot read /v1/schema; required scope is missing.');
|
|
230
|
+
}
|
|
231
|
+
if (response.status === 401) {
|
|
232
|
+
throw new ConnectError('token_verification_failed', 'Grant token was rejected by /v1/schema.');
|
|
233
|
+
}
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
throw new ConnectError('token_verification_failed', `/v1/schema verification failed with HTTP ${response.status}.`);
|
|
236
|
+
}
|
|
237
|
+
return response;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function storeCredential(cacheRoot, providerUrl, payload) {
|
|
241
|
+
const host = new URL(providerUrl).host.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
242
|
+
const cacheFile = join(cacheRoot, 'clients', `${host}.json`);
|
|
243
|
+
await mkdir(dirname(cacheFile), { recursive: true, mode: 0o700 });
|
|
244
|
+
await ensurePdppGitignore(cacheRoot);
|
|
245
|
+
await writeFile(cacheFile, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
246
|
+
return cacheFile;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function ensurePdppGitignore(cacheRoot) {
|
|
250
|
+
const gitignorePath = join(cacheRoot, '.gitignore');
|
|
251
|
+
let current = '';
|
|
252
|
+
try {
|
|
253
|
+
current = await readFile(gitignorePath, 'utf8');
|
|
254
|
+
} catch {}
|
|
255
|
+
if (current.includes('*')) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
|
|
259
|
+
const prefix = current && !current.endsWith('\n') ? `${current}\n` : current;
|
|
260
|
+
await writeFile(gitignorePath, `${prefix}*\n!.gitignore\n`, { mode: 0o600 });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function getJson(fetchFn, url, errorCode) {
|
|
264
|
+
let response;
|
|
265
|
+
try {
|
|
266
|
+
response = await fetchFn(url, { headers: { Accept: 'application/json' } });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new ConnectError(errorCode, `Failed to fetch ${url}: ${error.message}.`);
|
|
269
|
+
}
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new ConnectError(errorCode, `Failed to fetch ${url}: HTTP ${response.status}.`);
|
|
272
|
+
}
|
|
273
|
+
return response.json();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function postJson(fetchFn, url, body) {
|
|
277
|
+
let response;
|
|
278
|
+
try {
|
|
279
|
+
response = await fetchFn(url, {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
282
|
+
body: JSON.stringify(body),
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new ConnectError('connect_request_failed', `Connect request failed at ${url}: ${error.message}.`);
|
|
286
|
+
}
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
let body = null;
|
|
289
|
+
try {
|
|
290
|
+
body = await response.json();
|
|
291
|
+
} catch {}
|
|
292
|
+
const errorCode = body?.error?.code ?? body?.error ?? body?.code;
|
|
293
|
+
if (errorCode === 'access_denied') {
|
|
294
|
+
throw new ConnectError('approval_denied', 'Owner denied the delegated access request.');
|
|
295
|
+
}
|
|
296
|
+
if (errorCode === 'expired_token') {
|
|
297
|
+
throw new ConnectError('approval_expired', 'Delegated access approval expired. Run connect again.');
|
|
298
|
+
}
|
|
299
|
+
if (errorCode === 'insufficient_scope') {
|
|
300
|
+
throw new ConnectError('insufficient_scope', 'Approved grant did not include the required PDPP scope.');
|
|
301
|
+
}
|
|
302
|
+
if (errorCode === 'invalid_grant') {
|
|
303
|
+
throw new ConnectError('token_verification_failed', 'Provider rejected the agent-connect polling handle.');
|
|
304
|
+
}
|
|
305
|
+
const detail = body?.error?.message ?? body?.error_description ?? body?.message;
|
|
306
|
+
const suffix = detail ? ` ${detail}` : '';
|
|
307
|
+
throw new ConnectError('connect_request_failed', `Connect request failed at ${url}: HTTP ${response.status}.${suffix}`);
|
|
308
|
+
}
|
|
309
|
+
return response.json();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function trimTrailingSlash(value) {
|
|
313
|
+
return value.length > 1 ? value.replace(/\/+$/, '') : value;
|
|
314
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
createPdppCliCommand,
|
|
3
3
|
getPdppCliPackageInfo,
|
|
4
4
|
PDPP_CLI_BIN_NAME,
|
|
5
|
-
PDPP_CLI_PACKAGE_NAME,
|
|
6
5
|
} from './package-info.js';
|
|
6
|
+
import { ConnectError, connectProvider, normalizeProviderUrl } from './connect/flow.js';
|
|
7
7
|
|
|
8
8
|
const HELP = `PDPP CLI
|
|
9
9
|
|
|
@@ -16,7 +16,6 @@ Agent access:
|
|
|
16
16
|
${createPdppCliCommand()}
|
|
17
17
|
|
|
18
18
|
Notes:
|
|
19
|
-
connect is gated until the reference AS supports no-owner-token scoped grant completion.
|
|
20
19
|
Do not ask users for owner bearer tokens for routine delegated access.
|
|
21
20
|
`;
|
|
22
21
|
|
|
@@ -41,32 +40,22 @@ export async function runCli(argv, io = { stdout: process.stdout, stderr: proces
|
|
|
41
40
|
return 64;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
try {
|
|
44
|
+
await connectProvider(providerUrl, { io });
|
|
45
|
+
return 0;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof ConnectError) {
|
|
48
|
+
io.stderr.write(`${error.message}\n`);
|
|
49
|
+
return error.exitCode;
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
io.stderr.write(
|
|
51
|
-
`${PDPP_CLI_PACKAGE_NAME} connect is not enabled yet. The reference AS still needs no-owner-token scoped grant completion before this command can be advertised.\n`
|
|
52
|
-
);
|
|
53
|
-
return 69;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
io.stderr.write(`Unknown command: ${command}\n\n${HELP}\n`);
|
|
57
56
|
return 64;
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
export function normalizeProviderUrl(value) {
|
|
61
|
-
try {
|
|
62
|
-
const parsed = new URL(value.includes('://') ? value : `https://${value}`);
|
|
63
|
-
parsed.hash = '';
|
|
64
|
-
return parsed.origin;
|
|
65
|
-
} catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
59
|
function readOption(argv, name) {
|
|
71
60
|
const index = argv.indexOf(name);
|
|
72
61
|
if (index === -1) {
|
|
@@ -75,3 +64,5 @@ function readOption(argv, name) {
|
|
|
75
64
|
|
|
76
65
|
return argv[index + 1];
|
|
77
66
|
}
|
|
67
|
+
|
|
68
|
+
export { connectProvider, normalizeProviderUrl };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const PDPP_CLI_PACKAGE_NAME: string;
|
|
2
|
+
export const PDPP_CLI_BIN_NAME: string;
|
|
3
|
+
export const PDPP_CLI_VERSION_POLICY: "beta";
|
|
4
|
+
export const PDPP_CLI_PACKAGE_SPECIFIER: string;
|
|
5
|
+
export const PDPP_CLI_DEFAULT_CLIENT_ID: "pdpp_cli";
|
|
6
|
+
export const PDPP_CLI_NO_OWNER_TOKEN_POLICY: "owner_browser_approval_required";
|
|
7
|
+
|
|
8
|
+
export interface PdppCliPackageInfo {
|
|
9
|
+
packageName: string;
|
|
10
|
+
packageSpecifier: string;
|
|
11
|
+
binName: string;
|
|
12
|
+
defaultClientId: "pdpp_cli";
|
|
13
|
+
versionPolicy: "beta";
|
|
14
|
+
runCommand: string;
|
|
15
|
+
noOwnerToken: true;
|
|
16
|
+
noOwnerTokenPolicy: "owner_browser_approval_required";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createPdppCliCommand(providerUrl?: string): string;
|
|
20
|
+
export function getPdppCliPackageInfo(providerUrl?: string): PdppCliPackageInfo;
|
package/src/package-info.js
CHANGED
|
@@ -2,6 +2,8 @@ export const PDPP_CLI_PACKAGE_NAME = '@pdpp/cli';
|
|
|
2
2
|
export const PDPP_CLI_BIN_NAME = 'pdpp';
|
|
3
3
|
export const PDPP_CLI_VERSION_POLICY = 'beta';
|
|
4
4
|
export const PDPP_CLI_PACKAGE_SPECIFIER = `${PDPP_CLI_PACKAGE_NAME}@${PDPP_CLI_VERSION_POLICY}`;
|
|
5
|
+
export const PDPP_CLI_DEFAULT_CLIENT_ID = 'pdpp_cli';
|
|
6
|
+
export const PDPP_CLI_NO_OWNER_TOKEN_POLICY = 'owner_browser_approval_required';
|
|
5
7
|
|
|
6
8
|
export function createPdppCliCommand(providerUrl = '<provider-url>') {
|
|
7
9
|
return `npx -y ${PDPP_CLI_PACKAGE_SPECIFIER} connect ${providerUrl}`;
|
|
@@ -12,8 +14,10 @@ export function getPdppCliPackageInfo(providerUrl) {
|
|
|
12
14
|
packageName: PDPP_CLI_PACKAGE_NAME,
|
|
13
15
|
packageSpecifier: PDPP_CLI_PACKAGE_SPECIFIER,
|
|
14
16
|
binName: PDPP_CLI_BIN_NAME,
|
|
17
|
+
defaultClientId: PDPP_CLI_DEFAULT_CLIENT_ID,
|
|
15
18
|
versionPolicy: PDPP_CLI_VERSION_POLICY,
|
|
16
19
|
runCommand: createPdppCliCommand(providerUrl),
|
|
17
20
|
noOwnerToken: true,
|
|
21
|
+
noOwnerTokenPolicy: PDPP_CLI_NO_OWNER_TOKEN_POLICY,
|
|
18
22
|
};
|
|
19
23
|
}
|